iOS 多線程系列 -- 基礎(chǔ)概述
iOS 多線程系列 -- pthread
iOS 多線程系列 -- NSThread
iOS 多線程系列 -- GCD全解一(基礎(chǔ))
iOS 多線程系列 -- GCD全解二(常用方法)
iOS 多線程系列 -- GCD全解三(進(jìn)階)
iOS 多線程系列 -- NSOperation
測(cè)試Demo的GitHub地址
4. GCD進(jìn)階:
4.1 Dispatch Source 調(diào)度源
4.1.1 Dispatch Source基礎(chǔ)介紹
簡單來說,dispatch source是一個(gè)監(jiān)視某些類型事件的對(duì)象。當(dāng)這些事件發(fā)生時(shí),它會(huì)自動(dòng)將對(duì)應(yīng)的處理程序塊(block)提交給調(diào)度隊(duì)列以響應(yīng)觸發(fā)事件.
調(diào)度源不可重入凫佛。事件源被掛起或當(dāng)事件處理程序塊正在執(zhí)行時(shí)接收到的新事件會(huì)在源恢復(fù)之后或者正在執(zhí)行的程序塊返回后合并/提交.合并機(jī)制的出現(xiàn),是dispatch source為了防止事件積壓到dispatch queue.如果新事件在上一個(gè)事件處理器出列并執(zhí)行之前到達(dá),dispatch source會(huì)將新舊事件的數(shù)據(jù)合并折砸。根據(jù)事件類型的不同,合并操作可能會(huì)替換舊事件,或者更新舊事件的信息(參考下文Dispatch Source自定義事件打印結(jié)果)
-
Dispatch Source有多種類型,不同類型可以實(shí)現(xiàn)不同功能,具體類型如下:
- Timer
- DISPATCH_SOURCE_TYPE_TIMER : 定時(shí)器
- 自定義的事件,并且也是有自己來觸發(fā)
- DISPATCH_SOURCE_TYPE_DATA_ADD : 合并通過調(diào)用dispatch_source_merge_data()獲得的數(shù)據(jù)
- DISPATCH_SOURCE_TYPE_DATA_OR : 同樣是合并由dispatch_source_merge_data()獲得的數(shù)據(jù),不過合并原則是按位OR運(yùn)算.
- 相關(guān)函數(shù)dispatch_source_merge_data()
- Mach port相關(guān)事件響應(yīng)
- DISPATCH_SOURCE_TYPE_MACH_SEND : 端口發(fā)送
- DISPATCH_SOURCE_TYPE_MACH_RECV : 端口接收
- 文件系統(tǒng)監(jiān)聽
- DISPATCH_SOURCE_TYPE_VNODE : 文件系統(tǒng)有變更
- DISPATCH_SOURCE_TYPE_WRITE : 可寫入文件映像
- DISPATCH_SOURCE_TYPE_READ : 可讀取文件映像
- 其他
- DISPATCH_SOURCE_TYPE_SIGNAL : 接收信號(hào)
- DISPATCH_SOURCE_TYPE_MEMORYPRESSURE : 內(nèi)存壓力
- DISPATCH_SOURCE_TYPE_PROC : 檢測(cè)到與進(jìn)程相關(guān)的事件
- Timer
4.1.2 使用Dispatch Source步驟 ,以自定義source事件為例
-
步驟一: 創(chuàng)建Dispatch Source, 用dispatch_source_create方法
- 參數(shù)一: Dispatch Source的類別,具體類別上面已介紹
- 參數(shù)二/三 : 取決于參數(shù)一的配置信息,不同類別的具體配置,可以看頭文件詳細(xì)介紹.
- 參數(shù)四 : Dispatch Source關(guān)聯(lián)的隊(duì)列
dispatch_source_create(dispatch_source_type_t type, uintptr_t handle, unsigned long mask, dispatch_queue_t _Nullable queue);
- 創(chuàng)建DISPATCH_SOURCE_TYPE_DATA_ADD類型source
dispatch_source_t source = dispatch_source_create(DISPATCH_SOURCE_TYPE_DATA_ADD, 0, 0, dispatch_get_global_queue(0, 0));
-
步驟二: 設(shè)置響應(yīng)事件,每當(dāng)監(jiān)聽到事件后就會(huì)調(diào)用這個(gè)響應(yīng)block
- 使用dispatch_source_set_event_handler 設(shè)置響應(yīng)事件,參數(shù)一是對(duì)應(yīng)的source,參數(shù)二是響應(yīng)的具體代碼塊
- dispatch_source_merge_data,向自定義的事件源傳遞一個(gè)unsigned long類型數(shù)據(jù),這個(gè)數(shù)據(jù)可以使用dispatch_source_get_data獲取到,這樣就實(shí)現(xiàn)了事件觸發(fā)到事件響應(yīng)之間的數(shù)據(jù)傳輸
dispatch_source_set_event_handler(source, ^{
NSLog(@"%lu 人已報(bào)名",dispatch_source_get_data(source));
});
- 步驟三:對(duì)于一個(gè)Dispatch Source還可以設(shè)置取消時(shí)的對(duì)應(yīng)響應(yīng).在這個(gè)source被cancel的時(shí)候調(diào)用.參數(shù)一是對(duì)應(yīng)的source,參數(shù)二是source被cancel時(shí)的響應(yīng)代碼塊
dispatch_source_set_cancel_handler(source, ^{
NSLog(@"報(bào)名已終止");
});
- 步驟三: 恢復(fù)Dispatch Source,新創(chuàng)建的Dispatch Source默認(rèn)掛起,需要恢復(fù)執(zhí)行
- 恢復(fù)source有兩個(gè)方法dispatch_resume 和dispatch_activate,任選其一,參數(shù)就是要恢復(fù)的source. dispatch_activate是iOS10才添加的新方法,按需使用即可.
dispatch_resume(source);
//dispatch_activate(source);
-
步驟四: 取消一個(gè)source
- dispatch_source_cancel,傳入想要取消的source即可. 注意:傳入nill會(huì)閃退,所以調(diào)用之前判斷source是否為nil
if (self.timer) { dispatch_source_cancel(self.timer); // self.timer為nil時(shí)會(huì)閃退 }
- 如果可能被多線程中調(diào)用此方法,還應(yīng)考慮線程安全,加鎖即可.
@synchronized (self.timer) { if (self.timer) { dispatch_source_cancel(self.timer); // timer為null時(shí)會(huì)閃退 } }
示例代碼:
dispatch_source_t source = dispatch_source_create(DISPATCH_SOURCE_TYPE_DATA_ADD, 0, 0,dispatch_get_main_queue());
dispatch_source_set_event_handler(source, ^{
NSLog(@"%lu 人已報(bào)名",dispatch_source_get_data(source));
});
dispatch_resume(source);
dispatch_apply(5, dispatch_get_global_queue(0, 0), ^(size_t index) {
NSLog(@"用戶%zd 報(bào)名郊游",index);
dispatch_source_merge_data(source, 1); // 觸發(fā)事件,傳遞數(shù)據(jù)
});
- 打印結(jié)果
2017-07-01 18:34:30.514 Test - 多線程[66363:2522266] 用戶2 報(bào)名郊游
2017-07-01 18:34:30.513 Test - 多線程[66363:2522111] 用戶0 報(bào)名郊游
2017-07-01 18:34:30.513 Test - 多線程[66363:2522265] 用戶1 報(bào)名郊游
2017-07-01 18:34:30.514 Test - 多線程[66363:2522359] 用戶3 報(bào)名郊游
2017-07-01 18:34:30.514 Test - 多線程[66363:2522266] 用戶4 報(bào)名郊游
2017-07-01 18:34:30.514 Test - 多線程[66363:2522111] 5 人已報(bào)名
-
Dispatch Source進(jìn)階分析
- 細(xì)心的同學(xué)會(huì)發(fā)現(xiàn)上面的打印結(jié)果很有意思,有五個(gè)用戶報(bào)名,結(jié)果統(tǒng)計(jì)人數(shù)打印只有一次,為什么不是五次?難道不應(yīng)該報(bào)名一次統(tǒng)計(jì)一次嘛?這就是為什么使用Dispatch Source的原因所在,分析如下:
- 主線程只不過是GCD的另一個(gè)dispatch queue,我們把大量的響應(yīng)工作push到主線程中,主線程每一次都處理響應(yīng)事件可能會(huì)消耗很多資源,我們不想對(duì)響應(yīng)工作進(jìn)行頻繁而累贅的更新吧恃,理想的情況是當(dāng)主線程繁忙時(shí)將所有的響應(yīng)工作聯(lián)結(jié)起來。這時(shí)用dispatch source就完美了,使用DISPATCH_SOURCE_TYPE_DATA_ADD,我們可以將工作拼接起來言疗,然后主線程可以知道從上一次處理完事件到現(xiàn)在一共發(fā)生了多少改變,然后將這一整段改變一次更新至最新進(jìn)度.所以從打印結(jié)果可以看到,
系統(tǒng)把五次響應(yīng)聯(lián)結(jié)起來,組成了一次任務(wù)輸出
,這在節(jié)省性能方面很有幫助!
4.1.3 使用Dispatch Source自定義定時(shí)器
在上面提到的幾個(gè)步驟的基礎(chǔ)上,還需要使用,dispatch_source_set_timer設(shè)置定時(shí)器屬性,當(dāng)然創(chuàng)建source的類型要選擇DISPATCH_SOURCE_TYPE_TIMER
- dispatch_source_set_timer方法這是定時(shí)器屬性,參數(shù)解析:
- 第一個(gè)參數(shù):dispatch_source_t 創(chuàng)建的定時(shí)器類型source
- 第二個(gè)參數(shù):dispatch_time_t start, 定時(shí)器開始時(shí)間,類型為 dispatch_time_t,dispatch_time_t有兩個(gè)創(chuàng)建方法,分別是:dispatch_time和dispatch_walltime,區(qū)別介紹如下:
- 當(dāng)我們?cè)O(shè)置為dispatch_time 或者 DISPATCH_TIME_NOW 時(shí),系統(tǒng)會(huì)使用默認(rèn)時(shí)鐘來進(jìn)行計(jì)時(shí)颂砸。然而當(dāng)系統(tǒng)休眠的時(shí)候,默認(rèn)時(shí)鐘是不走的,也就會(huì)導(dǎo)致計(jì)時(shí)器停止
- 同為設(shè)置時(shí)間,但是dispatch_w,alltime為“鐘表”時(shí)間,相對(duì)比較準(zhǔn)確,所以選擇使用后者噪奄。dispatch_walltime有兩個(gè)參數(shù)
- 參數(shù)when可以為Null/DISPATCH_TIME_NOW,默認(rèn)為獲取當(dāng)前時(shí)間;
- 參數(shù)delta為增量,
注意delta的單位是納秒
.即獲取當(dāng)前時(shí)間的基礎(chǔ)上,增加delta納秒的時(shí)間為開始計(jì)時(shí)時(shí)間.如果想要延遲1秒的話,那就是1000000000這樣寫太長了,系統(tǒng)理所當(dāng)然的提供了對(duì)應(yīng)的常量宏.關(guān)鍵字解釋:NSEC納秒,PER每,SEC秒,MSEC毫秒,USEC微秒.所以NSEC_PER_SEC的意思是每秒有多少納秒,那么延遲1秒就可以寫成1* NSEC_PER_SEC
- 介紹兩個(gè)常用dispatch_time_t宏,ull表示unsigned long long類型,~表示按位取反. 所以就能理解DISPATCH_TIME_NOW = 0, DISPATCH_TIME_FOREVER至少是一個(gè)不小于2^64 - 1的數(shù)值
- 第三個(gè)參數(shù):uint64_t interval,定時(shí)器間隔時(shí)長,由業(yè)務(wù)需求而定死姚。
- 第四個(gè)參數(shù):uint64_t leeway, 允許誤差,此處傳0即可,需要注意,就算指定 leeway 值為 0,系統(tǒng)也無法保證完全精確的觸發(fā)時(shí)間,只是會(huì)盡可能滿足這個(gè)需求。
dispatch_time_t dispatch_time(dispatch_time_t when, int64_t delta);
dispatch_time_t dispatch_walltime(const struct timespec *_Nullable when, int64_t delta);
#define NSEC_PER_SEC 1000000000ull
#define NSEC_PER_MSEC 1000000ull
#define USEC_PER_SEC 1000000ull
#define NSEC_PER_USEC 1000ull
#define DISPATCH_TIME_NOW (0ull)
#define DISPATCH_TIME_FOREVER (~0ull)
- GCD定時(shí)器示例代碼:
- (void)dispatchTimer
{
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_global_queue(0, 0));
_timer = timer;
//NSEC_PER_SEC宏其定義是1000000000納秒,也就是1秒
dispatch_source_set_timer(timer, dispatch_walltime(DISPATCH_TIME_NOW,1 * NSEC_PER_SEC), 1ull * NSEC_PER_SEC, 0);
dispatch_source_set_event_handler(timer, ^{
NSLog(@"-----handle dispatchTimer thread = %@",[NSThread currentThread]);
});
dispatch_source_set_cancel_handler(timer, ^{
NSLog(@"-----cancel dispatchTimer thread = %@",[NSThread currentThread]);
});
dispatch_resume(timer);//啟動(dòng)定時(shí)器
}
4.1.4 Dispatch Source補(bǔ)充
- dispatch_source_testcancel,查看source是否已經(jīng)取消,返回0表示沒有取消,返回值非0表示已經(jīng)取消
- 參考資料:
- gcd介紹(三)-dispatch-sources
- GCD dispatch source](http://blog.csdn.net/pingshw/article/details/16940009))
4.2 dispatch semaphore
信號(hào)量是一個(gè)整形值并且具有一個(gè)初始計(jì)數(shù)值勤篮,并且支持兩個(gè)操作:信號(hào)通知(dispatch_semaphore_signal)和等待(dispatch_semaphore_wait).
4.2.1 操作dispatch semaphore的三個(gè)方法
- dispatch_semaphore_create 創(chuàng)建一個(gè)semaphore,有一個(gè)long類型的參數(shù)value,表示這個(gè)信號(hào)量的初始值,注意: value的值不能小于0,否則創(chuàng)建失敗返回NULL.
dispatch_semaphore_t dispatch_semaphore_create(long value);
- dispatch_semaphore_wait 讓信號(hào)值減1,等待信號(hào).參數(shù)dsema表示要操作的信號(hào)量, timeout為等待時(shí)間.讓信號(hào)值減1后這個(gè)方法有兩種處理操作:
- 如果信號(hào)值減1后值小于0,阻塞線程直到信號(hào)值不小于0為止,什么時(shí)候線程被喚醒是在dispatch_semaphore_signal方法中處理的
- 如果信號(hào)值減1后值大于等于0,繼續(xù)往下執(zhí)行,不阻塞線程
long dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout);
- dispatch_semaphore_signal 讓信號(hào)值加1,發(fā)送一個(gè)信號(hào),參數(shù)dsema表示信號(hào)值要加1的信號(hào)量.如果信號(hào)量在加1之前小于0, 會(huì)喚醒這個(gè)被阻塞的線程.喚醒成功返回一個(gè)非零的值,否則返回0.
long dispatch_semaphore_signal(dispatch_semaphore_t dsema);
4.2.2 進(jìn)階 - dispatch semaphore的實(shí)用場(chǎng)景
上面已經(jīng)介紹dispatch semaphore的基本使用方法,那利用dispatch semaphore可以實(shí)現(xiàn)那些功能?
-
dispatch semaphore實(shí)現(xiàn)GCD線程并發(fā)控制
- NSOperationQueue中可以利用maxConcurrentOperationCount設(shè)置最大并發(fā)數(shù),GCD中如何實(shí)現(xiàn)最大并發(fā)數(shù)量的控制呢? 我們可以利用dispatch semaphore實(shí)現(xiàn)GCD線程并發(fā)控制,如下代碼: 調(diào)度組中最多有3個(gè)queue任務(wù)在執(zhí)行,只有執(zhí)行完一個(gè)才能添加另一個(gè)到調(diào)度組,進(jìn)而實(shí)現(xiàn)并發(fā)控制
dispatch_async(dispatch_get_global_queue(0, 0), ^{ dispatch_group_t group = dispatch_group_create(); dispatch_semaphore_t semaphore = dispatch_semaphore_create(3); dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); for (int i = 0; i < 100; i++) { dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER); dispatch_group_async(group, queue, ^{ sleep(5); NSLog(@"結(jié)束任務(wù)i = %d , thread = %@",i,[NSThread currentThread]); dispatch_semaphore_signal(semaphore); }); } dispatch_group_wait(group, DISPATCH_TIME_FOREVER); });
-
控制共有資源的多線程訪問數(shù)據(jù)安全
- 如果多個(gè)線程同時(shí)操縱一份共有資源,會(huì)引發(fā)數(shù)據(jù)錯(cuò)誤的問題.使用dispatch_semaphore_create(1) 創(chuàng)建一個(gè)同一時(shí)間只允許一個(gè)線程操縱數(shù)據(jù)的信號(hào)量即可保證數(shù)據(jù)的安全.其實(shí)有點(diǎn)類似于線機(jī)制,一種線程同步技術(shù).
- 示例代碼,保證數(shù)組內(nèi)用戶插入順序如下:
// 保證用戶的插入順序 dispatch_semaphore_t semaphore = dispatch_semaphore_create(1); NSMutableArray *users = [NSMutableArray array]; for (int i = 0; i < 300; i++) { dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^(){ dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER); // 訪問共有資源的任務(wù)代碼 NSLog(@"i = %zd , 添加一個(gè)用戶,thread = %@",i,[NSThread currentThread]); [users addObject:@(i)]; dispatch_semaphore_signal(semaphore); }); }