標籤:
GCD對於iOS開發人員來說肯定不陌生,他和NSThread,NSOperation一起作為iOS開發中主要的三種多線程實現方法,而GCD是最最底層的,所以對於作為一個iOSer,GCD是必須掌握的。
我通過對於以下兩篇文章的閱讀,基本上掌握了GCD的基本使用方法。所以首先感謝兩位作者。
- GCD 深入理解:第一部分
- iOS多線程開發——GCD的使用與多線程開發淺析(二)
一、基本概念
對於新手來說,最常見同時最容易搞混的的莫過於GCD中的一些基本概念了。
並行與並發(Parallelism && Concurrency)
- 並行:顧名思義就是同時行動,兩個任務在兩個線程(Thread)上進行處理,彼此互不干涉。而究其根本是因為多核進行處理,從而更快的解決任務。(可以類比於兩條水管中同時給遊泳池放水)
- 並發:顧名思義,同時發生。對於有的效能較差的機子,比如說那些只有單核的裝置,為了讓使用者感覺能夠同時處理多個任務,他就需要通過不斷的切換正在處理的線程,從而實現一種“偽並行”,這樣就防止使用者因為一個任務進行太久而無法進行下一個任務。(可以類比於我用兩個口給遊泳池放水,不過因為只有一根水管,而放水的方向有2個方面,所以需要不斷的切換,從而實現兩邊同時達到需要的水位)
- 區分如:
串列與並發(Serial && Concurrent)
- 串列:由於所有任務在一個線程被執行的時候只有一個任務會被執行,所以每個任務都要依賴於先來先處理(FIFO)原則,處理所有得到的內容。
- 並行:當接收到任務的時候,並行狀態下,他會開啟多個線程,將每個任務都分配到各個線程中,從而使得每個任務都能夠在同一時間被執行。這樣有效減少了所消耗的時間。
- 區分圖如下:
隊列:就像前面所說,當你告訴電腦你需要執行哪些事情的時候,電腦就會把你告訴他的一件件事情放到自己的隊伍中,很明顯,先告訴他的事情放在前面,後告訴他的事情放在後面,即先進先處理(FIFO)原則處理所有事物,就像排隊一樣。
- 在GCD中主要的隊列有2種,就是前面提到的:
- 蘋果公司提供給我們的已存在的隊列有5種(不包括後期我們自己添加的內容)
- 主隊列(main queue):預設情況下所有事情都是在主隊列下進行處理,只要有編碼經驗的人來說,主隊列很明顯是(串列隊列)。
- 全域調度隊列(Global Dispatch Queues):預設情況下全域隊列是系統提供的並發隊列的統稱,根據隊列的優先程度不同分為以下幾個(優先度從低到高):
- background
- low
- default
- high
- 同步與非同步(synchronization && asynchronous)
-同步:同步指的是在原來的內容執行完成之後,再執行你下面所需要的任務。
-非同步:非同步指的是在和原來的內容執行的同時,在另一個地方同時處理新的任務。
二、基本使用
既然學了,那肯定要使用它。那麼我們先類比一個環境:
這個程式裡面只有主隊列,然後你想從網上下載一個圖片,放到你自己的手機裡面,然後使用者就可以對手機裡面的這張圖片進行各種各樣的處理。
但是問題來了:我們都知道下載需要時間,雖然現在已經是4G時代,下載速度很快,但是如果這個圖片很大,那麼他就需要很長的時間來進行等待。而在這段時間內,使用者什麼事情都不能幹,只能默默的乾等著。
這很明顯是一個很差的使用者體驗
那麼既然我們已經知道問題所在,就需要想辦法解決它:
- 我們需要在使用者等待的時候添加一個動畫效果,告訴他我們正在下載(這個和今天主題無關,我們就不繼續深入瞭解)
- 將下載任務放到後台(如果仍舊放在前台的話,就算有動畫,動畫效果也不會動)
那麼這個時候就需要非同步隊列的存在了
就像前面的基本概念中所提到的,執行分為同步執行和非同步執行,任務隊列分為串列隊列和並發隊列。所以他們彼此一一結合,總共有4種情況存在,主要的彼此結合的狀態圖如下所示:
單隊列串列隊列,同步執行
代碼如下:
func serialDispatchQueueWithSync() { let queue = dispatch_queue_create("serialDispatchQueue", DISPATCH_QUEUE_SERIAL) print("0") dispatch_sync(queue) { print("1") } dispatch_sync(queue) { print("2") } dispatch_sync(queue) { print("3") } dispatch_sync(queue) { print("4") } dispatch_sync(queue) { print("5") } print("6")}
運行結果如下:
0123456
串列隊列,非同步執行
代碼如下:
func serialDispatchQueueWithASync() { let queue = dispatch_queue_create("serial", DISPATCH_QUEUE_SERIAL) print("0") dispatch_async(queue) { print("1") } dispatch_async(queue) { print("2") } dispatch_async(queue) { print("3") } dispatch_async(queue) { print("4") } dispatch_async(queue) { print("5") } dispatch_async(queue) { print("6") } print("7")}
運行結果如下:
07123456
解釋:因為queue是非同步執行的,即調用dispatch_async函數,所以輸出1 2 3 4 5 6的時候是在另一個線程中的,所以和主線程中的輸出1 7 無關。
並發隊列,同步執行
代碼如下:
func concurrentDispatchsQueueWithSync() { let queue = dispatch_queue_create("concurrent", DISPATCH_QUEUE_CONCURRENT) print("0") dispatch_sync(queue) { print("1") print("12") print("13") } dispatch_sync(queue) { print("2") print("22") print("23") } dispatch_sync(queue) { print("3") print("32") print("33") } dispatch_sync(queue) { print("4") print("42") print("43") } print("5")}
運行結果如下:
0112132222333233442435
並發隊列,非同步執行
代碼如下:
func concurrentDispatchsQueueWithASync() { let queue = dispatch_queue_create("concurrent", DISPATCH_QUEUE_CONCURRENT) print("0") dispatch_async(queue) { print("1") print("12") print("13") } dispatch_async(queue) { print("2") print("22") print("23") } dispatch_async(queue) { print("3") print("32") print("33") } dispatch_async(queue) { print("4") print("42") print("43") } print("5")}
運行結果如下:
071212322134322354233524353
其他常見用法:掛起與恢複
- 線程掛起:
dispatch_suspnd()
- 線程恢複:
dispatch_resume()
訊號量
由於有的時候(比如說在建立添加數組中的對象的時候),因為不同線程操縱的是同一個對象,所以很容易發生報錯,這個時候需要通過訊號量來對對應的內容進行控制,當訊號量為0的時候,進入等待狀態,不能執行下面的內容;當訊號量為1的時候,可以執行,同時訊號量減一,等到執行完畢,訊號量加一。
- 等待執行dispatch_semaphore_wait()
- 訊號量加一 dispatch_semahore_signal
只執行一次
有的時候,有的東西的建立只能被建立一次(即單例),這個時候就需要用到dispatch_once()
代碼如下:
var token: dispatch_once_t = 0func test() { dispatch_once(&token) { println("This is printed only on the first call to test()") } println("This is printed for each call to test()")}
關於讀寫問題的解決方案(買票問題)
由於日常編碼的過程中經常會遇到,當你在調用這個這個變數的時候。另一個線程也在調用這個變數(只在並發過程中),如果兩個線程都是在讀取資料,那並沒有什麼問題,但是如果其中一個在寫入,或者兩個都在寫入,那麼就會出現很大問題,所以蘋果官方在GCD中也給我們準備了一個方法,讓我們解決這個問題,那就是dispatch_barrier_async,這個方法讓對的內容在並發過程中加入,從而方便組織內容的修改,從而使得對應的內容只能在當前線程中被進行修改。
常見問題:死結:
let queue = dispatch_get_main_queue()dispatch_async(queue) { dispatch_sync(queue, { print("1") })}
產生原理:因為dispatch_sync()會等到本身結束之後才會在主線程繼續執行接下去的代碼,但是dispatch_sync()這個方法調用的就是主線程,所以午飯等到主線程結束,所以就無法返回,就會卡在這裡。
解釋:由於是並發隊列,所以他會建立多個線程,從而保證每個線程的任務都能夠儘快完成,所以順序有一定的出入。
最後通過兩張動態圖來最後總結單隊列:dispatch_sync
- (void)viewDidLoad{ [super viewDidLoad]; dispatch_sync(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{ NSLog(@"First Log"); }); NSLog(@"Second Log");}
- 主隊列按照預定的順序下來
- viewDidLoad在主線程進行執行。
- 直到執行到
dispatch_sync
- 調用
dispatch_sync代碼,將block添加到全域隊列中,主隊列掛起。
- 全域隊列先完成之前存放在全域隊列中的內容。
- 完成之前的任務後,執行
dispatch_sync的block中的內容。
- 完成block中的任務,主隊列上的任務得以恢複
- 主隊列繼續執行其他任務。
dispatch_async
- (void)viewDidLoad{ [super viewDidLoad]; dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{ NSLog(@"First Log"); }); NSLog(@"Second Log");}
- 主隊列一路按順序執行任務——接著是一個執行個體化
UIViewController 的任務,其中包含了viewDidLoad。
viewDidLoad在主線程執行。
- 主線程目前在
viewDidLoad內,正要到達dispatch_async。
dispatch_async Block被添加到一個全域隊列中,將在稍後執行。
viewDidLoad在添加dispatch_async到全域隊列後繼續進行,主線程把注意力轉向剩下的任務。同時,全域隊列並發地處理它未完成地任務。記住Block在全域隊列中將按照(FIFO)順序出列,但可以並發執行。
- 添加到
dispatch_async的代碼塊開始執行。
dispatch_async Block完成,兩個NSLog語句將它們的輸出放在控制台上。
調度組
既然能夠處理一個個的任務,那麼我們就繼續類比一個環境,當我們需要在網上下載內容的時候(這些內容需要彼此聯絡在一起才能正常使用),這個時候,上面的單隊列就不夠了(或者說如果使用單隊列產生的效果不是時間太長,就是檔案的完整性不夠好)
這個時候我們就需要引入多隊列,當多個內容都處理完成之後,讓系統告訴我們,我們已經完成了以上的下載。可以繼續做下一步事情了。
任務開始
在GCD中,我們可以通過dispatch_group_enter來通知當前任務的開始,而與之相對應的,我們必須在任務完成後,手動通知調度組任務結束(dispatch_group_leave)這樣才能讓調度組知道我們這個任務已經結束。
代碼如下(由於demo中有一個Photo類,所以此處貼上OC代碼):
dispatch_group_enter(downloadGroup); // 3Photo *photo = [[Photo alloc] initwithURL:url withCompletionBlock:^(UIImage *image, NSError *_error) { if (_error) { error = _error; }dispatch_group_leave(downloadGroup); // 4}];[[PhotoManager sharedManager] addPhoto:photo];
任務提醒
當我們所有的任務都手動通知後,那麼就需要條用提醒來告訴他(我已經完成了所有內容,接下去需要試試哪裡能夠執行了),而提醒方式在GCD中有兩種:
dispatch_group_wait() + dispatch_async()
dispatch_group_notify
其他常見用法dispatch_apply()
有的時候需要調用for迴圈來反覆執行,但是當需要執行的代碼量偏大的時候,for的效率比較低,這個時候需要用dispatch_apply()來執行,這樣節省效率,不過當需要執行的代碼量比較小的時候,dispatch_apply()的效率就比較差了。
這個方法的效果和dispatch_sync一樣,所以要注意死結(後面會提到)
iOS多線程編程——淺談GCD