對於iOS多線程開發,我們時刻處於學習之中,在看書中,看文檔中,項目開發中,都可以去提高自己。最近剛看完了《Objective-C高級編程 iOS與OS X多線程和內存管理》這本書後,對多線程有了更為深入的理解,故在此做一個總結與記錄。這本書是iOS開發者必讀的書之一,寫得很不錯。書的封面如下,故也稱獅子書:
。
(1)多線程會遇到的問題
。
多線程會出現什麼問題呢?當多個線程對同一個數據進行修改的時候,會造成數據不一致的情況;當多個線程互相等待會造成死鎖;過多的線程並發會大量消耗內存。所以多線程出現bug會嚴重影響App的功能和性能。
(2)多線程的作用
我們為什麼一定要用多線程呢?只要有一個主線程不就可以了麼。
。
從圖中可以看到,如果我們有一個很耗時的操作放到主線程中執行,那麼會嚴重阻塞主線程的執行,導致主界面不能及時響應用戶的操作,出現卡死狀態。當創建多線程之後,我們可以把耗時操作放到其他線程中去執行,比如網絡操作,圖片的上傳下載等,當執行成功後再回到主線程更新界面即可。所以,使用多線程是必要的。
(3)GCD中的隊列——Dispatch Queue
。
Dispatch Queue就是GCD中的調度隊列,我們只要把任務添加到隊列中去,線程就會按照順序取出然後去執行,按照先進先出FIFO的原則。
(4)Dispatch Queue的種類
GCD中存在兩種Dispatch Queue,一種是等待現在執行中處理的Serial Dispatch Queue(串行隊列);另一種是不等待現在執行中處理的Concurrent Dispatch Queue(並發隊列).
Serial Dispatch Queue:等待現在執行中處理結束;
Concurrent Dispatch Queue:不等待現在執行中處理結束,使用多個線程同時執行多個處理。
。
Serial Dispatch Queue為什麼一定要等待處理結束才去執行下一個處理呢?因為Serial Dispatch Queue只有一個線程,一個線程同一時間當然只能執行一個任務,所以後續任務必須要進行等待。
Concurrent Dispatch Queue由於可以創建多個線程,所以只要按順序取出任務即可,把取出的任務放到不同的線程去執行,任務之間是否執行結束沒有必然關系,所以不需要等待前一個處理結束,看起來就像是多個任務同時在執行一樣,其實也的確是在同時執行。當然這個並發數量系統會有限制,我們代碼也可以設置最大並發數。
看了以上的解釋,就知道Serial Dispatch Queue、Concurrent Dispatch Queue和線程的關系了,關系如下:
。
(5)多個Serial Dispatch Queue實現並發,以及遇到的問題
當生成多個Serial Dispatch Queue時,各個Serial Dispatch Queue將並行執行。雖然在一個Serial Dispatch Queue中同時只能執行一個追加處理,但是如果將處理分別追加到4個Serial Dispatch Queue中,各個Serial Dispatch Queue執行一個,即可以同時執行四個處理。
。
雖然這種笨辦法也可以實現並發,但是也會遇到大問題,那就是消耗大量內存:
。
(6)資源競爭問題的解決
當多個線程對同一數據進行操作時可造成競爭或者數據不一致。最簡單的解決辦法就是使用Serial Dispatch Queue。Serial Dispatch Queue只創建一個線程,而一次只能執行一個任務,只有當該任務執行結束才能去執行下一個,所以同一時間對某個競爭資源的訪問是唯一的。示意圖如下:
。
(7)生成的Dispatch Queue必須由程序員釋放。這是因為Dispatch Queue並沒有像Block那樣具有作為OC對象來處理的技術。通過dispatch_queue_create函數生成的Dispatch Queue在使用結束後通過dispatch_release釋放。看個下面的例子:
。
這樣立即釋放queue是否有問題呢?
在dispatch_async函數中追加Block到Dispatch Queue後,即使立即釋放Dispatch Queue,該Dispatch Queue由於被Block持有也不會被廢棄,因而Block能夠執行。Block執行結束後會釋放Dispatch Queue,這時誰都不持有Dispatch Queue,因此它會被廢棄。
(8)系統標准提供的Dispatch Queue
-- Main Dispatch Queue:在主線程中執行的queue,因為主線程只有一個,所以Main Dispatch Queue自然就是Serial Dispatch Queue。追加到Main Dispatch Queue的處理在主線程的RunLoop中執行。
。
-- Global Dispatch Queue:是所有應用程序都能夠使用的Concurrent Dispatch Queue,沒有必要通過dispatch_queue_create函數逐個生成Concurrent Dispatch Queue,只要獲取Global Dispatch Queue即可。其中有四個優先級,但是用於Global Dispatch Queue的線程並不能保證實時性,因此執行優先級只是大致的判斷。
對於Main Dispatch Queue和Global Dispatch Queue執行dispatch_retain函數和dispatch_release函數不會引起任何變化,也不會有任何問題。這也是獲取並使用Global Dispatch Queue比生成、使用、釋放Concurrent Dispatch Queue更輕松的原因。
(9)dispatch_set_target_queue:改變生成的Dispatch Queue的執行優先級
指定要變更執行優先級的Dispatch Queue為dispatch_set_target_queue函數的第一個參數,指定與要使用的執行優先級相同的Global Dispatch Queue為第二個參數(目標),第一個參數如果指定系統提供的Main Dispatch Queue和Global Dispatch Queue,則不會知道出現什麼狀態,因此這些均不可指定。
(10)dispatch_after:延遲執行
NSEC_PER_SEC:秒
NSEC_PER_MSEC:毫秒
(11)dispatch_barrier_async
會等待追加到Concurrent Dispatch Queue上的並行執行的處理全部結束之後,再將指定的處理追加到該Concurrent Dispatch Queue中。然後在由dispatch_barrier_async函數追加的處理執行完畢後,Concurrent Dispatch Queue才恢復為一般的動作,追加到該Concurrent Dispatch Queue的處理又開始並行執行。示意圖如下:
..
(12)dispatch_async
將指定的block“非同步”的追加到指定的Dispatch Queue中,dispatch_async函數不做任何等待。
。
(13)dispatch_sync造成的問題
一旦調用dispatch_sync函數,那麼在指定的處理執行結束之前,該函數不會返回。但是dispatch_sync容易造成死鎖。
。
該源代碼在Main Dispatch Queue即主線程中執行指定的Block,並等待其執行結束。而其實在主線程中正在執行這些源代碼,所以無法執行追加到Main Dispatch Queue的Block。下面的例子也一樣:
。
Main Dispatch Queue中執行的Block等待Main Dispatch Queue中要執行的Block執行結束。當然Serial Dispatch Queue也會引起相同的問題。
。
(14)dispatch_apply
dispatch_apply函數是dispatch_sync函數和Dispatch Group的關聯API。該函數按指定的次數將指定的Block追加到指定的Dispatch Queue中,並等到全部處理結束。
因為在Global Dispatch Queue中執行處理,所以各個處理的執行時間不定,是不進行等待的追加任務。但是輸出結果中最後的done必定在最後的位置上。這是因為dispatch_apply函數會等待全部處理執行結束。
。
由於dispatch_apply函數也與dispatch_sync函數相同,會等待處理執行結束,因此推薦在dispatch_async函數中非同步的執行dispatch_apply函數。
。
(15)dispatch_suspend/dispatch_resume
當追加大量處理到Dispatch Queue時,在追加處理的過程中,有時希望不執行已追加的處理。在這種情況下,只要掛起Dispatch Queue即可。當可以執行時再恢復。
-- dispatch_suspend函數掛起指定的Dispatch Queue:
dispatch_suspend(queue);
dispatch_resume函數恢復指定的Dispatch Queue:
dispatch_resume(queue);
這些函數對已經執行的處理沒有影響。掛起後,追加到Dispatch Queue中但尚未執行的處理在此之後停止執行。而恢復則使得這些處理能夠繼續執行。
(16)Dispatch Semaphore
當不使用信號量的時候出現的bug.
.
這裡使用Global Dispatch Queue更新NSMutableArray類對象,所以執行後由內存錯誤導致應用程序異常結束的概率很高。
Dispatch Semaphore是持有計數信號,該計數是多線程編程中的計數類型信號。計數為0時等待,計數為1或者大於1時,減去1而不等待。
創建semaphore:
參數表示計數的初始值。
。
dispatch_semaphore_wait函數等待Dispatch Semaphore的計數值達到或者等於1.當計數值大於等於1,或者在等待中計數值大於等於1時,對該計數進行減法並從dispatch_semaphore_wait函數返回。
semaphore可以進行以下分支處理:
dispatch_semaphore_wait函數返回0時,可安全的執行需要進行排他控制的處理。該處理結束時通過dispatch_semaphore_signal函數將Dispatch Semaphore的計數數值加1.
案例:
。
(17)dispatch_once
dispatch_once函數是保證在應用程序執行中只執行一次指定處理的API。下面這種經常出現的用來初始化的源代碼可通過dispatch_once函數簡化:
。
在多核CPU中,在正在更新表示是否初始化的標志變量時讀取,就有可能多次執行初始化處理。而用dispatch_once函數初始化就不用擔心了。這就是單例模式,在生成單例對象時使用。
(18)GCD的基本實現與描述
蘋果官方說明:通常,應用程序中編寫的線程管理用的代碼要在系統級實現。
什麼是系統級實現?就是在iOS和macOS的核心XNU內核級上實現。因此,無論程序員如何努力編寫管理線程的代碼,在性能方面也不可能勝過XNU內核級所實現的GCD。
用於實現Dispatch Queue而使用的軟件組件:
。
Dispatch Queue沒有“取消”這一概念。一旦將處理追加到Dispatch Queue中,就沒有辦法可以將該處理刪除,也沒有辦法就執行中取消該處理。