前言
在看AFNetworking3.0源碼時(shí)藤乙,注意到在 AFURLSessionManager.m 里面的 tasksForKeyPath: 方法中使用到dispatch_semaphore
燕差,對(duì)dispatch_semaphore11不甚理解絮爷,經(jīng)查原來是通過引入信號(hào)量
dispatch_semaphore``的方式把NSURLSession的異步方法 getTasksWithCompletionHandler: 變成了同步方法。
- (NSArray *)tasksForKeyPath:(NSString *)keyPath {
__block NSArray *tasks = nil;
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
[self.session getTasksWithCompletionHandler:^(NSArray *dataTasks, NSArray *uploadTasks, NSArray *downloadTasks) {
if ([keyPath isEqualToString:NSStringFromSelector(@selector(dataTasks))]) {
tasks = dataTasks;
} else if ([keyPath isEqualToString:NSStringFromSelector(@selector(uploadTasks))]) {
tasks = uploadTasks;
} else if ([keyPath isEqualToString:NSStringFromSelector(@selector(downloadTasks))]) {
tasks = downloadTasks;
} else if ([keyPath isEqualToString:NSStringFromSelector(@selector(tasks))]) {
tasks = [@[dataTasks, uploadTasks, downloadTasks] valueForKeyPath:@"@unionOfArrays.self"];
}
dispatch_semaphore_signal(semaphore);
}];
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
return tasks;
}
這里是把本來異步的getTasksWithCompletionHandler
方法變成了同步的方式了欢峰,通過引入信號(hào)量的方式葬荷,等待異步方法獲取到tasks,然后再返回纽帖。
1. dispatch_semaphore介紹
- 信號(hào)量是基于計(jì)數(shù)器的一種多線程同步機(jī)制宠漩,用來管理對(duì)資源的并發(fā)訪問。
- 信號(hào)量是基于計(jì)數(shù)器的一種多線程同步機(jī)制懊直,用來管理對(duì)資源的并發(fā)訪問扒吁。
- 信號(hào)量就是一種可用來控制訪問資源的數(shù)量的標(biāo)識(shí),設(shè)定了一個(gè)信號(hào)量室囊,在線程訪問之前瘦陈,加上信號(hào)量的處理,則可告知系統(tǒng)按照我們指定的信號(hào)量數(shù)量來執(zhí)行多個(gè)線程波俄。
- 其實(shí),這有點(diǎn)類似鎖機(jī)制了蛾默,只不過信號(hào)量都是系統(tǒng)幫助我們處理了懦铺,我們只需要在執(zhí)行線程之前,設(shè)定一個(gè)信號(hào)量值支鸡,并且在使用時(shí)冬念,加上信號(hào)量處理方法就行了趁窃。
簡(jiǎn)單來講 信號(hào)量為0則阻塞線程,大于0則不會(huì)阻塞急前。則我們通過改變信號(hào)量的值醒陆,來控制是否阻塞線程,從而達(dá)到線程同步裆针。
1.1 dispatch_semaphore相關(guān)的3個(gè)函數(shù)
- dispatch_semaphore_create
// 創(chuàng)建信號(hào)量刨摩,參數(shù):信號(hào)量的初值,如果小于0則會(huì)返回NULL
dispatch_semaphore_t dispatch_semaphore_create(long value);
- dispatch_semaphore_wait
// 等待降低信號(hào)量世吨,接收一個(gè)信號(hào)和時(shí)間值(多為DISPATCH_TIME_FOREVER)
// 若信號(hào)的信號(hào)量為0澡刹,則會(huì)阻塞當(dāng)前線程,直到信號(hào)量大于0或者經(jīng)過輸入的時(shí)間值耘婚;
// 若信號(hào)量大于0罢浇,則會(huì)使信號(hào)量減1并返回,程序繼續(xù)住下執(zhí)行
long dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout);
- dispatch_semaphore_signal
// 提高信號(hào)量沐祷, 使信號(hào)量加1并返回
long dispatch_semaphore_signal(dispatch_semaphore_t dsema);
在dispatch_semaphore_wait
和dispatch_semaphore_signal
這兩個(gè)函數(shù)中間的執(zhí)行代碼嚷闭,每次只會(huì)允許限定數(shù)量的線程進(jìn)入,這樣就有效的保證了在多線程環(huán)境下赖临,只能有限定數(shù)量的線程進(jìn)入胞锰。
可用于處理在多個(gè)線程訪問共有資源時(shí)候,會(huì)因?yàn)槎嗑€程的特性而引發(fā)數(shù)據(jù)出錯(cuò)的問題思杯。
1.2 線程同步方法
1.2.1 使用NSoperation下可以直接設(shè)置并發(fā)數(shù)
1.2.2 使用GCD讓線程同步方法
- dispatch_group
- dispatch_barrier
- dispatch_semaphore
2.應(yīng)用場(chǎng)景
2.1 保持線程同步胜蛉,將異步操作轉(zhuǎn)換為同步操作
// 保持線程同步
- (void)semaphore {
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
__block int j = 0;
dispatch_async(queue, ^{
j = 100;
dispatch_semaphore_signal(semaphore);
});
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
NSLog(@"finish j = %zd", j);
}
輸出結(jié)果
如果注掉dispatch_semaphore_wait這一行,則 j = 0
- 注釋:block塊異步執(zhí)行添加到了全局并發(fā)隊(duì)列里色乾,所以程序在主線程會(huì)跳過block塊(同時(shí)開辟子線程異步執(zhí)行block塊)誊册,執(zhí)行塊外的代碼
dispatch_semaphore_wait
,因?yàn)閟emaphore信號(hào)量為0暖璧,且時(shí)間為DISPATCH_TIME_FOREVER
案怯,所以會(huì)阻塞當(dāng)前線程(主線程),進(jìn)而只執(zhí)行子線程的block塊澎办,直到執(zhí)行塊內(nèi)部的dispatch_semaphore_signal
使得信號(hào)量+1嘲碱。正在被阻塞的線程(主線程)會(huì)恢復(fù)繼續(xù)執(zhí)行。這樣保證了線程之間的同步局蚀。
2.2 為線程加鎖
// 給線程加鎖
- (void)addLock {
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);
for (int i = 0; i < 10; i++) {
dispatch_async(queue, ^{
// 相當(dāng)于加鎖
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
NSLog(@"i = %zd semaphore = %@", i, semaphore);
// 相當(dāng)于解鎖
dispatch_semaphore_signal(semaphore);
});
}
}
輸出結(jié)果:
- 注釋:當(dāng)線程1執(zhí)行到
dispatch_semaphore_wait
這一行時(shí)麦锯,semaphore
的信號(hào)量為1,所以使信號(hào)量-1變?yōu)?琅绅,并且線程1繼續(xù)往下執(zhí)行扶欣;如果當(dāng)在線程1 NSLog這一行代碼還沒執(zhí)行完的時(shí)候,又有線程2來訪問,執(zhí)行dispatch_semaphore_wait
時(shí)由于此時(shí)信號(hào)量為0料祠,且時(shí)間為DISPATCH_TIME_FOREVER
,所以會(huì)一直阻塞線程2(此時(shí)線程2處于等待狀態(tài))骆捧,直到線程1執(zhí)行完NSLog并執(zhí)行完dispatch_semaphore_signal
使信號(hào)量為1后,線程2才能解除阻塞繼續(xù)住下執(zhí)行髓绽。以上可以保證同時(shí)只有一個(gè)線程執(zhí)行NSLog這一行代碼敛苇。
2.3 獲取通訊錄
- 做通訊錄的時(shí)候需要判斷權(quán)限,才能獲取通訊錄
// 獲取通訊錄
- (void)getAddressBook {
//這個(gè)變量用于記錄授權(quán)是否成功顺呕,即用戶是否允許我們?cè)L問通訊錄
__block int tip=0;
//創(chuàng)建通訊簿的引用
ABAddressBookRef addressBooks=ABAddressBookCreateWithOptions(NULL, NULL);
//創(chuàng)建一個(gè)初始信號(hào)量為0的信號(hào)
dispatch_semaphore_t sema=dispatch_semaphore_create(0);
//申請(qǐng)?jiān)L問權(quán)限
ABAddressBookRequestAccessWithCompletion(addressBooks, ^(bool granted, CFErrorRef error) {
//granted為YES是表示用戶允許枫攀,否則為不允許
if (!granted) {
tip=1;
}
//發(fā)送一次信號(hào)
dispatch_semaphore_signal(sema);
});
//等待信號(hào)觸發(fā)
dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
CFRelease(addressBooks);
}
2.4 使用 Dispatch Semaphore 控制并發(fā)線程數(shù)量
// 控制并發(fā)線程數(shù)量
- (void)dispatchAsyncLimit:(dispatch_queue_t)queue limitSemaphoreCount:(NSUInteger)limitSemaphoreCount bloc:(dispatch_block_t)block {
//控制并發(fā)數(shù)的信號(hào)量
static dispatch_semaphore_t limitSemaphore;
//專門控制并發(fā)等待的線程
static dispatch_queue_t receiverQueue;
//使用 dispatch_once而非 lazy 模式,防止可能的多線程搶占問題
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
limitSemaphore = dispatch_semaphore_create(limitSemaphoreCount);
receiverQueue = dispatch_queue_create("receiver", DISPATCH_QUEUE_SERIAL);
});
// 如不加 receiverQueue 放在主線程會(huì)阻塞主線程
dispatch_async(receiverQueue, ^{
//可用信號(hào)量后才能繼續(xù)塘匣,否則等待
dispatch_semaphore_wait(limitSemaphore, DISPATCH_TIME_FOREVER);
dispatch_async(queue, ^{
!block ? : block();
//在該工作線程執(zhí)行完成后釋放信號(hào)量
dispatch_semaphore_signal(limitSemaphore);
});
});
}
- 注釋:以上栗子有點(diǎn)像-[NSOperationQueue maxConcurrentOperationCount]脓豪。 在能保證靈活性的情況下,通常更好的做法是使用操作隊(duì)列忌卤,而不是通過GCD和信號(hào)量來構(gòu)建自己的解決方案扫夜。
信號(hào)量屬于底層工具。它非常強(qiáng)大驰徊,但在多數(shù)需要使用它的場(chǎng)合笤闯,最好從設(shè)計(jì)角度重新考慮,看是否可以不用棍厂。應(yīng)該優(yōu)先考慮是否可以使用諸如操作隊(duì)列這樣的高級(jí)工具颗味。通常可以通過增加一個(gè)分派隊(duì)列dispatch_suspend牺弹,或者通過其他方式分解操作來避免使用信號(hào)量浦马。信號(hào)量并非不好,只是它本身是鎖张漂,能不用鎖就不要用晶默。盡量用cocoa框架中的高級(jí)抽象,信號(hào)量非常接近底層航攒。但有時(shí)候磺陡,例如需要把異步任務(wù)轉(zhuǎn)換為同步任務(wù)時(shí),信號(hào)量是最合適的工具漠畜。
2.5 同時(shí)下載多張圖片
問題描述:
1.假設(shè)現(xiàn)在系統(tǒng)有兩個(gè)空閑資源可以被利用币他,但同一時(shí)間卻有三個(gè)線程要進(jìn)行訪問,這種情況下憔狞,該如何處理呢蝴悉?
2.我們要下載很多圖片,并發(fā)異步進(jìn)行瘾敢,每個(gè)下載都會(huì)開辟一個(gè)新線程辫封,可是我們又擔(dān)心太多線程肯定cpu吃不消硝枉,那么我們這里也可以用信號(hào)量控制一下最大開辟線程數(shù)。
代碼如下
- (void)dispatchSignal {
// crate的value表示倦微,最多幾個(gè)資源可訪問
dispatch_semaphore_t semaphore = dispatch_semaphore_create(2);
// 隊(duì)列
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
//任務(wù)1
dispatch_async(queue, ^{
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
NSLog(@"run task 1");
sleep(1);
NSLog(@"complete task 1");
dispatch_semaphore_signal(semaphore);
});
//任務(wù)2
dispatch_async(queue, ^{
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
NSLog(@"run task 2");
sleep(1);
NSLog(@"complete task 2");
dispatch_semaphore_signal(semaphore);
});
//任務(wù)3
dispatch_async(queue, ^{
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
NSLog(@"run task 3");
sleep(1);
NSLog(@"complete task 3");
dispatch_semaphore_signal(semaphore);
});
}
輸出結(jié)果:
- 總結(jié):由于設(shè)定的信號(hào)值為2,先執(zhí)行兩個(gè)線程正压,等執(zhí)行完一個(gè)欣福,才會(huì)繼續(xù)執(zhí)行下一個(gè),保證同一時(shí)間執(zhí)行的線程數(shù)不超過2焦履。
假設(shè)我們?cè)O(shè)定信號(hào)值=1
// crate的value表示拓劝,最多幾個(gè)資源可訪問
dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);
輸出結(jié)果:
假設(shè)我們?cè)O(shè)定信號(hào)值=3,就是不限制線程執(zhí)行了,因?yàn)橐还膊胖挥?個(gè)線程嘉裤。
// crate的value表示郑临,最多幾個(gè)資源可訪問
dispatch_semaphore_t semaphore = dispatch_semaphore_create(3);
輸出結(jié)果:
以上只是舉的比較簡(jiǎn)單的例子,在一些特殊場(chǎng)景下屑宠,合理利用信號(hào)量去控制厢洞,能夠方便的解決我們的難題。
本文參考
iOS GCD之dispatch_semaphore(信號(hào)量)
iOS GCD中級(jí)篇 - dispatch_semaphore(信號(hào)量)的理解及使用
非常感謝以上作者
iOS多線程詳細(xì)總結(jié)系列文章
iOS GCD之dispatch_semaphore(信號(hào)量)
iOS 多線程-GCD 詳細(xì)總結(jié)
iOS 多線程: [NSOperation NSOperationQueue] 詳解
iOS 多線程:[pthread,NSThread]詳細(xì)總結(jié)