iOS開發之再探多執行緒程式設計:Grand Central Dispatch詳解
Swift3.0相關程式碼已在github上更新。之前關於iOS開發多執行緒的內容釋出過一篇部落格,其中介紹了NSThread、操作佇列以及GCD,介紹的不夠深入。今天就以GCD為主題來全面的總結一下GCD的使用方式。GCD的歷史以及好處在此就不做過多的贅述了。本篇部落格會通過一系列的例項來好好的總結一下GCD。GCD在iOS開發中還是比較重要的,使用場景也是非常多的,處理一些比較耗時的任務時基本上都會使用到GCD, 在使用是我們也要主要一些執行緒安全也死鎖的東西。
本篇部落格中對iOS中的GCD技術進行了較為全面的總結,下方模擬器的截圖就是我們今天要介紹的內容,都是關於GCD的。下方檢視控制器中每點選一個Button都會使用GCD的相關技術來執行不同的內容。本篇部落格會對使用到的每個技術點進行詳細的講解。在講解時,為了易於理解,我們還會給出原理圖,這些原理圖都是根據本篇部落格中的例項進行創作的,在其他地方可見不著。
上面每個按鈕都對應著一坨坨的程式碼,上面這個截圖算是我們本篇部落格的一個,下面我們將會對每坨程式碼進行詳細的介紹。通過這些介紹,你應該對GCD有了更全面而且更詳細的瞭解。建議參考著下方的介紹,然後自己動手去便實現程式碼,這樣效果是灰常的好的。本篇部落格中的所有程式碼都會在github上進行分享,本篇部落格的後方會給出github分享地址。其實本篇部落格可以作為你的GCD參考手冊,雖然本篇部落格沒有囊括所有GCD的東西,但是平時經常使用的部分還是有的。廢話少說,進入今天部落格的主題,接下來我們將一個Button接著一個Button的介紹。
一、常用GCD方法的封裝
為了便於例項的實現,我們首先對一些常用的GCD方法進行封裝和提取。該部分算是為下方的具體例項做準備的,本部分封裝了一些下面示例所公用的方法。接下來我們將逐步的對每個提取的函式進行介紹,為下方示例的實現做準備。在封裝方法之前,要說明一點的是在GCD中我們的任務是放在佇列中在不同的執行緒中執行的,要明白一點就是我們的任務是放在佇列中的Block中,然後Block再在相應的執行緒中完成我們的任務。
如下圖所示,在下方佇列中存入了三個Block,每個Block對應著一個任務,這些任務會根據佇列的特性已經執行方式被放到相應的執行緒中來執行。佇列可分為並行佇列(Concurrent Qeueu)和序列佇列(Serial Queue),佇列可以進行同步執行(Synchronize)以及非同步執行(Asynchronize), 稍後會進行詳細的分析與介紹。我們要知道佇列第GCD的基礎。
1.獲取當前執行緒與當前執行緒休眠
首先我們將獲取當前執行緒的方法進行封裝,因為有時候我們會經常檢視我們的任務是在那些執行緒中執行的。在此我們使用了NSThread的currentThread()方法來獲取當前執行緒。下方的getCurrentThread
上述程式碼段是獲取當前執行緒的方法,接著我們要實現一個讓當前執行緒休眠的方法。因為我們在示例時,常常會讓當前執行緒來休眠一段時間來模擬那些耗時的操作。下方程式碼段中的currentThreadSleep()函式就是我們提取的當前執行緒休眠的函式。該函式有一個NSTimeInterval型別的引數,該引數就是要休眠的時間。NSTimeInterval其實就是Double型別的別名,所以我們在呼叫currentThreadSleep()方法時需要傳入一個Double型別的休眠時間。當然你也可以呼叫sleep()方法來對當前執行緒進行休眠,但是需要注意的是sleep()的引數是UInt32位的整型。下方就是我們休眠當前執行緒的函式。
2.獲取主佇列與全域性佇列
下方封裝的getMainQueue()函式就是獲取主佇列的函式,因為有時候我們在其他執行緒中處理完耗時的任務(比如網路請求)後,需要在主佇列中對UI進行更新。因為我們知道在iOS中有個RunLoop的概念,在iOS系統中觸控事件、螢幕重新整理等都是在RunLoop中做的。因為本篇部落格的主題是GCD, 在此就對RunLoop做過多的贅述了,如果你對RunLoop不太瞭解,那麼你就先簡單將RunLoop理解成1/60執行一次的迴圈即可,當然真正的RunLoop要比一個單純的迴圈複雜的多,以後有機會的話在以RunLoop為主題更新一篇部落格吧。言歸正傳,下方就是獲取我們主佇列的方法,簡單一點的說因為我們要更新UI,所以要獲取主佇列。
接下來我們要封裝一個獲取全域性佇列(Global Queue)的函式,在封裝函式之前我們先來聊聊什麼是全域性佇列。全域性佇列是系統提供的一個佇列,該佇列拿過來就能用,按執行方式來說,全域性佇列應該稱得上是並行佇列,關於串並行佇列的具體概念下方會給出介紹。我們在獲取全域性佇列的時候要知道其佇列的優先順序,優先順序越高的佇列就越先執行,當然該處的優先順序不是絕對的。佇列真正的執行順序還需要根據CUP當前的狀態來定,大部分是按照你指定的佇列優先順序來執行的,不過也有例外。下方例項會給出詳細的介紹。下方就是我們獲取全域性佇列的函式,在獲取全域性佇列為為全域性佇列指定一個優先順序,預設為DISPATCH_QUEUE_PRIORITY_DEFAULT。
3.建立序列佇列與並行佇列
因為我們在實現例項時會建立一些並行佇列和序列佇列,所以我們要對並行佇列的創建於序列佇列的建立進行提取。GCD中是呼叫dispatch_queue_create()函式來建立我們想要的執行緒的。dispatch_queue_create()函式有兩個引數,第一個引數是佇列的標示,用來標示你建立的佇列物件,一般是域名的倒寫如“cn.zeluli”這種形式。第二個引數是所建立佇列的型別,DISPATCH_QUEUE_CONCURRENT就說明建立的是並行佇列,DISPATCH_QUEUE_SERIAL表明你建立的是序列佇列。至於兩者的區別,還是那句話,下方例項中會給出詳細的介紹。
二、同步執行與非同步執行
同步執行可分為序列佇列的同步執行和並行佇列的同步執行,而非同步執行又可分為序列佇列的非同步執行和並行佇列的非同步執行。也許聽起來有些拗口,不通過下方的圖解你會很好的理解這一概念。上一部分算是我們的準備工作,接下來才是我們真正的主題。在第一部分我們實現了獲取當前執行緒,對當前執行緒休眠,獲取主佇列和全域性佇列以及建立並行佇列和序列佇列。在本部分將要利用上述函式來進一步討論序列佇列與並行佇列的同步執行,以及序列佇列與並行佇列的非同步執行。並且會給出同步執行與非同步執行的區別。
在聊同步執行與非同步執行之前我們先聊聊序列佇列(Serial Queue)與並行佇列(Concurrent Queue)的區別。無論是Serial Queue還是Concurrent Queue,都是佇列,只要是佇列都遵循FIFO(First In First Out -- 先入先出)的規則,排隊嘛,當然是誰先來的誰先走了。不過在Serial Queue中要等到前面的任務出佇列並執行完後,下一個任務才能出佇列進行執行。而Concurrent Queue則不然,只要是佇列前面的任務出隊列了,並且還有有空餘執行緒,不管前面的任務是否執行完了,下一任務都可以進行出佇列。
關於序列佇列和並行佇列的問題,我們可以拿銀行辦業務排隊來類比一下。比如你現在在序列佇列中排的是1號視窗,你必須等前面一個人在1號視窗辦完業務你才可以去1號視窗中去辦你的業務,就算其他視窗空著你也不能去,因為你選擇的是序列佇列。但是如果你是在並行佇列中的話,只要你前面的人去視窗中辦業務了,此時你無需關係你前面的人的業務是否辦完了,只要有其他視窗空著你就可以去辦理你的業務。總結一下:序列佇列就是認準一個執行緒,一條道走到黑,比較專注;並行佇列就是能利用其他執行緒就利用,比較靈活,不鑽牛角尖。接下來我們要看一下兩個佇列的不同執行方法。
1.同步執行
首先我們先來介紹同步執行,關於同步執行的主要例項對應著“同步執行序列佇列”和“同步執行並行佇列”這兩個按鈕。Serial Queue可以同步執行,Concurrent Queue亦可以同步執行。我們先拋開佇列,看一下同步執行的程式碼如何。下方的函式就是對同步執行的任務進行封裝。同步執行就是使用dispatch_sync()方法進行執行。在下方函式中通過for-in迴圈以同步執行的方式往queue(佇列)中添加了3個Block執行塊。函式的引數是佇列型別(dispatch_queue_t),可以給該函式傳入序列佇列和並行佇列。
也就是說要同步執行序列佇列就給函式傳入序列佇列的物件,如果要同步執行並行佇列就傳入並行佇列物件。此時我們就用到了之前封裝的建立序列佇列和並行佇列的方法(參見第一部分)。下方程式碼段就是點選“同步執行序列佇列”和“同步執行並行佇列”這兩個按鈕所做的事情。點選“同步執行序列佇列”按鈕時就建立一個序列佇列的物件傳給上面同步執行的函式(performQueuesUseSynchronization()),點選“同步執行並行佇列”按鈕時就建立一個並行佇列的物件給上面的函式。
下方截圖是點選兩個按鈕所執行的結果。紅框中是同步執行序列佇列的結果,可以看出來是在當前執行緒(主執行緒)下按著FIFO的順序來執行的。而綠框中的是同步執行並行佇列的執行結果,從結果中部門不難看出,與紅框中的結果一致,也是在當前執行緒中按著FIFO的順序來執行的。
通過上面兩種不同佇列的同步執行方式我們給出了下面的分析圖。Serial Queue與Concurrent Queue中都有4個Block(編號為1--4),然後使用dispatch_sync()來同步執行。由上述示例我們可以得出,同步執行方式,也就是使用dispatch_sync()來執行佇列不會開闢新的執行緒,會在當前執行緒中執行任務。如果當前執行緒是主執行緒的話,那麼就會阻塞主執行緒,因為主執行緒被阻塞了,就會會造成UI卡死的現象。因為同步執行是在當前執行緒中來執行的任務,也就是說現在可以供佇列使用的執行緒只有一個,所以序列佇列與並行佇列使用同步執行的結果是一樣的,都必須等到上一個任務出佇列並執行完畢後才可以去執行下一個任務。我們可以使用同步執行的這個特點來為一些程式碼塊加同步鎖。下方就是上面程式碼以及執行結果的描述圖。
2、非同步執行
接下來我們看非同步執行,同樣非同步執行也分為序列佇列的非同步執行和並行佇列的非同步執行。在GCD中使用dispatch_async()函式來進行非同步執行,dispatch_async()函式的引數與dispatch_sync()函式的引數一致。只不過是dispatch_async()非同步執行不會在當前執行緒中執行,它會開闢新的執行緒,所以非同步執行不會阻塞當前執行緒。下方程式碼段就是我們封裝的非同步執行的函式,其中主要是對dispatch_async()函式的使用。下方為了讓佇列中的Block的三個輸出語句順序輸出,我們將其放在了一個同步佇列中來執行,從而這三個輸出語句可以順序執行。
(1)、序列佇列的非同步執行
有了上面的函式後,我們就可以給上面的函式傳入Serial Queue佇列的物件,從而觀察序列佇列非同步執行結果。對應這我們第一張截圖中的“非同步執行序列佇列”的按鈕,下方是點選該按鈕執行的方法。在該按鈕點選的方法中我們呼叫了performQueuesUseAsynchronization()方法,並且傳入了一個序列佇列。也就是序列佇列的非同步執行。
點選按鈕就會執行上述方法,下方是點選按鈕後,也就是“非同步執行序列佇列”時在控制檯中輸出的結果。從輸出結果中我們不難看出,非同步執行並沒有阻塞當前執行緒。使用dispatch_saync()開闢了新的執行緒(執行緒的number = 3)來執行Block中的內容。而Block內容外的東西依然在之前的執行緒(在該示例中是main_thread)中進行執行。從下方的結果中來分析,就是for迴圈執行完畢後主執行緒的任務就結束了,至於Block中的內容就交給新開闢的執行緒3來執行了。
根據上面的輸出結果,我們可以畫出下方非同步執行序列佇列的分析圖。線上程1中的一個序列佇列如果使用非同步執行的話,會開闢一個新的執行緒2來執行佇列中的Block任務。在新開闢的執行緒中依然是FIFO, 並且執行順序是等待上一個任務執行完畢後才開始執行下一個任務。如下所示。
(2)、並行佇列的非同步執行
接下來來討論一下並行佇列的非同步執行方式。其實並行佇列與非同步執行方式相結合才能大大的提供效率,因為使用非同步執行並行佇列時會開闢多個執行緒來同時執行並行佇列中的任務。比如現在開闢了10個執行緒,那麼非同步佇列會根據FIFO的順序出來10個任務,這10個任務會進入到不同的執行緒中來執行,至於每個任務執行完的先後順序由每個任務的複雜度而定。非同步佇列的特點是隻要有可用的執行緒,任務就會出佇列進行執行,而不關心之前出佇列的任務(Block)是否執行完畢。下方的方法就是點選“非同步執行並行佇列”按鈕所呼叫的方法。該方法會呼叫performQueuesUseAsynchronization()函式,並傳入一個並行佇列的物件。
點選按鈕就會執行上述方法,並行佇列就會非同步執行。下方結果就是並行佇列非同步執行後輸出的結果,解析來讓我們來分析一下輸出結果。下方第一個紅框中是並行佇列中任務的順序,由前到後為0、1、2,緊接著是每個任務執行後所輸出的結果。從任務執行完列印結果我們可以看出,執行完成的順序是2、1、0,每個任務都會在一個新的執行緒中來執行的。如果你在點選一下按鈕,執行完成的順序有可能是2、0、1等其他的順序,所以並行佇列非同步執行中每個任務結束時間有主要由任務本身的複雜度而定的。
根據上面的執行結果,我們畫出了下方的解說圖。當並行佇列非同步執行時會開闢多個新的執行緒來執行佇列中的任務,佇列中的任務出佇列的順序仍然是FIFO,只不過是不需要等到前面的任務執行完而已,只要是有空餘執行緒可以使用就可以按FIFO的順序出佇列進行執行。
三、延遲執行
在GCD中我們使用dispatch_after()函式來延遲執行佇列中的任務, dispatch_after()是非同步執行佇列中的任務的,也就是說使用dispatch_after()來執行佇列中的任務不會阻塞當前任務。等到延遲時間到了以後就會開闢一個新的執行緒然後執行佇列中的任務。要注意一點是延遲時間到了後再開闢新的執行緒然後立即執行佇列中的任務。下方是dispatch_after()函式的使用方式。
在下方程式碼中使用了兩種方式來建立延遲時間,一個是使用dispatch_time()來建立延遲時間,另一個是使用dispatch_walltime()來建立時間。前者是取的是當前裝置的時間,後者去的是掛鐘的時間,也就是絕對時間,如果裝置休眠了那麼前者也就休眠了,而後者是是根據掛鐘時間不會有當前裝置的狀態而左右的。下面在建立dispatch_time_t物件的時候,有一個引數是NSEC_PER_SEC,從命名只能怪我們就可以知道NSEC_PER_SEC表示什麼意思,就是每秒包含多少納秒。你可以將該值進行列印,發現NSEC_PER_SEC = 1_000_000_000。也就是一秒等於10億納秒。如果下方的time不乘於NSEC_PER_SEC那麼就代表1納秒的時間,也就是說此處的時間是按納秒(nanosecond)來計算的。下方就是延遲執行的的程式碼,因為改程式碼輸出結果比較簡單,在此就不做過多的贅述了。需要注意的是延遲執行會在新開闢的佇列中進行執行,不會阻塞新的執行緒。
四、佇列的優先順序
佇列也是有優先順序的,但其優先順序不是絕對的大部分情況因為XUN核心用於GCD不是實時性的,優先順序只是大致的來判斷佇列的執行優先順序。佇列分為四個優先順序,由高到底分別是High > Default > Low > Background。上面在獲取全域性佇列時我們可以為獲取的佇列指定優先順序,並且可以使用dispatch_set_target_queue()函式將一個佇列的優先順序賦值給另一個佇列。下方我們先給全域性佇列指定優先順序,然後在將其賦值給其他佇列。
1.為全域性佇列指定優先順序
本部分對應著“設定全域性佇列的優先順序”這個button,點選該button就會獲取4個不同優先順序的全域性佇列,然後非同步進行全域性佇列的執行,最後觀察執行的結果。下方就是點選該按鈕所要執行的函式。我先獲取了四種不同優先順序的全域性佇列,然後進行非同步執行,並列印執行結果。
上述程式碼的執行結果如下,雖然在上述程式碼中優先順序高的程式碼放在了最後來進行非同步執行,可是卻先被列印了。列印的順序是Hight->Default->Low->Background,這個列印順序就是其執行順序,從列印順序中我們不難看出優先順序高的先被執行。當然這不是絕對的。
2. 為自建立的佇列指定優先順序
在GCD中你可以使用dispatch_set_target_queue()函式為你自己建立的佇列指定優先順序,這個過程還需藉助我們的全域性佇列。下方的程式碼段中我們先建立了一個序列佇列,然後通過dispatch_set_target_queue()函式將全域性佇列中的高優先順序賦值給我們剛建立的這個序列佇列,如下所示。
五、任務組dispatch_group
GCD的任務組在開發中是經常被使用到,當你一組任務結束後再執行一些操作時,使用任務組在合適不過了。dispatch_group的職責就是當佇列中的所有任務都執行完畢後在去做一些操作,也就是說在任務組中執行的佇列,當佇列中的所有任務都執行完畢後就會發出一個通知來告訴使用者任務組中所執行的佇列中的任務執行完畢了。關於將佇列放到任務組中執行有兩種方式,一種是使用dispatch_group_async()函式,將佇列與任務組進行關聯並自動執行佇列中的任務。另一種方式是手動的將佇列與組進行關聯然後使用非同步將佇列進行執行,也就是dispatch_group_enter()與dispatch_group_leave()方法的使用。下方就給出詳細的介紹。
1.佇列與組自動關聯並執行
首先我們來介紹dispatch_group_async()函式的使用方式,該函式會將佇列與相應的任務組進行關聯,並且自動執行。當與任務組關聯的佇列中的任務都執行完畢後,會通過dispatch_group_notify()函式發出通知告訴使用者任務組中的所有任務都執行完畢了。使用通知的方式是不會阻塞當前執行緒的,如果你使用dispatch_group_wait()函式,那麼就會阻塞當前執行緒,直到任務組中的所有任務都執行完畢。
下方封裝的函式就是使用dispatch_group_async()函式將佇列與任務組進行關聯並執行。首先我們建立了一個concurrentQueue並行佇列,然後又建立了一個型別為dispatch_group_t的任務組group。使用dispatch_group_async()函式將兩者進行關聯並執行。使用dispatch_group_notify()函式進行監聽group中佇列的執行結果,如果執行完畢後,我們就在主執行緒中對結果進行處理。dispatch_group_notify()函式有兩個引數一個是傳送通知的group,另一個是處理返回結果的佇列。
呼叫上述函式的輸出結果如下。從輸出結果中我們不難看出,佇列中任務的執行以及通知結果的處理都是非同步執行的,不會阻塞當前的執行緒。在任務組中所有任務都處理完畢後,就會在主執行緒中執行dispatch_group_notify()中的閉包塊。
2. 手動關聯佇列與任務組
接下來我們將手動的管理任務組與佇列中的關係,也就是不使用dispatch_group_async()函式。我們使用dispatch_group_enter()與dispatch_group_leave()函式將佇列中的每次任務加入到到任務組中。首先我們使用dispatch_group_enter()函式進入到任務組中,然後非同步執行佇列中的任務,最後使用dispatch_group_leave()函式離開任務組即可。下面的函式中我們使用了dispatch_group_wait()函式,該函式的職責就是阻塞當前執行緒,來等待任務組中的任務執行完畢。該函式的第一個引數是所要等待的group,第二個引數是等待超時時間,此處我們設定的是DISPATCH_TIME_FOREVER,就說明等待任務組的執行永不超時,直到任務組中所有任務執行完畢。
下方是上述函式執行後的輸出結果,dispatch_group_wait()函式下方的print()函式在所有任務執行完畢之前是不會被呼叫的,因為dispatch_group_wait()會將當前執行緒進行阻塞。當然雖然是手動的將佇列與任務組進行關聯的,display_group_notify()函式還是好用的。執行結果如下所示。
六、訊號量(semaphore)同步鎖
有時候多個執行緒對一個數據進行操作的時候,為了資料的一致性,我們只允許一次只有一個執行緒來操作這個資料。為了保證一次只有一個執行緒來修改我們的資源資料,我們就得用到訊號量同步鎖了。也就是說一個存放資源房間的門後邊又把鎖,當有一個執行緒進到這個房間後就將這把鎖鎖上。當這個執行緒修改完該資源後,就將鎖給開啟,鎖開啟後其他的執行緒就可以持有資源了。如果你上了鎖不打卡,而其他執行緒等待使用該資源時,就會產生死鎖。所以當你不使用的時候,就不要持有資源呢。
上述這個過程,在GCD中我們可以使用訊號量機制來完成。在GCD中有一個叫dispatch_semaphore_t的東西,這個就是我們的訊號量。我們可以對訊號量進行操作,如果訊號量為0那麼就是上鎖的狀態,其他執行緒想使用資源就得等待了。如果訊號量不為零,那麼就是開鎖狀態,開鎖狀態下資源就可以訪問。下方程式碼就是訊號量的具體使用程式碼。
下方第一個紅框中就是通過dispatch_semaphore_create()來建立訊號量,該函式需要一個引數,該引數所指定的就是訊號量的值,我們為訊號指定的值為1。第二個紅框中是“上鎖的過程”,通過dispatch_semaphore_wait()函式對訊號量操作,該函式中的第一個引數是所操作的訊號量,第二個引數是等待時間。dispatch_semaphore_wait()函式是對訊號量減一,訊號量為零的話就對當前執行緒所操作的資源加鎖。其他執行緒等待當前執行緒操作資源的時間為DISPATCH_TIME_FOREVER,也就是說其他執行緒要一直等下去,等待當前執行緒操作資源完畢。噹噹前執行緒對資源操作完畢後呼叫dispatch_semaphore_signal()將訊號量加1,將資源進行解鎖,以便於其他等待的執行緒進行資源的訪問。當解鎖後,其他執行緒等待的時間結束,就可以進行資源的訪問了。
七、佇列的迴圈、掛起、恢復
在本篇部落格的第七部分,我們要聊一下佇列的迴圈執行以及佇列的掛起與恢復。該部分比較簡單,但是也是比較常用的。在重複執行佇列中的任務時,我們通常使用dispatch_apply()函式,該函式迴圈執行佇列中的任務,但是dispatch_apply()函式本身會阻塞當前執行緒。如果你使用dispatch_apply()函式來執行並行佇列,雖然會開啟多個執行緒來迴圈執行並行佇列中的任務,但是仍然會阻塞當前執行緒。如果你使用dispatch_apply()函式來執行序列佇列的話,那麼就不會開闢新的執行緒,當然就會將當前執行緒進行阻塞。說到佇列的掛起與恢復你可以使用dispatch_suspend()來掛起佇列,使用dispatch_resum()來恢復佇列。請看下方例項。
1、dispatch_apply()函式
dispatch_apply()函式是用來迴圈來執行佇列中的任務的,使用方式為:dispatch_apply(迴圈次數, 任務所在的佇列) { 要迴圈執行的任務 }。使用該函式迴圈執行並行佇列中的任務時,會開闢新的執行緒,不過有可能會在當前執行緒中執行一些任務。而使用dispatch_apply()執行序列佇列中的任務時,會在當前執行緒中執行。無論是使用並行佇列還是序列佇列,dispatch_apply()都會阻塞當前執行緒。下方程式碼段就是dispatch_apply()的使用示例:
下方則是上述函式的執行結果。在結果中我們將每次執行任務所使用的執行緒進行了列印。
2. 佇列的掛起與喚醒
佇列的掛起與喚醒相對較為簡單,如果你想對一個佇列中的任務的執行進行掛起,那麼你就使用dispatch_suspend()函式即可。如果你要喚醒某個掛起的佇列,那麼你就可以使用dispatch_resum()函式。這兩個函式所需的引數都是你要掛起或者喚醒的佇列,鑑於知識點的簡單性就不做過多的贅述了。下方是對非同步執行的並行佇列進行掛起,在當前執行緒休眠2秒後喚醒被掛起的執行緒。具體程式碼如下:
八、任務柵欄dispatch_barrier_async()
顧名思義,任務柵欄就是將佇列中的任務進行隔離的,是任務能分撥的進行非同步執行。我想用下方的圖來介紹一下barrier的作用。我們假設下方是並行佇列,然後並行佇列中有1.1、1.2、2.1、2.2四個任務,前兩個任務與後兩個任務本中間的柵欄給隔開了。如果沒有中間的柵欄的話,四個任務會在非同步的情況下同時執行。但是有柵欄的攔著的話,會先執行柵欄前面的任務。等前面的任務都執行完畢了,會執行柵欄自帶的Block ,最後非同步執行柵欄後方的任務。這麼一說有點與前面的dispatch_group類似,當執行完一些列的任務後,我們想做一些事情的話,我們也可通過dispatch_barrier_async()來實現。
下方程式碼段就是我們dispatch_barrier_async(), 具體的使用方式。上面的紅色框中的程式碼是非同步執行的第一批任務,中間是我們給任務佇列新增的任務柵欄,dispatch_barrier_asyn()的一個引數就是柵欄所在的佇列,而後邊的尾隨閉包就是在柵欄前面的所有任務都執行完畢後就會執行該尾隨閉包中的內容。而最下方黃色框中的部分就是第二批次執行的任務,該批任務會在dispatch_barrier_asyn()柵欄的尾隨閉包執行後會繼續執行。
接下來我們來看一下上述程式碼的執行結果,點選我們第一部分截圖的“使用任務隔離柵欄”按鈕就會執行上述方法。下方就是上述程式碼片段的執行結果。從下面的輸出結果中不難看出,dispatch_barrier_asyn之前的任務會先非同步執行,也就是下方的第一批任務。第一批任務完成後,會在第一批任務中的最後完成任務的執行緒中來執行柵欄中的任務塊。當柵欄中的任務執行完畢後,佇列中的第二批任務中的第一個會進入執行柵欄任務的執行緒中來執行,其他的會開闢新的執行緒。如下所示。
我們可以用一個圖來結合上述示例來解釋柵欄的工作方式。下圖畫的就是柵欄工作的方式,需要注意的是佇列中的第一批任務中的最後一個任務與柵欄中的任務已經第二批第一個任務是用一個執行緒來執行的。這就是為什麼柵欄能進行任務隔離的根本了。從下方的圖中我們不難發現,任務1.3、柵欄任務、任務2.1線上程5中是同步執行的。具體請看下圖。
九、dispatch_source
dispatch_source在GCD中是一個比較靈活的東西,功能也是非常強大的。簡單的說,dispatch_source的主要功能就是對某些型別事件的物件進行監聽,當事件發生時將要處理的事件放到關聯的佇列中進行執行。dispatch源支援事件的取消,我們也可以對取消事件的進行處理。下方是dispatch源的不同型別,因為篇幅有限在此就不做過多的贅述了,關於這些型別的資料網上一抓一大把。今天就以DATA_ADD, DATA_OR, TIMER為例,看一下source的使用方式。
1. DATA_ADD 與DATA_OR
DISPATCH_SOURCE_TYPE_DATA_ADD和DISPATCH_SOURCE_TYPE_DATA_OR用法差不多一個是將資料來源進行相加,一個是進行或操作。我們就以相加的為例,或操作的程式碼在部落格中就不給出了,不過我們github上分享的程式碼會有完整的示例。下方函式是DISPATCH_SOURCE_TYPE_DATA_ADD型別的dispatch源的使用。
首先我們獲取了一個全域性佇列queue,然後建立了一個dispatch源,命名為dispatchSource。在建立dispatch源時,我們為dispatch源指定了型別,並且為其關聯的一個queue佇列。關聯這個佇列的作用是用來處理dispatch源中的事件的。然後我們使用dispatch_source_set_event_handler()為我們的source指定處理事件。該事件會在一定的條件下回觸發,至於觸發的條件有dispatch源的型別鎖定。因為此處我們dispatch源的型別是DISPATCH_SOURCE_TYPE_DATA_ADD,所以使用dispatch_source_merge_data()就可以觸發上面我們指定的事件。因為dispatch源建立後是處於掛起的狀態,所以我們需要使用dispatch_resume()對我們建立的事件源進行恢復。恢復後的dispatch源才可以監聽條件從而觸發事件。
下方程式碼段在for迴圈中呼叫dispatch_source_merge_data()方法。在執行過程中我們還可以呼叫dispatch_source_cancel()對dispatch源進行取消。當dispatch source被取消後,就會執行我們所設定取消dispatch_source要處理的事件。我們通過dispatch_source_set_candel_handel()來指定取消dispatch source要執行的事件。關於dispatch_source的取消,我們會在下面倒計時的時候給出。
我們此處建立的dispatch_source的型別是Data Add型別,也就是說當我們指定的源事件未處理完時,那麼下一個Data就要進行等待。而等待的資料會通過dispatch_source_merge_data()方法進行合併。如果你建立的是DISPATCH_SOURCE_TYPE_DATA_ADD型別的dispatch_source,那麼就會按照加法進行合併。如果你是建立的DISPATCH_SOURCE_TYPE_DATA_OR型別的dispatch_source, 那麼就會通過或運算進行合併。合併在一起的資料會一同觸發我們設定的事件。
上述程式碼段就是對DATA_ADD類的的dispatch源進行的測試。我們定義了一個變數sum來模擬資料的合併,然後觀察每次合併的資料與我們自定的sum中計算的資料是否相同。合併後每次執行一次事件我們都將sum進行歸零,然後進行下一輪的合併。下方就是上述程式碼輸出的結果。從下方的結果中我們可以看出,在上述的10次迴圈中執行了四次我們指定的source事件,而且每次執行事件所merge的Data與我們手動記錄的sum一致。這就是DATA_ADD的工作方式,執行效果如下所示。關於Data_Or的執行方式在此就不做過多的贅述了。
2.定時器
在GCD的dispatch源中還有定時器型別,我們可以建立定時器型別的dispatch源,然後通過dispatch_source_set_event_handler()來設定源事件。然後通過dispatch_source_set_timer()函式來為定時器型別的dispatch_source指定時間間隔,該函式第一個引數就是dispatch source,第二個引數就是觸發事件的時間間隔,第三個引數就是允許誤差的時間。當我們設定的倒計時的次數到是,我們就呼叫dispatch_source_cancel()來進行dispatch_source的取消,當取消後就會執行dispatch_source_set_cancel_handel()方法中的尾隨閉包。
下方示例是使用DISPATCH_SOURCE_TYPE_TIMER型別的dispatch source進行的10秒到計時,等我們設定的事件執行10次後我們就取消dispatch_source。對於下方的示例來說,當dispatch source通過dispatch_resume()函式進行喚醒後,會開始倒計時。會在倒計時10秒後結束計時。
下方就是上述倒計時程式碼所執行後的結果。從執行結果中我們不難看出,當倒計時開始時,會新開闢一些新的執行緒來順序執行倒計時任務。儘管你使用的是並行佇列,雖然每次開闢的執行緒可能會不同,但是都會順序的執行倒計時任務,
今天部落格的內容也夠多的了,應該說還算是幹活滿滿,上述所有程式碼將會在github上進行分享,下方是分享地址。有什麼問題,或者需要補充的加QQ群吧,之前的群人已經滿了,加不進去了,就建立了一個新的。