本篇是三部曲的最後一篇,講解了本書的第三章的內容。在這一章裏,做者主要介紹了GCD技術,它是基於C語言的API,開發者只須要將任務放在block內,並指定好追加的隊列,就能夠完成多線程開發。git
可是多線程開發時容易發生的一些問題:程序員
雖然解決這些問題的代價是會使程序的複雜度上升,可是多線程技術仍然是必須使用的:由於使用多線程編程能夠保證應用程序的響應性能。若是耗時操做阻塞了主線程的RunLoop,會致使用戶界面沒法響應用戶的操做,因此必須開啓子線程將耗時操做放在子線程中處理。那麼咱們應該怎麼進行多線程開發呢?在講解以前先看一下本文結構(GCD部分):github
本文的Demo地址:knightsj/iOS_Demo/gcd_demo 雖然文章裏應給出了詳細的輸出結果,但仍是但願讀者能夠將demo下載後仔細對照一下代碼並體會。數據庫
Dispatch Queue是執行處理的等待隊列,按照任務(block)追加到隊列裏的順序,先進先出執行處理。編程
而等待隊列有兩種數組
將任務追加到串行隊列:安全
- (void)serialQueue
{
dispatch_queue_t queue = dispatch_queue_create("serial queue", NULL);
for (NSInteger index = 0; index < 6; index ++) {
dispatch_async(queue, ^{
NSLog(@"task index %ld in serial queue",index);
});
}
}
複製代碼
輸出:多線程
gcd_demo[33484:2481120] task index 0 in serial queue
gcd_demo[33484:2481120] task index 1 in serial queue
gcd_demo[33484:2481120] task index 2 in serial queue
gcd_demo[33484:2481120] task index 3 in serial queue
gcd_demo[33484:2481120] task index 4 in serial queue
gcd_demo[33484:2481120] task index 5 in serial queue
複製代碼
經過dispatch_queue_create函數能夠建立隊列,第一個函數爲隊列的名稱,第二個參數是
NULL
和DISPATCH_QUEUE_SERIAL
時,返回的隊列就是串行隊列。併發
爲了不重複代碼,我在這裏使用了for循環,將任務追加到了queue中。app
注意,這裏的任務是按照順序執行的。說明任務是以阻塞的形式執行的:必須等待上一個任務執行完成才能執行如今的任務。也就是說:一個Serial Dispatch Queue中同時只能執行一個追加處理(任務block),並且系統對於一個Serial Dispatch Queue只生成並使用一個線程。
可是,若是咱們將6個任務分別追加到6個Serial Dispatch Queue中,那麼系統就會同時處理這6個任務(由於會另開啓6個子線程):
- (void)multiSerialQueue
{
for (NSInteger index = 0; index < 10; index ++) {
//新建一個serial queue
dispatch_queue_t queue = dispatch_queue_create("different serial queue", NULL);
dispatch_async(queue, ^{
NSLog(@"serial queue index : %ld",index);
});
}
}
複製代碼
輸出結果:
gcd_demo[33576:2485282] serial queue index : 1
gcd_demo[33576:2485264] serial queue index : 0
gcd_demo[33576:2485267] serial queue index : 2
gcd_demo[33576:2485265] serial queue index : 3
gcd_demo[33576:2485291] serial queue index : 4
gcd_demo[33576:2485265] serial queue index : 5
複製代碼
從輸出結果能夠看出來,這裏的6個任務並非按順序執行的。
須要注意的是:一旦開發者新建了一個串行隊列,系統必定會開啓一個子線程,因此在使用串行隊列的時候,必定只建立真正須要建立的串行隊列,避免資源浪費。
將任務追加到併發隊列:
- (void)concurrentQueue
{
dispatch_queue_t queue = dispatch_queue_create("concurrent queue", DISPATCH_QUEUE_CONCURRENT);
for (NSInteger index = 0; index < 6; index ++) {
dispatch_async(queue, ^{
NSLog(@"task index %ld in concurrent queue",index);
});
}
}
複製代碼
輸出結果:
gcd_demo[33550:2484160] task index 1 in concurrent queue
gcd_demo[33550:2484159] task index 0 in concurrent queue
gcd_demo[33550:2484162] task index 2 in concurrent queue
gcd_demo[33550:2484182] task index 3 in concurrent queue
gcd_demo[33550:2484183] task index 4 in concurrent queue
gcd_demo[33550:2484160] task index 5 in concurrent queue
複製代碼
能夠看到,dispatch_queue_create函數的第二個參數是
DISPATCH_QUEUE_CONCURRENT
。
注意,這裏追加到併發隊列的6個任務並非按照順序執行的,符合上面併發隊列的定義。
擴展知識:iOS和OSX基於Dispatch Queue中的處理數,CPU核數,以及CPU負荷等當前系統的狀態來決定Concurrent Dispatch Queue中併發處理的任務數。
如今咱們知道dispatch_queue_create方法第一個參數指定了這個新建隊列的名稱,推薦使用逆序quan cheng全程域名(FQDN,fully qualified domain name)。這個名稱能夠在Xcode和CrashLog中顯示出來,對bug的追蹤頗有幫助。
在繼續講解以前作個小總結,如今咱們知道了:
實際上,系統給咱們提供了兩種特殊的隊列,分別對應串行隊列和併發隊列:
主隊列:放在這個隊列裏的任務會追加到主線程的RunLoop中執行。須要刷新UI的時候咱們能夠直接獲取這個隊列,將任務追加到這個隊列中。
全局併發隊列:開發者能夠不須要特地經過dispatch_queue_create方法建立一個Concurrent Dispatch Queue,能夠將任務直接放在這個全局併發隊列裏面。
有一個常見的例子能夠充分體現兩者的使用方法:
//獲取全局併發隊列進行耗時操做
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
//加載圖片
NSData *dataFromURL = [NSData dataWithContentsOfURL:imageURL];
UIImage *imageFromData = [UIImage imageWithData:dataFromURL];
dispatch_async(dispatch_get_main_queue(), ^{
//獲取主隊列,在圖片加載完成後更新UIImageView
UIImageView *imageView = [[UIImageView alloc] initWithImage:imageFromData];
});
});
複製代碼
這個函數有兩個做用:
dispatch_queue_create方法生成的串行隊列合併發隊列的優先級都是與默認優先級的Globle Dispatch Queue一致。
若是想要變動某個隊列的優先級,須要使用dispatch_set_target_queue函數。 舉個🌰:建立一個在後臺執行動做處理的Serial Dispatch Queue
//需求:生成一個後臺的串行隊列
- (void)changePriority
{
dispatch_queue_t queue = dispatch_queue_create("queue", NULL);
dispatch_queue_t bgQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0);
//第一個參數:須要改變優先級的隊列;
//第二個參數:目標隊列
dispatch_set_target_queue(queue, bgQueue);
}
複製代碼
有時,咱們將不能併發執行的處理追加到多個Serial Dispatch Queue中時,可使用dispatch_set_target_queue函數將目標函數定爲某個Serial Dispatch Queue,就能夠防止這些處理的併發執行。
代碼:
NSMutableArray *array = [NSMutableArray array];
for (NSInteger index = 0; index < 5; index ++) {
//5個串行隊列
dispatch_queue_t serial_queue = dispatch_queue_create("serial_queue", NULL);
[array addObject:serial_queue];
}
[array enumerateObjectsUsingBlock:^(dispatch_queue_t queue, NSUInteger idx, BOOL * _Nonnull stop) {
dispatch_async(queue, ^{
NSLog(@"任務%ld",idx);
});
}];
複製代碼
輸出:
gcd_demo[40329:2999714] 任務1
gcd_demo[40329:2999726] 任務0
gcd_demo[40329:2999717] 任務2
gcd_demo[40329:2999715] 任務3
gcd_demo[40329:2999730] 任務4
複製代碼
咱們能夠看到,若是僅僅是將任務追加到5個串行隊列中,那麼這些任務就會併發執行。
那接下來看看使用dispatch_set_target_queue方法之後:
//多個串行隊列,設置了target queue
NSMutableArray *array = [NSMutableArray array];
dispatch_queue_t serial_queue_target = dispatch_queue_create("queue_target", NULL);
for (NSInteger index = 0; index < 5; index ++) {
//分別給每一個隊列設置相同的target queue
dispatch_queue_t serial_queue = dispatch_queue_create("serial_queue", NULL);
dispatch_set_target_queue(serial_queue, serial_queue_target);
[array addObject:serial_queue];
}
[array enumerateObjectsUsingBlock:^(dispatch_queue_t queue, NSUInteger idx, BOOL * _Nonnull stop) {
dispatch_async(queue, ^{
NSLog(@"任務%ld",idx);
});
}];
複製代碼
輸出:
gcd_demo[40408:3004382] 任務0
gcd_demo[40408:3004382] 任務1
gcd_demo[40408:3004382] 任務2
gcd_demo[40408:3004382] 任務3
gcd_demo[40408:3004382] 任務4
複製代碼
很顯然,這些任務就按順序執行了。
dispatch_after解決的問題:某個線程裏,在指定的時間後處理某個任務:
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"三秒以後追加到隊列");
});
複製代碼
注意:不是在3秒以後處理任務,準確來講是3秒以後追加到隊列。因此說,若是這個線程的runloop執行1/60秒一次,那麼這個block最快會在3秒後執行,最慢會在(3+1/60)秒後執行。並且,若是這個隊列自己還有延遲,那麼這個block的延遲執行時間會更多。
若是遇到這樣到需求:所有處理完多個預處理任務(block_1 ~ 4)後執行某個任務(block_finish),咱們有兩個方法:
分別詳細講解一下兩種需求的實現方式:
這個需求的實現方式相對簡單一點,只要將全部的任務(block_1 ~ 4 + block_finish)放在一個串行隊列中便可,由於都是按照順序執行的,只要不作多餘的事情,這些任務就會乖乖地按順序執行。
dispatch_group_t group = dispatch_group_create();
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
for (NSInteger index = 0; index < 5; index ++) {
dispatch_group_async(group, queue, ^{
NSLog(@"任務%ld",index);
});
}
dispatch_group_notify(group, queue, ^{
NSLog(@"最後的任務");
});
複製代碼
輸出:
gcd_demo[40905:3057237] 任務0
gcd_demo[40905:3057235] 任務1
gcd_demo[40905:3057234] 任務2
gcd_demo[40905:3057253] 任務3
gcd_demo[40905:3057237] 任務4
gcd_demo[40905:3057237] 最後的任務
複製代碼
由於這些預處理任務都是追加到global dispatch queue中的,因此這些任務的執行任務的順序是不定的。可是最後的任務必定是最後輸出的。
dispatch_group_notify函數監聽傳入的group中任務的完成,等這些任務所有執行之後,再將第三個參數(block)追加到第二個參數的queue(相同的queue)中。
dispatch_group_wait 也是配合dispatch_group 使用的,利用這個函數,咱們能夠設定group內部全部任務執行完成的超時時間。
一共有兩種狀況:超時的狀況和沒有超時的狀況:
- (void)dispatch_wait_1
{
dispatch_group_t group = dispatch_group_create();
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
for (NSInteger index = 0; index < 5; index ++) {
dispatch_group_async(group, queue, ^{
for (NSInteger i = 0; i< 1000000000; i ++) {
}
NSLog(@"任務%ld",index);
});
}
dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, 1ull * NSEC_PER_SEC);
long result = dispatch_group_wait(group, time);
if (result == 0) {
NSLog(@"group內部的任務所有結束");
}else{
NSLog(@"雖然過了超時時間,group還有任務沒有完成");
}
}
複製代碼
輸出:
gcd_demo[41277:3087481] 雖然過了超時時間,group還有任務沒有完成,結果是斷定爲超時
gcd_demo[41277:3087563] 任務0
gcd_demo[41277:3087564] 任務2
gcd_demo[41277:3087579] 任務3
gcd_demo[41277:3087566] 任務1
gcd_demo[41277:3087563] 任務4
複製代碼
- (void)dispatch_wait_2
{
dispatch_group_t group = dispatch_group_create();
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
for (NSInteger index = 0; index < 5; index ++) {
dispatch_group_async(group, queue, ^{
for (NSInteger i = 0; i< 100000000; i ++) {
}
NSLog(@"任務%ld",index);
});
}
dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, 1ull * NSEC_PER_SEC);
long result = dispatch_group_wait(group, time);
if (result == 0) {
NSLog(@"group內部的任務所有結束");
}else{
NSLog(@"雖然過了超時時間,group還有任務沒有完成");
}
}
複製代碼
輸出:
gcd_demo[41357:3092079] 任務2
gcd_demo[41357:3092076] 任務3
gcd_demo[41357:3092092] 任務1
gcd_demo[41357:3092077] 任務0
gcd_demo[41357:3092079] 任務4
gcd_demo[41357:3091956] group內部的任務所有結束,在超時的時間之內完成,結果斷定爲沒有超時
複製代碼
注意: 一旦調用dispatch_group_wait之後,當通過了函數中指定的超時時間後 或者 指定的group內的任務所有執行後會返回這個函數的結果:
也就是說: 若是指定的超時時間爲DISPATCH_TIME_NOW,那麼則沒有等待,當即判斷group內的任務是否完成。
能夠看出,指定的超時時間爲DISPATCH_TIME_NOW的時候至關於dispatch_group_notify函數的使用:判斷group內的任務是否都完成。
然而dispatch_group_notify函數是做者推薦的,由於經過這個函數能夠直接設置最後任務所被追加的隊列,使用起來相對比較方便。
關於解決數據競爭的方法:讀取處理是能夠併發的,可是寫入處理倒是不容許併發執行的。
因此合理的方案是這樣的:
咱們看看如何使用dispatch_barrier_async來解決這個問題。
爲了幫助你們理解,我構思了一個例子:
這個需求有三個關鍵點:
用代碼看一下:
- (void)dispatch_barrier
{
dispatch_queue_t meetingQueue = dispatch_queue_create("com.meeting.queue", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(meetingQueue, ^{
NSLog(@"總裁查看合同");
});
dispatch_async(meetingQueue, ^{
NSLog(@"董事1查看合同");
});
dispatch_async(meetingQueue, ^{
NSLog(@"董事2查看合同");
});
dispatch_async(meetingQueue, ^{
NSLog(@"董事3查看合同");
});
dispatch_barrier_async(meetingQueue, ^{
NSLog(@"總裁簽字");
});
dispatch_async(meetingQueue, ^{
NSLog(@"總裁審覈合同");
});
dispatch_async(meetingQueue, ^{
NSLog(@"董事1審覈合同");
});
dispatch_async(meetingQueue, ^{
NSLog(@"董事2審覈合同");
});
dispatch_async(meetingQueue, ^{
NSLog(@"董事3審覈合同");
});
}
複製代碼
輸出結果:
gcd_demo[41791:3140315] 總裁查看合同
gcd_demo[41791:3140296] 董事1查看合同
gcd_demo[41791:3140297] 董事3查看合同
gcd_demo[41791:3140299] 董事2查看合同
gcd_demo[41791:3140299] 總裁簽字
gcd_demo[41791:3140299] 總裁審覈合同
gcd_demo[41791:3140297] 董事1審覈合同
gcd_demo[41791:3140296] 董事2審覈合同
gcd_demo[41791:3140320] 董事3審覈合同
複製代碼
在這裏,咱們能夠將meetingQueue當作是會議的時間線。總裁簽字這個行爲至關於寫操做,其餘都至關於讀操做。使用dispatch_barrier_async之後,以前的全部併發任務都會被dispatch_barrier_async裏的任務攔截掉,就像函數名稱裏的「柵欄」同樣。
所以,使用Concurrent Dispatch Queue 和 dispatch_barrier_async 函數能夠實現高效率的數據庫訪問和文件訪問。
到目前爲止的全部例子都使用的是異步函數,有異步就必定會有同步,那麼如今就來區分一下同步和異步函數的區別:
舉個例子:
- (void)dispatch_sync_1
{
//同步處理
NSLog(@"%@",[NSThread currentThread]);
NSLog(@"同步處理開始");
__block NSInteger num = 0;
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_sync(queue, ^{
//模仿耗時操做
for (NSInteger i = 0; i< 1000000000; i ++) {
num++;
}
NSLog(@"%@",[NSThread currentThread]);
NSLog(@"同步處理完畢");
});
NSLog(@"%ld",num);
NSLog(@"%@",[NSThread currentThread]);
}
複製代碼
輸出結果:
gcd_demo[5604:188687] <NSThread: 0x60800006fa40>{number = 1, name = main}
gcd_demo[5604:188687] 同步處理開始
gcd_demo[5604:188687] <NSThread: 0x60800006fa40>{number = 1, name = main}
gcd_demo[5604:188687] 同步處理完畢
gcd_demo[5604:188687] 1000000000
gcd_demo[5604:188687] <NSThread: 0x60800006fa40>{number = 1, name = main}
複製代碼
在最開始的時候只打印前兩行,循環完畢以後纔打印後面的內容。 由於是同步函數,它阻塞了當前線程(主線程),因此只能等到block內部的任務都結束後,才能打印下面的兩行。
可是若是使用異步函數會怎樣呢?
- (void)dispatch_sync_2
{
//異步處理
NSLog(@"%@",[NSThread currentThread]);
NSLog(@"異步處理開始");
__block NSInteger num = 0;
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(queue, ^{
//模仿耗時操做
for (NSInteger i = 0; i< 1000000000; i ++) {
num++;
}
NSLog(@"%@",[NSThread currentThread]);
NSLog(@"異步處理完畢");
});
NSLog(@"%ld",num);
NSLog(@"%@",[NSThread currentThread]);
}
複製代碼
輸出:
gcd_demo[5685:194233] <NSThread: 0x600000071f00>{number = 1, name = main}
gcd_demo[5685:194233] 異步處理開始
gcd_demo[5685:194233] 0
gcd_demo[5685:194233] <NSThread: 0x600000071f00>{number = 1, name = main}
gcd_demo[5685:194280] <NSThread: 0x608000260400>{number = 3, name = (null)}
gcd_demo[5685:194280] 異步處理完畢
複製代碼
咱們能夠看到,不一樣於上面的狀況,block下面的兩個輸出是先打印的(由於沒有通過for循環的計算,num的值是0)。由於是異步處理,因此沒有等待block中任務的完成就當即返回了。
瞭解了同步異步的區別以後,咱們看一下使用同步函數容易發生的問題:若是給同步函數傳入的隊列是串行隊列的時候就會容易形成死鎖。看一下一個死鎖的例子:
- (void)dispatch_sync_3
{
NSLog(@"任務1");
dispatch_queue_t queue = dispatch_get_main_queue();
dispatch_sync(queue, ^{
NSLog(@"任務2");
});
NSLog(@"任務3");
}
複製代碼
上面的代碼只能輸出任務1,並造成死鎖。 由於任務2被追加到了主隊列的最後,因此它須要等待任務3執行完成。 但又由於是同步函數,任務3也在等待任務2執行完成。 兩者互相等待,因此造成了死鎖。
經過dispatch_apply函數,咱們能夠按照指定的次數將block追加到指定的隊列中。並等待所有處理執行結束。
- (void)dispatch_apply_1
{
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_apply(10, queue, ^(size_t index) {
NSLog(@"%ld",index);
});
NSLog(@"完畢");
}
複製代碼
gcd_demo[6128:240332] 1
gcd_demo[6128:240331] 0
gcd_demo[6128:240334] 2
gcd_demo[6128:240332] 4
gcd_demo[6128:240334] 6
gcd_demo[6128:240331] 5
gcd_demo[6128:240332] 7
gcd_demo[6128:240334] 8
gcd_demo[6128:240331] 9
gcd_demo[6128:240259] 3
gcd_demo[6128:240259] 完畢
複製代碼
咱們也能夠用這個函數來遍歷數組,取得下標進行操做:
- (void)dispatch_apply_2
{
NSArray *array = @[@1,@10,@43,@13,@33];
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_apply([array count], queue, ^(size_t index) {
NSLog(@"%@",array[index]);
});
NSLog(@"完畢");
}
複製代碼
輸出:
gcd_demo[6180:244316] 10
gcd_demo[6180:244313] 1
gcd_demo[6180:244316] 33
gcd_demo[6180:244314] 43
gcd_demo[6180:244261] 13
gcd_demo[6180:244261] 完畢
複製代碼
咱們能夠看到dispatch_apply函數與dispatch_sync函數一樣具備阻塞的做用(dispatch_apply函數返回後纔打印完畢)。
咱們也能夠在dispatch_async函數裏執行dispatch_apply函數:
- (void)dispatch_apply_3
{
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(queue, ^{
NSArray *array = @[@1,@10,@43,@13,@33];
__block NSInteger sum = 0;
dispatch_apply([array count], queue, ^(size_t index) {
NSNumber *number = array[index];
NSInteger num = [number integerValue];
sum += num;
});
dispatch_async(dispatch_get_main_queue(), ^{
//回到主線程,拿到總和
NSLog(@"完畢");
NSLog(@"%ld",sum);
});
});
}
複製代碼
掛起函數調用後對已經執行的處理沒有影響,可是追加到隊列中可是還沒有執行的處理會在此以後中止執行。
dispatch_suspend(queue);
dispatch_resume(queue);
複製代碼
經過dispatch_once處理的代碼只執行一次,並且是線程安全的:
- (void)dispatch_once_1
{
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
for (NSInteger index = 0; index < 5; index++) {
dispatch_async(queue, ^{
[self onceCode];
});
}
}
- (void)onceCode
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSLog(@"只執行一次的代碼");
});
}
複製代碼
輸出:
gcd_demo[7556:361196] 只執行一次的代碼
複製代碼
該函數主要用於單例模式的使用。
到這裏終於總結完啦,這本書加深了我對iOS內存管理,block以及GCD的理解,但願我寫的這三篇能對您有所幫助~
本文已經同步到我的博客:傳送門
---------------------------- 2018年7月17日更新 ----------------------------
注意注意!!!
筆者在近期開通了我的公衆號,主要分享編程,讀書筆記,思考類的文章。
由於公衆號天天發佈的消息數有限制,因此到目前爲止尚未將全部過去的精選文章都發布在公衆號上,後續會逐步發佈的。
並且由於各大博客平臺的各類限制,後面還會在公衆號上發佈一些短小精幹,以小見大的乾貨文章哦~
掃下方的公衆號二維碼並點擊關注,期待與您的共同成長~