很長時間以來,我我的(可能還有不少同窗),對多線程編程都存在一些誤解。一個很明顯的表現是,不少人有這樣的見解:編程
新開一個線程,能提升速度,避免阻塞主線程swift
畢竟多線程嘛,幾個線程一塊兒跑任務,速度快,還不阻塞主線程,簡直完美。安全
在某些場合,咱們還見過另外一個「高深」的名詞——「異步」。這東西好像和多線程挺相似,通過一番百度(閱讀了不少質量層次不齊的文章)以後,不少人也沒能真正搞懂何爲「異步」。網絡
因而,帶着對「多線程」和「異步」的懵懂,不少人又開開心心踏上了多線程編程之旅,好比文章待會兒會提到的GCD。多線程
其實,若是不考慮其餘任何因素和技術,多線程有百害而無一利,只能浪費時間,下降程序效率。併發
是的,我很清醒的寫下這句話。app
試想一下,一個任務由十個子任務組成。如今有兩種方式完成這個任務: 異步
1. 建十個線程,把每一個子任務放在對應的線程中執行。執行完一個線程中的任務就切換到另外一個線程。
2. 把十個任務放在一個線程裏,按順序執行。async
操做系統的基礎知識告訴咱們,線程,是執行程序最基本的單元,它有本身棧和寄存器。說得再具體一些,線程就是「一個CPU執行的一條無分叉的命令列」。ide
對於第一種方法,在十個線程之間來回切換,就意味着有十組棧和寄存器中的值須要不斷地被備份、替換。 而對於對於第二種方法,只有一組寄存器和棧存在,顯然效率完勝前者。
經過剛剛的分析咱們看到,多線程自己會帶來效率上的損失。準確來講,在處理併發任務時,多線程不只不能提升效率,反而還會下降程序效率。
所謂的「併發」,英文翻譯是concurrent。要注意和「並行(parallelism)」的區別。
併發指的是一種現象,一種常常出現,無可避免的現象。它描述的是「多個任務同時發生,須要被處理」這一現象。它的側重點在於「發生」。
好比有不少人排隊等待檢票,這一現象就能夠理解爲併發。
並行指的是一種技術,一個同時處理多個任務的技術。它描述了一種可以同時處理多個任務的能力,側重點在於「運行」。
好比景點開放了多個檢票窗口,同一時間內能服務多個遊客。這種狀況能夠理解爲並行。
並行的反義詞就是串行,表示任務必須按順序來,一個一個執行,前一個執行完了才能執行後一個。
咱們常常掛在嘴邊的「多線程」,正是採用了並行技術,從而提升了執行效率。由於有多個線程,因此計算機的多個CPU能夠同時工做,同時處理不一樣線程內的指令。
併發是一種現象,面對這一現象,咱們首先建立多個線程,真正加快程序運行速度的,是並行技術。也就是讓多個CPU同時工做。而多線程,是爲了讓多個CPU同時工做成爲可能。
同步方法就是咱們平時調用的哪些方法。由於任何有編程經驗的人都知道,好比在第一行調用foo()
方法,那麼程序運行到第二行的時候,foo方法確定是執行完了。
所謂的異步,就是容許在執行某一個任務時,函數馬上返回,可是真正要執行的任務稍後完成。
好比咱們在點擊保存按鈕以後,要先把數據寫到磁盤,而後更新UI。同步方法就是等到數據保存完再更新UI,而異步則是馬上從保存數據的方法返回並向後執行代碼,同時真正用來保存數據的指令將在稍後執行。
假設如今有三個任務須要處理。假設單個CPU處理它們分別須要三、一、1秒。
並行與串行,其實討論的是處理這三個任務的速度問題。若是三個CPU並行處理,那麼一共只須要3秒。相比於串行處理,節約了兩秒。
而同步/異步,其實描述的是任務之間前後順序問題。假設須要三秒的那個是保存數據的任務,而另外兩個是UI相關的任務。那麼經過異步執行第一個任務,咱們省去了三秒鐘的卡頓時間。
對於同步執行的三個任務來講,系統傾向於在同一個線程裏執行它們。由於即便開了三個線程,也得等他們分別在各自的線程中完成。並不能減小總的處理時間,反而徒增了線程切換(這就是文章開頭舉的例子)
對於異步執行的三個任務來講,系統傾向於在三個新的線程裏執行他們。由於這樣能夠最大程度的利用CPU性能,提高程序運行效率。
因而咱們能夠得出結論,在須要同時處理IO和UI的狀況下,真正起做用的是異步,而不是多線程。能夠不用多線程(由於處理UI很是快),但不能不用異步(不然的話至少要等IO結束)。
注意到我把「傾向於」這三個加粗了,也就是說異步方法並不必定永遠在新線程裏面執行,反之亦然。在接下來關於GCD的部分會對此作出解釋。
GCD以block爲基本單位,一個block中的代碼能夠爲一個任務。下文中提到任務,能夠理解爲執行某個block
同時,GCD中有兩大最重要的概念,分別是「隊列」和「執行方式」。
使用block的過程,歸納來講就是把block放進合適的隊列,並選擇合適的執行方式去執行block的過程。
關於同步異步、串行並行和線程的關係,下面經過一個表格來總結
能夠看到,同步方法不必定在本線程,異步方法方法也不必定新開線程(考慮主隊列)。
然而事實上,在本文一開始就揭開了「多線程」的神祕面紗,因此咱們在編程時,更應該考慮的是:
同步 Or 異步以及串行 Or 並行,而非僅僅考慮是否新開線程。
固然,瞭解任務運行在那個線程中也是爲了更加深刻的理解整個程序的運行狀況,尤爲是接下來要討論的死鎖問題。
在使用GCD的過程當中,若是向當前串行隊列中同步派發一個任務,就會致使死鎖。
這句話有點繞,咱們首先舉個例子看看:
override func viewDidLoad() { super.viewDidLoad() let mainQueue = dispatch_get_main_queue() let block = { () in print(NSThread.currentThread()) } dispatch_sync(mainQueue, block) }
這段代碼就會致使死鎖,由於咱們目前在主隊列中,又將要同步地添加一個block
到主隊列(串行)中。
咱們知道dispatch_sync
表示同步的執行任務,也就是說執行dispatch_sync
後,當前隊列會阻塞。而dispatch_sync
中的block若是要在當前隊列中執行,就得等待當前隊列程執行完成。
在上面這個例子中,主隊列在執行dispatch_sync
,隨後隊列中新增一個任務block
。由於主隊列是同步隊列,因此block
要等dispatch_sync
執行完才能執行,可是dispatch_sync
是同步派發,要等block
執行完纔算是結束。在主隊列中的兩個任務互相等待,致使了死鎖。
其實在一般狀況下咱們沒必要要用dispatch_sync
,由於dispatch_async
可以更好的利用CPU,提高程序運行速度。
只有當咱們須要保證隊列中的任務必須順序執行時,才考慮使用dispatch_sync
。在使用dispatch_sync
的時候應該分析當前處於哪一個隊列,以及任務會提交到哪一個隊列。
瞭解完隊列以後,很天然的會有一個想法:咱們怎麼知道全部任務都已經執行完了呢?
在單個串行隊列中,這個不是問題,由於只要把回調block添加到隊列末尾便可。
可是對於並行隊列,以及多個串行、並行隊列混合的狀況,就須要使用 dispatch_group
了。
let group = dispatch_group_create() dispatch_group_async(group, serialQueue, { () -> Void in for _ in 0..<2 { print("group-serial \(NSThread.currentThread())") } }) dispatch_group_async(group, serialQueue, { () -> Void in for _ in 0..<3 { NSLog("group-02 - %@", NSThread.currentThread()) } }) dispatch_group_notify(group, serialQueue, { () -> Void in print("完成 - \(NSThread.currentThread())") })
首先咱們要經過 dispatch_group_create()
方法生成一個組。
接下來,咱們把 dispatch_async
方法換成 dispatch_group_async
。這個方法多了一個參數,第一個參數填剛剛建立的分組。
想問 dispatch_sync
對應的分組方法是什麼的童鞋面壁思過三秒鐘,思考一下 group 出現的目的和 dispatch_sync
的特色。
最後調用 dispatch_group_notify
方法。這個方法表示把第三個參數 block 傳入第二個參數隊列中去。並且能夠保證第三個參數 block 執行時,group 中的全部任務已經所有完成。
dispatch_group_wait
方法是一個頗有用的方法,它的完整定義以下:
dispatch_group_wait(group: dispatch_group_t, _ timeout: dispatch_time_t) -> Int
第一個參數表示要等待的 group,第二個則表示等待時間。返回值表示通過指定的等待時間,屬於這個 group 的任務是否已經所有執行完,若是是則返回 0,不然返回非 0。
第二個 dispatch_time_t
類型的參數還有兩個特殊值:DISPATCH_TIME_NOW
和 DISPATCH_TIME_FOREVER
。
前者表示馬上檢查屬於這個 group 的任務是否已經完成,後者則表示一直等到屬於這個 group 的任務所有完成。
經過 GCD 還能夠進行簡單的定時操做,好比在 1 秒後執行某個 block 。代碼以下:
let mainQueue = dispatch_get_main_queue() let time = dispatch_time(DISPATCH_TIME_NOW, Int64(3) * Int64(NSEC_PER_SEC)) NSLog("%@",NSThread.currentThread()) dispatch_after(time, mainQueue, {() in NSLog("%@",NSThread.currentThread())})
dispatch_after
方法有三個參數。第一個表示時間,也就是從如今起日後三秒鐘。第2、三個參數分別表示要提交的任務和提交到哪一個隊列。
須要注意的是和dispatch_after
僅表示在指定時間後提交任務,而非執行任務。若是任務提交到主隊列,它將在main runloop中執行,對於每隔1/60秒執行一次的RunLoop,任務最多有可能在3+1/60秒後執行。
NSOperation
和 NSOperationQueue
主要涉及這幾個方面:
NSOperation
和 NSOperationQueue
用法介紹 NSOperation
的暫停、恢復和取消 NSOperation
的狀態進行檢測 NSOperation
的之間的依賴關係從簡單意義上來講,NSOperation
是對 GCD 中的 block 進行的封裝,它也表示一個要被執行的任務。
與 GCD 中的 block 相似,NSOperation
對象有一個 start()
方法表示開始執行這個任務。
不只如此,NSOperation
表示的任務還能夠被取消。它還有三種狀態 isExecuted
、isFinished
和 isCancelled
以方便咱們經過 KVC 對它的狀態進行監聽。
想要開始執行一個任務能夠這麼寫:
let operation = NSBlockOperation { () -> Void in print(NSThread.currentThread()) } operation.addExecutionBlock { () -> Void in print("execution block1 -- \(NSThread.currentThread())") } operation.start()
以上代碼會獲得這樣的執行結果:
<NSThread: 0x7f89b1c070f0>{number = 1, name = main} execution block1 -- <NSThread: 0x7f89b1e17030>{number = 2, name = (null)}
首先咱們建立了一個NSBlockOperation
,而且設置好它的 block ,也就是將要執行的任務。這個任務會在主線程中執行。
用 NSBlockOperation
是由於 NSOperation
是一個基類,不該該直接生成 NSOperation
對象,而是應該用它的子類。NSBlockOperation
是蘋果預約義的子類,它能夠用來封裝一個或多個 block ,後面會介紹如何本身建立 NSOperation
的子類。
同時,還能夠調用 addExecutionBlock
方法追加幾個任務,這些任務會並行執行(也就是說頗有可能運行在別的線程裏)。
最後,調用 start
方法讓 NSOperation
方法運行起來。start
是一個同步方法。
剛剛咱們知道,默認的 NSOperation
是同步執行的。簡單的看一下 NSOperation
類的定義會發現它有一個只讀屬性 asynchronous
這意味着若是想要異步執行,就須要自定義 NSOperation
的子類。或者使用 NSOperationQueue
NSOperationQueue
相似於 GCD 中的隊列。咱們知道 GCD 中的隊列有三種:主隊列、串行隊列和並行隊列。NSOperationQueue
更簡單,只有兩種:主隊列和非主隊列。
咱們本身生成的 NSOperationQueue
對象都是非主隊列,主隊列能夠用 NSOperationQueue.mainQueue
取得。
NSOperationQueue
的主隊列是串行隊列,並且其中全部 NSOperation
都會在主線程中執行。
對於非主隊列來講,一旦一個 NSOperation
被放入其中,那這個NSOperation
必定是併發執行的。由於 NSOperationQueue
會爲每個 NSOperation
建立線程並調用它的 start
方法。
NSOperationQueue
有一個屬性叫 maxConcurrentOperationCount
,它表示最多支持多少個 NSOperation
併發執行。若是 maxConcurrentOperationCount
被設爲 1,就覺得這個隊列是串行隊列。
所以,NSOperationQueue
和 GCD 中的隊列有這樣的對應關係:
回到開頭的問題,如何利用 NSOperationQueue
實現異步操做呢,代碼以下:
let operationQueue = NSOperationQueue() let operation = NSBlockOperation () operation.addExecutionBlock { () -> Void in print("exec block1 -- \(NSThread.currentThread())") } operation.addExecutionBlock { () -> Void in print("exec block2 -- \(NSThread.currentThread())") } operation.addExecutionBlock { () -> Void in print("exec block3 -- \(NSThread.currentThread())") } operationQueue.addOperation(operation) print("操做結束")
獲得運行結果以下:
操做結束 exec block1 -- <NSThread: 0x125672f10>{number = 2, name = (null)} exec block2 -- <NSThread: 0x12556ba40>{number = 3, name = (null)} exec block3 -- <NSThread: 0x125672f10>{number = 2, name = (null)}
使用 NSOperationQueue
來執行任務與以前的區別在於,首先建立一個非主隊列。而後用 addOperation
方法替換以前的 start
方法。剛剛已經說過,NSOperationQueue
會爲每個 NSOperation
創建線程並調用他們的 start
方法。
觀察一下運行結果,全部的 NSOperation
都沒有在主線程執行,從而成功的實現了異步、並行處理。
在學習 NSOperation
的時候,咱們老是用GCD的概念去解釋。可是 NSOperation
做爲對 GCD 更高層次的封裝,它有着一些 GCD 沒法實現(或者至少說很難實現)的特性。因爲 NSOperation
和 NSOperationQueue
良好的封裝,這些新特性的使用都很是簡單。
若是咱們有兩次網絡請求,第二次請求會用到第一次的數據。假設此時網絡狀況很差,第一次請求超時了,那麼第二次請求也沒有必要發送了。固然,用戶也有可能人爲地取消某個 NSOperation
。
當某個 NSOperation
被取消時,咱們應該儘量的清除 NSOperation
內部的數據而且把 cancelled
和 finished
設爲 true
,把executing
設爲 false
。
//取消某個NSOperation operation1.cancel() //取消某個NSOperationQueue剩餘的NSOperation queue.cencelAllOperations()
依然考慮剛剛所說的兩次網絡請求的例子。由於第二次請求會用到第一次的數據,因此咱們要保證發出第二次請求的時候第一個請求已經執行完。可是咱們同時還但願利用到 NSOperationQueue
的併發特性(由於可能不止這兩個任務)。
這時候咱們能夠設置 NSOperation
之間的依賴關係。語法很是簡潔:
operation2.addDependency(operation1)
須要注意的是 NSOperation
之間的相互依賴會致使死鎖
queue.suspended = true //暫停queue中全部operation queue.suspended = false //恢復queue中全部operation
這個更加簡單,只要修改 suspended
屬性便可
GCD中,任務(block)是沒有優先級的,而隊列具備優先級。和GCD相反,咱們通常考慮 NSOperation
的優先級
NSOperation
有一個NSOperationQueuePriority
枚舉類型的屬性 queuePriority
public enum NSOperationQueuePriority : Int { case VeryLow case Low case Normal case High case VeryHigh }
須要注意的是,NSOperationQueue
也不能徹底保證優先級高的任務必定先執行。
通過以上分析,咱們大概對 NSOperation
和 GCD
都有了比較詳細的瞭解,同時在親自運用這二者的過程當中有了本身的理解。
GCD以 block 爲單位,代碼簡潔。同時 GCD 中的隊列、組、信號量、source、barriers 都是組成並行編程的基本原語。對於一次性的計算,或是僅僅爲了加快現有方法的運行速度,選擇輕量化的 GCD 就更加方便。
而 NSOperation
能夠用來規劃一組任務之間的依賴關係,設置它們的優先級,任務能被取消。隊列能夠暫停、恢復。NSOperation
還能夠被子類化。這些都是 GCD 所不具有的。
因此咱們要記住的是:
NSOperation
和 GCD 並非互斥的,有效地結合二者能夠開發出更棒的應用
NSOperation
有本身獨特的優點,GCD
也有一些強大的特性。接下來咱們由淺入深,討論如下幾個部分:
dispatch_suspend
和 dispatch_resume
dispathc_once
dispatch_barrier_async
dispatch_semaphore
咱們知道NSOperationQueue
有暫停(suspend)和恢復(resume)。其實GCD中的隊列也有相似的功能。用法也很是簡單:
dispatch_suspend(queue) //暫停某個隊列 dispatch_resume(queue) //恢復某個隊列
這些函數不會影響到隊列中已經執行的任務,隊列暫停後,已經添加到隊列中但尚未執行的任務不會執行,直到隊列被恢復。
首先咱們來看一下最簡單的 dispathc_once
函數,這在單例模式中被普遍使用。
dispathc_once
函數能夠確保某個 block 在應用程序執行的過程當中只被處理一次,並且它是線程安全的。因此單例模式能夠很簡單的實現,以 OC 中 Manager 類爲例+ (Manager *)sharedInstance { static Manager *sharedManagerInstance = nil; static dispatch_once_t once; dispatch_once($once, ^{ sharedManagerInstance = [[Manager alloc] init]; }); return sharedManagerInstance; }
這段代碼中咱們建立一個值爲 nil 的 sharedManagerInstance
靜態對象,而後把它的初始化代碼放到 dispatch_once
中完成。
這樣,只有第一次調用 sharedInstance
方法時纔會進行對象的初始化,之後每次只是返回 sharedManagerInstance
而已。
咱們知道數據在寫入時,不能在其餘線程讀取或寫入。可是多個線程同時讀取數據是沒有問題的。因此咱們能夠把讀取任務放入並行隊列,把寫入任務放入串行隊列,而且保證寫入任務執行過程當中沒有讀取任務能夠執行。
這樣的需求比較常見,GCD提供了一個很是簡單的解決辦法——dispatch_barrier_async
假設咱們有四個讀取任務,在第2、三個任務之間有一個寫入任務,代碼大概是這樣:
let queue = dispatch_queue_create("com.gcd.kt", DISPATCH_QUEUE_CONCURRENT) dispatch_async(queue, block1_for_reading) dispatch_async(queue, block2_for_reading) /* 這裏插入寫入任務,好比: dispatch_async(queue, block_for_writing) */ dispatch_async(queue, block3_for_reading) dispatch_async(queue, block4_for_reading)
若是代碼這樣寫,因爲這幾個 block 是併發執行,就有可能在前兩個 block 中讀取到已經修改了的數據。若是是有多寫入任務,那問題更嚴重,可能會有數據競爭。
若是使用 dispatch_barrier_async
函數,代碼就能夠這麼寫:
dispatch_async(queue, block1_for_reading) dispatch_async(queue, block2_for_reading) dispatch_barrier_async(queue, block_for_writing) dispatch_async(queue, block3_for_reading) dispatch_async(queue, block4_for_reading)
dispatch_barrier_async
會把並行隊列的運行週期分爲這三個過程:
dispatch_barrier_async
中的任務,這時候即便向並行隊列提交任務,也不會執行 dispatch_barrier_async
中的任務執行完成後,並行隊列恢復正常。總的來講,dispatch_barrier_async
起到了「承上啓下」的做用。它保證此前的任務都先於本身執行,此後的任務也遲於本身執行。正如barrier的含義同樣,它起到了一個柵欄、或是分水嶺的做用。
這樣一來,使用並行隊列和 dispatc_barrier_async
方法,就能夠高效的進行數據和文件讀寫了。
首先介紹一下信號量(semaphore)的概念。信號量是持有計數的信號,不過這麼解釋等於沒解釋。咱們舉個生活中的例子來看看。
假設有一個房子,它對應進程的概念,房子裏的人就對應着線程。一個進程能夠包括多個線程。這個房子(進程)有不少資源,好比花園、客廳等,是全部人(線程)共享的。
可是有些地方,好比臥室,最多隻有兩我的能進去睡覺。怎麼辦呢,在臥室門口掛上兩把鑰匙。進去的人(線程)拿着鑰匙進去,沒有鑰匙就不能進去,出來的時候把鑰匙放回門口。
這時候,門口的鑰匙數量就稱爲信號量(Semaphore)。很明顯,信號量爲0時須要等待,信號量不爲零時,減去1並且不等待。
在GCD中,建立信號量的語法以下:
var semaphore = dispatch_semaphore_create(2)
這句代碼經過 dispatch_semaphore_create
方法建立一個信號量並設置初始值爲 2。而後就能夠調用 dispatch_semaphore_wait
方法了。
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER)
dispatch_semaphore_wait
方法表示一直等待直到信號量的值大於等於 1,當這個方法執行後,會把第一個信號量參數的值減 1。
第二個參數是一個 dispatch_time_t
類型的時間,它表示這個方法最大的等待時間。這在第一章中已經講過,好比 DISPATCH_TIME_FOREVER
表示永久等待。
返回值也和 dispatch_group_wait
方法同樣,返回 0 表示在規定的等待時間內第一個參數信號量的值已經大於等於 1,不然表示已超過規定等待時間,但信號量的值仍是 0。
dispatch_semaphore_wait
方法返回 0,由於此時的信號量的值大於等於一,任務得到了能夠執行的權限。這時候咱們就能夠安全的執行須要進行排他控制的任務了。
任務結束時還須要調用 dispatch_semaphore_signal()
方法,將信號量的值加 1。這相似於以前所說的,從臥室出來要把鎖放回門上,不然後來的人就沒法進入了。
咱們來看一個完整的例子:
var semaphore = dispatch_semaphore_create(1) let queue = dispatch_queue_create("com.gcd.kt", DISPATCH_QUEUE_CONCURRENT) var array: [Int] = [] for i in 1...100000 { dispatch_async(queue, { () -> Void in /* 某個線程執行到這裏,若是信號量值爲1,那麼wait方法返回1,開始執行接下來的操做。 與此同時,由於信號量變爲0,其它執行到這裏的線程都必須等待 */ dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER) /* 執行了wait方法後,信號量的值變成了0。能夠進行接下來的操做。 這時候其它線程都得等待wait方法返回。 能夠對array修改的線程在任意時刻都只有一個,能夠安全的修改array */ array.append(i) /* 排他操做執行結束,記得要調用signal方法,把信號量的值加1。 這樣,若是有別的線程在等待wait函數返回,就由最早等待的線程執行。 */ dispatch_semaphore_signal(semaphore) }) }