iOS多執行緒程式設計總結(上)

bestswifter發表於2017-12-27

#多執行緒之謎

很長時間以來,我個人(可能還有很多同學),對多執行緒程式設計都存在一些誤解。一個很明顯的表現是,很多人有這樣的看法:

新開一個執行緒,能提高速度,避免阻塞主執行緒

畢竟多執行緒嘛,幾個執行緒一起跑任務,速度快,還不阻塞主執行緒,簡直完美。

在某些場合,我們還見過另一個“高深”的名詞——“非同步”。這東西好像和多執行緒挺類似,經過一番百度(閱讀了很多質量層次不齊的文章)之後,很多人也沒能真正搞懂何為“非同步”。

於是,帶著對“多執行緒”和“非同步”的懵懂,很多人又開開心心踏上了多執行緒程式設計之旅,比如文章待會兒會提到的GCD。

#何為多執行緒

其實,如果不考慮其他任何因素和技術,多執行緒有百害而無一利,只能浪費時間,降低程式效率。

是的,我很清醒的寫下這句話。

試想一下,一個任務由十個子任務組成。現在有兩種方式完成這個任務:

  1. 建十個執行緒,把每個子任務放在對應的執行緒中執行。執行完一個執行緒中的任務就切換到另一個執行緒。
  2. 把十個任務放在一個執行緒裡,按順序執行。

作業系統的基礎知識告訴我們,執行緒,是執行程式最基本的單元,它有自己棧和暫存器。說得再具體一些,執行緒就是**“一個CPU執行的一條無分叉的命令列”**。

對於第一種方法,在十個執行緒之間來回切換,就意味著有十組棧和暫存器中的值需要不斷地被備份、替換。 而對於對於第二種方法,只有一組暫存器和棧存在,顯然效率完勝前者。

#併發與並行

通過剛剛的分析我們看到,多執行緒本身會帶來效率上的損失。準確來說,在處理併發任務時,多執行緒不僅不能提高效率,反而還會降低程式效率。

所謂的“併發”,英文翻譯是concurrent。要注意和**“並行(parallelism)”**的區別。

併發指的是一種現象,一種經常出現,無可避免的現象。它描述的是“多個任務同時發生,需要被處理”這一現象。它的側重點在於“發生”。

比如有很多人排隊等待檢票,這一現象就可以理解為併發。

並行指的是一種技術,一個同時處理多個任務的技術。它描述了一種能夠同時處理多個任務的能力,側重點在於“執行”。

比如景點開放了多個檢票視窗,同一時間內能服務多個遊客。這種情況可以理解為並行。

並行的反義詞就是序列,表示任務必須按順序來,一個一個執行,前一個執行完了才能執行後一個。

我們經常掛在嘴邊的“多執行緒”,正是採用了並行技術,從而提高了執行效率。因為有多個執行緒,所以計算機的多個CPU可以同時工作,同時處理不同執行緒內的指令。

#小結 併發是一種現象,面對這一現象,我們首先建立多個執行緒,真正加快程式執行速度的,是並行技術。也就是讓多個CPU同時工作。而多執行緒,是為了讓多個CPU同時工作成為可能。

#同步與非同步 同步方法就是我們平時呼叫的哪些方法。因為任何有程式設計經驗的人都知道,比如在第一行呼叫foo()方法,那麼程式執行到第二行的時候,foo方法肯定是執行完了。

所謂的非同步,就是允許在執行某一個任務時,函式立刻返回,但是真正要執行的任務稍後完成。

比如我們在點選儲存按鈕之後,要先把資料寫到磁碟,然後更新UI。同步方法就是等到資料儲存完再更新UI,而非同步則是立刻從儲存資料的方法返回並向後執行程式碼,同時真正用來儲存資料的指令將在稍後執行。

#區別與聯絡

###區別

假設現在有三個任務需要處理。假設單個CPU處理它們分別需要3、1、1秒。

並行與序列,其實討論的是處理這三個任務的速度問題。如果三個CPU並行處理,那麼一共只需要3秒。相比於序列處理,節約了兩秒。

而同步/非同步,其實描述的是任務之間先後順序問題。假設需要三秒的那個是儲存資料的任務,而另外兩個是UI相關的任務。那麼通過非同步執行第一個任務,我們省去了三秒鐘的卡頓時間。

###聯絡

對於同步執行的三個任務來說,系統傾向於在同一個執行緒裡執行它們。因為即使開了三個執行緒,也得等他們分別在各自的執行緒中完成。並不能減少總的處理時間,反而徒增了執行緒切換(這就是文章開頭舉的例子)

對於非同步執行的三個任務來說,系統傾向於在三個新的執行緒裡執行他們。因為這樣可以最大程度的利用CPU效能,提升程式執行效率。

###結論

於是我們可以得出結論,在需要同時處理IO和UI的情況下,真正起作用的是非同步,而不是多執行緒。可以不用多執行緒(因為處理UI非常快),但不能不用非同步(否則的話至少要等IO結束)。

注意到我把“傾向於”這三個加粗了,也就是說非同步方法並不一定永遠在新執行緒裡面執行,反之亦然。在接下來關於GCD的部分會對此做出解釋。

#GCD簡介

GCD以block為基本單位,一個block中的程式碼可以為一個任務。下文中提到任務,可以理解為執行某個block

同時,GCD中有兩大最重要的概念,分別是“佇列”和“執行方式”。

使用block的過程,概括來說就是把block放進合適的佇列,並選擇合適的執行方式去執行block的過程。

###佇列總的來說可以分為三種:

  1. 序列佇列(先進入佇列的任務先出佇列,每次只執行一個任務)
  2. 併發佇列(依然是“先入先出”,不過可以形成多個任務併發)
  3. 主佇列(這是一個特殊的序列佇列,而且佇列中的任務一定會在主執行緒中執行)

###兩種基本的執行方式

  1. 同步執行
  2. 非同步執行

關於同步非同步、序列並行和執行緒的關係,下面通過一個表格來總結

同步 非同步
主佇列 在主執行緒中執行 在主執行緒中執行
序列佇列 在當前執行緒中執行 新建執行緒執行
併發佇列 在當前執行緒中執行 新建執行緒執行

可以看到,同步方法不一定在本執行緒,非同步方法方法也不一定新開執行緒(考慮主佇列)。

然而事實上,在本文一開始就揭開了“多執行緒”的神祕面紗,所以我們在程式設計時,更應該考慮的是:

同步 Or 非同步

以及

序列 Or 並行

而非僅僅考慮是否新開執行緒。

當然,瞭解任務執行在那個執行緒中也是為了更加深入的理解整個程式的執行情況,尤其是接下來要討論的死鎖問題。

#GCD的死鎖問題

在使用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的時候應該分析當前處於哪個佇列,以及任務會提交到哪個佇列。

#GCD任務組

瞭解完佇列之後,很自然的會有一個想法:我們怎麼知道所有任務都已經執行完了呢?

在單個序列佇列中,這個不是問題,因為只要把回撥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

dispatch_group_wait方法是一個很有用的方法,它的完整定義如下:

dispatch_group_wait(group: dispatch_group_t, _ timeout: dispatch_time_t) -> Int

第一個參數列示要等待的group,第二個則表示等待時間。返回值表示經過指定的等待時間,屬於這個group的任務是否已經全部執行完,如果是則返回0,否則返回非0。

第二個dispatch_time_t型別的引數還有兩個特殊值:DISPATCH_TIME_NOWDISPATCH_TIME_FOREVER

前者表示立刻檢查屬於這個group的任務是否已經完成,後者則表示一直等到屬於這個group的任務全部完成。

#dispatch_after方法

通過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方法有三個引數。第一個表示時間,也就是從現在起往後三秒鐘。第二三個引數分別表示要提交的任務和提交到哪個佇列。

需要注意的是dispatch_after僅表示在指定時間後提交任務,而非執行任務。如果任務提交到主佇列,它將在main runloop中執行,對於每隔1/60秒執行一次的RunLoop,任務最多有可能在3+1/60秒後執行。

#總結

到目前為止,我們已經理解了多執行緒程式設計的基本概念,以及GCD的簡單使用。在下一章iOS多執行緒程式設計總結(中)中會介紹更高一層的NSOperation。

相關文章