iOS 併發概念淺析

發表於2016-04-26

在進行iOS開發過程中,我們常會遇到網路請求、複雜計算、資料存取等比較耗時的操作,如果處理不合理,將對APP的流暢度產生較大影響。除了優化APP架構,併發(concurrency)是一個常用且較好的解決方法,但併發涉及序列、併發、並行、同步、非同步、多執行緒、GCD、NSOperation和NSOperationQueue等諸多容易混淆的概念,為求概念清晰明瞭,還請茗茶靜坐,聽我徐徐道來。

一、執行緒和任務

執行緒(thread) 和任務(task)是其他併發概念的基礎,因此也是首要需理清的概念,以下是其要點,詳細可參考Thread (computing)Task (computing))。

1)任務(task)

a)任務(task)是從程式中劃分出來,可以獨立執行的程式碼片段;
b)任務間可以新增依賴關係,如B任務依賴A任務,taskB.addDependency(taskA),這意味著B任務的執行以A任務完成為前提。

需要注意的是一個任務是否可以新增依賴,完全取決於任務封裝類和其相關管理類的具體實現,GCD不支援任務依賴,NSOperationQueue就支援任務依賴。

下面的程式碼是對一個任務的簡單封裝,並支援任務間的依賴。

2)執行緒(thread)

a)執行緒(thread)是程式碼執行的獨立路徑,一條執行緒只能同時執行一行程式碼(一行程式碼,其實就是一條處理器命令)。
b)執行緒中程式碼管理是以任務(task)為單位,一條執行緒逐行執行一個任務中的程式碼(任務可以取消),完成後再逐行執行下一個任務中的程式碼。
c)一條執行緒跳出一個任務的執行,即意味著這個任務的完成。因此,一條執行緒不能執行taskA一段時間後,還未完成就開始執行taskB,然後又返回執行taskA(這其實是單執行緒內的併發,與單核處理器的併發概念相同,具體實踐中不存線上程內併發)。

二、概念釋疑

1)並行(parallelism)和併發(concurrency)

併發和並行都是指多個任務可以同時執行,都屬於多執行緒程式設計概念,因此二者必然十分相近,容易混淆。二者區別只有一點,即是否多工執行於嚴格的同一時刻。併發不是,並行是。

單核處理器時代(一個處理器同一時刻只能執行一條命令),為了實現多工的同時執行,系統利用時間分片(time-slicing)技術,將處理器的執行時間切分為多個小片段,一會執行threadA,一會執行threadB,一會再執行threadA,即在多個執行緒(任務是線上程上執行的)之間來回跳動執行。雖不是真的多執行緒多工同時執行,但由於處理器的處理速度非常快,在使用者看來,仍然是同時執行的。這種偽多執行緒就是併發。

多核處理器時代(不同處理器相互獨立,可以同時執行各自的命令),多條執行緒完全可以嚴格同一時刻執行,這種真多執行緒就是並行。

上述程式碼是三個執行緒的併發執行,可以看出thread1、thread2和thread3不可能嚴格同一時刻執行,但也都獲得了處理器的一小段執行時間。

上述程式碼是三個執行緒的並行執行,可以看出thread1、thread2和thread3有一段時間同時執行。

現在的終端裝置無論是手機還是PC的處理器,大多都已是多核處理器,可以實現平行計算,但為了最大化的利用處理器的效能,現代處理器還是融合了time-slicing技術和多核技術,因此實際執行中,有時併發,有時並行。但相對來說,併發是個更廣泛的概念,因此Apple的多執行緒程式設計叫做concurrency programming併發程式設計。漢語中,併發和並行的區別其實沒那麼清晰,可以互用,而且有時用並行更加明確,如串並行比序列、併發針對性更強。(為概念清晰期間,下文中有時會用並行,其實即是併發。)

2)串並行與執行緒

序列(serial)和並行

序列和並行主要區別在於一個任務的執行是否以上一個任務的完成為前提。序列中,一個任務的執行必須以上一個任務執行結束為前提,並行中,一個任務的執行與上一個任務的執行狀態無關。以排隊買票為例,序列像單個買票隊伍,單個賣票視窗,必須一個一個來,序列像單個買票隊伍,多個賣票視窗,多個人可以同時買票。

上文為三個序列任務,任務A完成後,才執行任務B,B結束後,才最後執行任務C。

上文為三個並行任務,任務A早於任務C開始,卻晚於任務C結束。

串並行與執行緒

串並行主要關注多個任務之間的相互依賴關係,與執行緒無關。但實際中,任務是線上程中執行的,是否序列一定在單執行緒上執行,並行一定在多個執行緒中執行呢?並非如此。
單執行緒既可以實現序列,也可以實現並行。

上述程式碼是三個執行緒的並行執行,可以看出thread1、thread2和thread3有一段時間同時執行。

現在的終端裝置無論是手機還是PC的處理器,大多都已是多核處理器,可以實現平行計算,但為了最大化的利用處理器的效能,現代處理器還是融合了time-slicing技術和多核技術,因此實際執行中,有時併發,有時並行。但相對來說,併發是個更廣泛的概念,因此Apple的多執行緒程式設計叫做concurrency programming併發程式設計。漢語中,併發和並行的區別其實沒那麼清晰,可以互用,而且有時用並行更加明確,如串並行比序列、併發針對性更強。(為概念清晰期間,下文中有時會用並行,其實即是併發。)

2)串並行與執行緒

序列(serial)和並行

序列和並行主要區別在於一個任務的執行是否以上一個任務的完成為前提。序列中,一個任務的執行必須以上一個任務執行結束為前提,並行中,一個任務的執行與上一個任務的執行狀態無關。以排隊買票為例,序列像單個買票隊伍,單個賣票視窗,必須一個一個來,序列像單個買票隊伍,多個賣票視窗,多個人可以同時買票。

上文為三個序列任務,任務A完成後,才執行任務B,B結束後,才最後執行任務C。

上文為三個並行任務,任務A早於任務C開始,卻晚於任務C結束。

串並行與執行緒

串並行主要關注多個任務之間的相互依賴關係,與執行緒無關。但實際中,任務是線上程中執行的,是否序列一定在單執行緒上執行,並行一定在多個執行緒中執行呢?並非如此。
單執行緒既可以實現序列,也可以實現並行。

需要指出的是單執行緒內的並行已經類似單核處理器,並不是本文提及的常規執行緒,現實中也不常見。

多執行緒既可以實現序列,也可以實現並行,實際上,多執行緒序列和並行都很常見。

3)同步(synchronize)、非同步(asynchronous)與執行緒

同步和非同步是站在當前執行緒的角度,考察新增任務到新執行緒後,何時返回到當前執行緒執行下面的程式碼的問題,也即新新增的執行緒阻不阻塞當前執行緒。

同步

block1是新增到系統全域性佇列中的新任務,由於是同步的,因此block1執行返回後,才會回到當前主執行緒,執行//2及以後的程式碼。輸出結果為:

非同步

block1是新增到系統全域性佇列中的新任務,由於是非同步的,因此block1新增全域性佇列後(會在另外一個執行緒上執行),不等到執行完成,就會返回到當前主執行緒,執行//2及以後的程式碼,所以輸出結果可能為 21 12。但由於block1和主執行緒中的任務都是不耗時的簡單任務,而建立新的執行緒是要消耗一定時間的(主執行緒一直存在,不用新建立),因此很可能的輸出結果是:

同非同步結合的情形

如果同非同步結合:

block1是新增到系統全域性佇列中的新任務,由於是非同步的,因此block1新增全域性佇列後(會在另外一個執行緒上執行),不等到執行完成,就返回到當前主執行緒,執行//4及以後的程式碼,結果是block1所在的執行緒與主執行緒同時執行,因此理論上,D和A誰先輸出不一定。但由於block1和主執行緒中的任務都是不耗時的簡單任務,而建立新的執行緒是要消耗一定時間的(主執行緒一直存在,不用新建立),因此一般輸出結果為DA。

block1所線上程輸出完A後,將block2新增到主排程佇列中,由於是非同步的,因此block2新增主排程佇列後(會在主執行緒上執行),不等到執行完成,就返回到block2所在的執行緒,繼續執行,因此A和C一定會輸出,且C一定在A之後輸出。但block2卻不一定能執行,因為block1在執行時,主執行緒也在執行(主執行緒是序列單執行緒,任務按順序一個一個執行),如果此時主執行緒執行到//5對應的死迴圈,則block2一定不能被執行,B一定不能被輸出,如果此時主執行緒尚未執行到//5對應的死迴圈,block2已經新增到主執行緒中,則block2會被執行,B能被輸出。但由於主執行緒無需另外建立,block1(所對應的執行緒需另外建立)執行到新增block2到主排程佇列時,主執行緒很可能已經執行到//5對應的死迴圈,因此block2很可能不被執行。

//6前有個死迴圈,因此E一定不會被輸出。

因此可能的輸出結果是;DAC ADC ADCB DACB ACDB ACBD ABDC ABCD
但很可能的輸出結果為:

4)同非同步與串並行

序列和同步,並行和非同步似是完全不同的概念,一個關注任務的獨立關係,一個看中的是返回的時機。但事實上,序列和同步近似,併發和非同步相同,他們指代的事情幾乎完全相同。
就同步和序列而言,需要任務執行結束後才能返回,其實就是一個任務執行完成後,才能執行其他的任務,反應的就是序列依賴關係。

而非同步和並行就更相同了,不等任務執行完成,就直接返回,反應的就是併發任務之間的獨立性。

當然,同非同步所暗含的序列和並行是當前執行緒的任務與新執行緒的任務之間的相互關係。

三、GCD與NSOperationQueue

GCD(grand central dispatch)和NSOperationQueue二者均是系統級的多執行緒封裝,在使用時,我們只需建立任務佇列即可,其他的如執行緒創立、任務分配等,均由系統自動處理。不得不說,這讓多執行緒程式設計變得更高效,更簡單,當然並不是沒有坑。
需要強調的是,GCD和NSOperationQueue的使用核心是任務(task)和任務佇列(task queue),暫時可以忘了執行緒(thread)這煩人的概念。

關於GCD和NSOperationQueue網上已經有不少高質量的文章對其詳細介紹,我推薦《iOS並行開發:從NSOperation和排程佇列開始》,其對基本概念、使用方法等的介紹非常清晰詳盡,我這裡就不再贅述了,只寫一些我認為容易忽略卻影響認知深度的小知識點。當然如果你英語過硬,去直接看官方文件《ConcurrencyProgrammingGuide》是最好的。

1)GCD

GCD是基於C的API,因此比較底層。

GCD所管理的排程佇列(dispatch queue)主要有三類,序列佇列(private dispatch queue)、併發佇列 (global dispatch queue,又稱全域性排程佇列)和主佇列(main dispatch queue)。

我們常用的 dispatch_get_global_queue(_:_:)所獲得的dispatch queue就是全域性排程佇列(global dispatch queue),併發,而且全域性排程佇列是全域性共用的,每一個優先順序的全域性排程佇列只有一個實體。四種不同優先順序的全域性排程佇列對應的四種優先順序的執行緒,同一個優先順序的全域性排程佇列可以同時擁有多條相應優先順序的執行緒。

dispatch_get_main_queue()所獲得的dispatch queue是主排程佇列,主排程佇列是序列佇列。

2)NSOperationQueue

NSOperationQueue是對GCD的Objective-C封裝,相對於GCD具有更多先進的特性,如可以新增NSOperation依賴,取消NSOperation等。

NSOperationQueue是併發佇列,且不遵循先進先出FIFO排序原則。

四、總結與感悟

上文基本就是我對併發的認識,有幾個點我想在這裡再強調下,
1)串並行、同非同步與執行緒無關,單執行緒、多執行緒都可以實現串並行和同非同步。
2)序列和同步相同,非同步和並行相同,他們只是看待同一件事物的角度不同。
3)GCD和NSOperationQueue的使用核心是任務(task)和任務佇列(task queue)。
4)全域性排程佇列(global dispatch queue)是全域性共用的,系統有時也會向這些排程佇列新增系統任務。
5) App的主排程佇列是序列單執行緒佇列。

通過本文的淺析,希望達到了理清概念的最初目的,當然這些只是併發程式設計的第一步,併發執行緒還有竟態資源、死鎖等諸多大坑,要想真正掌握,我們繼續努力。

相關文章