GCD 與多執行緒程式設計

發表於2016-11-02

【Xcode-Men】Hi,我們團隊的井小二童鞋給我們取了個隊名:Xcode-Men,簡稱X-Men,是不是屌屌的。我們開這個部落格,是想記錄下記錄下每位小夥伴的成長,以及我們團隊的成長。我們的團隊很年輕,準90後和90後已經成了我們的主力。希望他們在不久的將來,能脫穎而出;也希望他們離開團隊時,能非常自豪地說自己曾經是X-Men的一員,這是一種情懷,情懷【噫,好像不能用表情】。

這是我們團隊的第一篇技術總結,由王瑞華童鞋撰寫。總結了自己在開發過程GCD的一些心得。歡迎大家吐槽。

最近在開發股神的專案中用到很多GCD,所以總結了一下GCD的一些使用。

1. 什麼是GCD


官方文件是這麼說的:

>Grand Central Dispatch (GCD) comprises language features, runtime libraries, and system enhancements that provide systemic, comprehensive improvements to the support for concurrent code execution on multicore hardware in iOS and OS X.

大致意思是說,GCD是非同步執行任務的技術,提供語言特徵、執行時庫、系統全面的改進的多核硬體上的支援。開發者只需要定義想執行的任務並加入到佇列中就可以了。

也就是說,GCD用我們難以置信的非常簡潔的記述方法,實現了極為複雜繁瑣的多執行緒程式設計,可以說是一項劃時代的技術。

同時它具有很多優點:

– 直觀而簡單的程式設計介面

– 提供自動和整體的執行緒池管理

– 提供彙編級調優的速度

– 更加高效的使用記憶體

– 不會trap核心

– 非同步分派任務到dispatch queue,不對導致queue死鎖

– 伸縮性強

– serial dispatch queue比鎖和其他同步原語更加高效

2 GCD APIs


2.1序列佇列Serial Diapatch Queue

建立序列佇列,對於第二個引數可為DISPATCH_QUEUE_SERIAL或者NULL,佇列中操作會按順序執行。

dispatch_queue_t myQueue = dispatch_queue_create(“com.my.wangruih”, DISPATCH_QUEUE_SERIAL);

dispatch_async(myQueue, ^{

NSLog(@”1″);

});

dispatch_async(myQueue, ^{

NSLog(@”2″);

});

dispatch_async(myQueue, ^{

NSLog(@”3″);

});

以上語句的輸出結果為:

2016-06-27 15:11:40.612 TestGCD[34781:4486699] 1

2016-06-27 15:11:40.613 TestGCD[34781:4486699] 2

2016-06-27 15:11:40.613 TestGCD[34781:4486699] 3

三者相互依賴,序列執行。

2.2並行佇列Concurrent Diapatch Queue

往佇列中追加的操作,沒有相互依賴關係,執行會放到不同的執行緒中,執行的先後順序未知。

dispatch_queue_t myQueue

= dispatch_queue_create(“com.my.wangruih”, DISPATCH_QUEUE_CONCURRENT);

dispatch_async(myQueue, ^{

NSLog(@”1″);

});

dispatch_async(myQueue, ^{

NSLog(@”2″);

});

dispatch_async(myQueue, ^{

NSLog(@”3″);

});

執行後發現日誌為:

2016-06-27 15:18:23.895 TestGCD[34834:4493908] 2

2016-06-27 15:18:23.895 TestGCD[34834:4493899] 1

2016-06-27 15:18:23.895 TestGCD[34834:4493934] 3

當然這只是其中的一種情況,按照排列組合計算,可以產生6種不同的順序。

如前所訴,concurrent dispatch queue並行執行多個處理,而serial只能執行1個追加處理。雖然一個serial queue只能執行一個,但是可以建立多個,代價便是產生多個執行緒,過多的執行緒會消耗大量記憶體,頻繁的上下文切換會大幅降低效能。

2.3 Main Dispatch Queue/Global Dispatch Queue

在我們不打算自己生成dispatch queue的情況下,系統會為我們準備兩個,那就是Main Dispatch Queue(就是serial queue),Global Dispatch Queue(就是concurrent queue)。

– Main顧名思義就是主執行緒,所有的操作都會追加到主執行緒去執行,大多都是使用者介面的更新。

– Global是說有程式使用的concurrent queue,同時它具有四個優先順序分別為High,Default,Low,Background。通過XNU核心根據優先順序來排程執行緒執行。

說完了佇列再來說說,操作佇列的一些方法


3.1 dispath_set_target_queue

變更生成的dispatch queue的執行優先順序使用dispatch_set_target_queue函式,在實際的工作裡我倒是沒用到過該方法。

dispatch_queue_t mySerialQueue = dispatch_queue_create(“com.my.wangruih”, DISPATCH_QUEUE_SERIAL);

dispatch_queue_t myGlobalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0);

dispatch_set_target_queue(mySerialQueue, myGlobalQueue);

dispatch_async(mySerialQueue, ^{

NSLog(@”hello”);

});

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

NSLog(@”world”);

});

執行結果如下,根據結果看出該方法修改了優先順序。

2016-06-27 15:46:05.972 TestGCD[34901:4517280] world

2016-06-27 15:46:05.973 TestGCD[34901:4517287] hello

3.2 dispatch_after

這個在工作中倒是常常用到,我常用在tableview reloaddata場景中,因為有一些諸如點贊動畫之類的,加入該方法是的點贊動畫執行完畢後的0.5~1s再執行reloadData操作。

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{

NSLog(@”what happened”);

});

// 輸出

2016-06-27 15:51:07.921 TestGCD[34918:4521806] hello

2016-06-27 15:51:11.216 TestGCD[34918:4521762] what happened

大致該語句執行是在3秒之後,也就是3秒之後追加block到main queue。這個時間並不是絕對的,block只是追加到了main runloop中,而main queue可能有大量其他處理,會使得時間變長。第一個引數是指定的dispatch_time_t型別。該值有dispatch_time或dispatch_walltime。前者通常用於相對時間,後者為絕對時間。如希望在2016/11/11 -0:0執行。

dispatch_time_t getDispatchTimeByData(NSDate *date) {

NSTimeInterval interval;

double second, subsecond;

struct timespec time;

dispatch_time_t milestone;

interval = [date timeIntervalSince1970];

subsecond = modf(interval, &second);

time.tv_sec = second;

time.tv_nsec = subsecond * NSEC_PER_SEC;

milestone = dispatch_walltime(&time, 0);

return milestone;

}

NSDate *date = [[NSDate alloc] initWithTimeIntervalSinceNow:5];

dispatch_after(getDispatchTimeByData(date), dispatch_get_main_queue(), ^{

NSLog(@”happen sth”);

});

// 輸出

2016-06-27 16:04:06.949 TestGCD[34946:4531114] what happened

2016-06-27 16:04:09.439 TestGCD[34946:4531114] happen sth

3.3 dispatch_group

如果有3個操作(A,B,C)需要在並行執行完A,B之後再執行C操作,可以有多個實現方式,當然可以通過新增依賴關係addDependency來實現。通過dispatch_group的方式依然可以實現。

無論向什麼樣的dispatch queue中追加處理,使用dispatch group都能監視到所有處理的結束,就可以將結束的處理追加到dispatch queue中,這是使用dispatch group的原因。

dispatch_group_t group = dispatch_group_create();

dispatch_queue_t queue = dispatch_queue_create(“com.my.wangruih”, DISPATCH_QUEUE_CONCURRENT);

dispatch_group_async(group, queue, ^{

NSLog(@”A”);

});

dispatch_group_async(group, queue, ^{

NSLog(@”B”);

});

dispatch_group_notify(group, queue, ^{

NSLog(@”C”);

});

// 輸出

2016-06-28 09:07:18.261 TestGCD[35461:4595668] B

2016-06-28 09:07:18.261 TestGCD[35461:4595678] A

2016-06-28 09:07:18.262 TestGCD[35461:4595668] C

3.4 dispatch_barrier_async

在訪問資料庫或者檔案時,有可能發生資料競爭。此方法的作用便是在併發佇列中,完成在它之前提交到佇列中的任務後打斷,單獨執行其block。起到了一個執行緒鎖的作用。同樣適用於上節中的,AB,C的執行順序問題。

dispatch_queue_t queue = dispatch_queue_create(“com.my.wangruih”, DISPATCH_QUEUE_CONCURRENT);

dispatch_async(queue, ^{

NSLog(@”A”);

});

dispatch_async(queue, ^{

NSLog(@”B”);

});

dispatch_barrier_async(queue, ^{

NSLog(@”C”);

});

dispatch_async(queue, ^{

NSLog(@”D”);

});

// 輸出

2016-06-28 09:21:43.864 TestGCD[35515:4607006] A

2016-06-28 09:21:43.864 TestGCD[35515:4607015] B

2016-06-28 09:21:43.865 TestGCD[35515:4607015] C

2016-06-28 09:21:43.865 TestGCD[35515:4607015] D

3.5 dispatch_sync

它以為著事件是同步發生的,也就是指定的block同步追加到指定的dispatch queue中。在追加block結束之前,dispatch_sync函式會一直等待。這裡有一個經常提及的問題來考察對同步的理解。

題目如下,輸出結果是什麼,為什麼會這樣。

NSLog(@”1″);

dispatch_sync(dispatch_get_main_queue(), ^{

NSLog(@”2″);

});

NSLog(@”3″);

輸出結果是:

2016-06-28 09:28:10.713 TestGCD[35528:4611901] 1

原因:由於main queue是序列queue,採用FIFO執行任務,也就是block操作加在了佇列之後,dispathc_sync堵塞了主執行緒等待block語句完成後執行main thread,但block語句由於執行緒阻塞永不會執行,所以導致一直等待死鎖。

3.6 dispatch_asyc

與sync不同,它是非同步的追加到指定的dispatch queue中。dispatch_async函式不做任何等待。

這裡也有一個對於該函式理解的題目,如下,推斷他的執行順序

NSLog(@”1″);

dispatch_async(dispatch_get_main_queue(), ^{

NSLog(@”2″);

});

for (int i = 0; i < 10; i++) {

NSLog(@”3″);

}

列印log日誌為:

2016-06-28 09:39:16.048 TestGCD[35575:4619720] 1

2016-06-28 09:39:16.049 TestGCD[35575:4619720] 3

2016-06-28 09:39:16.049 TestGCD[35575:4619720] 3

2016-06-28 09:39:16.050 TestGCD[35575:4619720] 3

2016-06-28 09:39:16.050 TestGCD[35575:4619720] 3

2016-06-28 09:39:16.050 TestGCD[35575:4619720] 3

2016-06-28 09:39:16.059 TestGCD[35575:4619720] 2

也就是說**NSLog(@”2″);**永遠會在for迴圈之後執行。原因:main queue為序列佇列,遵循FIFO原則,同時為非同步執行,非同步block新增到佇列中的**“不等待”**立刻執行for迴圈,在下一次runloop時才會執行block語句塊的內容。

3.7 dispatch_semaphore_t

dispatch semaphore是持有計數的訊號,該計數是多執行緒中的計數型別訊號。所謂訊號,類似於過北京西站地鐵口安檢時常用的手牌。可以通過時舉起手牌,不可通過時放下手牌。而在dispatch semaphore中,使用計數來實現該功能。計數為0時等待,計數為1或者大於1時,減去1而不等待。

主要涉及三個函式 dipatch_semaphore_create()dispatch_semphore_signaldispatch_semaphore_wait.

對於以上三個函式通常都用停車位來解釋,dipatch_semaphore_create()說明了初始車位數,沒呼叫一次dispatch_semphore_signal剩餘車位數就增加一個,每呼叫dispatch_semaphore_wait剩餘車位數減少一個,等車位數為0時,再來車(即呼叫dispatch_semaphore_wait)就只能等車位。

該函式同樣能解決上節中A,B,C的執行依賴問題。

dispatch_semaphore_t semaphore1 = dispatch_semaphore_create(0);

dispatch_semaphore_t semaphore2 = dispatch_semaphore_create(0);

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

NSLog(@”A”);

dispatch_semaphore_signal(semaphore1);

});

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

NSLog(@”B”);

dispatch_semaphore_signal(semaphore2);

});

dispatch_semaphore_wait(semaphore1, DISPATCH_TIME_FOREVER);

dispatch_semaphore_wait(semaphore2, DISPATCH_TIME_FOREVER);

NSLog(@”C”);

// 輸出

2016-06-28 10:01:33.658 TestGCD[35622:4632780] B

2016-06-28 10:01:33.658 TestGCD[35622:4632775] A

2016-06-28 10:01:33.660 TestGCD[35622:4632744] C

3.8 dispatch_apply

dispatch_apply函式是dispatch_sync函式和dispatch group的關聯API。該函式按指定的次數將指定的block追加到指定的dispatch queue中,並等待全部處理執行完成。

dispatch_apply(5, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^(size_t index) {

NSLog(@”%zu”, index);

});

NSLog(@”done”);

// 輸出

2016-06-28 10:10:25.110 TestGCD[35649:4638944] 3

2016-06-28 10:10:25.110 TestGCD[35649:4638889] 0

2016-06-28 10:10:25.110 TestGCD[35649:4638940] 2

2016-06-28 10:10:25.110 TestGCD[35649:4638934] 1

2016-06-28 10:10:25.111 TestGCD[35649:4638889] 4

2016-06-28 10:10:25.111 TestGCD[35649:4638889] done

第一個引數為重複的次數,第二個引數為追加物件的dispatch queue,第三個引數為追加的處理。由於*dispatch_apply*函式也與*dispatch_sync*函式相同,會等待處理執行結束,推薦在*dispatch_async*函式中非同步地執行*dispatch_apply*函式

參考


《Objective-C高階程式設計 iOS與OS X多執行緒》

相關文章