本文是GCD多線程編程中dispatch_semaphore
內容的小結,經過本文,你能夠了解到:ios
今天我來說解一下dispatch_semaphore
在咱們日常開發中的一些基本概念與基本使用,dispatch_semaphore
俗稱信號量,也稱爲信號鎖,在多線程編程中主要用於控制多線程下訪問資源的數量,好比系統有兩個資源可使用,但同時有三個線程要訪問,因此只能容許兩個線程訪問,第三個應當等待資源被釋放後再訪問,這時咱們就可使用dispatch_semaphore
。git
與dispatch_semaphore
相關的共有3個方法,分別是dispatch_semaphore_create
,dispatch_semaphore_wait
,dispatch_semaphore_signal
下面咱們逐一瞭解一下這三個方法。github
測試代碼在這macos
/*! * @function dispatch_semaphore_create * * @abstract * Creates new counting semaphore with an initial value. * * @discussion * Passing zero for the value is useful for when two threads need to reconcile * the completion of a particular event. Passing a value greater than zero is * useful for managing a finite pool of resources, where the pool size is equal * to the value. * * @param value * The starting value for the semaphore. Passing a value less than zero will * cause NULL to be returned. * * @result * The newly created semaphore, or NULL on failure. */
API_AVAILABLE(macos(10.6), ios(4.0))
DISPATCH_EXPORT DISPATCH_MALLOC DISPATCH_RETURNS_RETAINED DISPATCH_WARN_RESULT
DISPATCH_NOTHROW
dispatch_semaphore_t
dispatch_semaphore_create(long value);
複製代碼
dispatch_semaphore_create
方法用於建立一個帶有初始值的信號量dispatch_semaphore_t
。編程
對於這個方法的參數信號量的初始值,這裏有2種狀況:bash
上面的2種狀況(線程同步、資源加鎖),咱們在後續的使用篇中會詳細講解。網絡
/*! * @function dispatch_semaphore_wait * * @abstract * Wait (decrement) for a semaphore. * * @discussion * Decrement the counting semaphore. If the resulting value is less than zero, * this function waits for a signal to occur before returning. * * @param dsema * The semaphore. The result of passing NULL in this parameter is undefined. * * @param timeout * When to timeout (see dispatch_time). As a convenience, there are the * DISPATCH_TIME_NOW and DISPATCH_TIME_FOREVER constants. * * @result * Returns zero on success, or non-zero if the timeout occurred. */
API_AVAILABLE(macos(10.6), ios(4.0))
DISPATCH_EXPORT DISPATCH_NONNULL_ALL DISPATCH_NOTHROW
long
dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout);
複製代碼
dispatch_semaphore_wait
這個方法主要用於等待
或減小
信號量,每次調用這個方法,信號量的值都會減一,而後根據減一後的信號量的值的大小,來決定這個方法的使用狀況,因此這個方法的使用一樣也分爲2種狀況:session
上面2種方式,放到咱們平常的開發中就是下面2種使用狀況:多線程
當咱們只須要同步線程
時,咱們可使用dispatch_semaphore_create(0)
初始化信號量爲0,而後使用dispatch_semaphore_wait
方法讓信號量減一,這時就屬於第一種減一後小於0的狀況,這時就會阻塞當前線程,直到另外一個線程調用dispatch_semaphore_signal
這個讓信號量加1的方法後,當前線程纔會被喚醒,而後執行當前線程中的代碼,這時就起到一個線程同步的做用。併發
當咱們須要對資源加鎖
,控制同時能訪問資源的最大數量(假設爲n)時,咱們就須要使用dispatch_semaphore_create(n)
方法來初始化信號量爲n,而後使用dispatch_semaphore_wait
方法將信號量減一,而後訪問咱們的資源,而後使用dispatch_semaphore_signal
方法將信號量加一。若是有n個線程來訪問這個資源,當這n個資源訪問都尚未結束時,就會阻塞當前線程,第n+1個線程的訪問就必須等待,直到前n個的某一個的資源訪問結束,這就是咱們很常見的資源加鎖的狀況。
/*! * @function dispatch_semaphore_signal * * @abstract * Signal (increment) a semaphore. * * @discussion * Increment the counting semaphore. If the previous value was less than zero, * this function wakes a waiting thread before returning. * * @param dsema The counting semaphore. * The result of passing NULL in this parameter is undefined. * * @result * This function returns non-zero if a thread is woken. Otherwise, zero is * returned. */
API_AVAILABLE(macos(10.6), ios(4.0))
DISPATCH_EXPORT DISPATCH_NONNULL_ALL DISPATCH_NOTHROW
long
dispatch_semaphore_signal(dispatch_semaphore_t dsema);
複製代碼
dispatch_semaphore_signal
方法用於讓信號量的值加一,而後直接返回。若是先前信號量的值小於0,那麼這個方法還會喚醒先前等待的線程。
這種狀況在咱們的開發中也是挺常見的,當主線程中有一個異步網絡任務,咱們須要等這個網絡請求成功拿到數據後,才能繼續作後面的處理,這時咱們就可使用信號量這種方式來進行線程同步。
咱們首先看看完整測試代碼:
- (IBAction)threadSyncTask:(UIButton *)sender {
NSLog(@"threadSyncTask start --- thread:%@",[NSThread currentThread]);
//1.建立一個初始值爲0的信號量
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
//2.定製一個異步任務
//開啓一個異步網絡請求
NSLog(@"開啓一個異步網絡請求");
NSURLSession *session = [NSURLSession sharedSession];
NSURL *url =
[NSURL URLWithString:[@"https://www.baidu.com/" stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]]];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
request.HTTPMethod = @"GET";
NSURLSessionDataTask *dataTask = [session dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
if (error) {
NSLog(@"%@", [error localizedDescription]);
}
if (data) {
NSDictionary *dict = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableContainers error:nil];
NSLog(@"%@", dict);
}
NSLog(@"異步網絡任務完成---%@",[NSThread currentThread]);
//4.調用signal方法,讓信號量+1,而後喚醒先前被阻塞的線程
NSLog(@"調用dispatch_semaphore_signal方法");
dispatch_semaphore_signal(semaphore);
}];
[dataTask resume];
//3.調用wait方法讓信號量-1,這時信號量小於0,這個方法會阻塞當前線程,直到信號量等於0時,喚醒當前線程
NSLog(@"調用dispatch_semaphore_wait方法");
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
NSLog(@"threadSyncTask end --- thread:%@",[NSThread currentThread]);
}
複製代碼
運行以後的log以下:
2019-04-27 17:24:27.050077+0800 GCD(四) dispatch_semaphore[34482:6102243] threadSyncTask end --- thread:<NSThread: 0x6000028aa7c0>{number = 1, name = main}
2019-04-27 17:24:27.050227+0800 GCD(四) dispatch_semaphore[34482:6102243] 開啓一個異步網絡請求
2019-04-27 17:24:27.050571+0800 GCD(四) dispatch_semaphore[34482:6102243] 調用dispatch_semaphore_wait方法
2019-04-27 17:24:27.105069+0800 GCD(四) dispatch_semaphore[34482:6105851] (null)
2019-04-27 17:24:27.105262+0800 GCD(四) dispatch_semaphore[34482:6105851] 異步網絡任務完成---<NSThread: 0x6000028c6ec0>{number = 6, name = (null)}
2019-04-27 17:24:27.105401+0800 GCD(四) dispatch_semaphore[34482:6105851] 調用dispatch_semaphore_signal方法
2019-04-27 17:24:27.105550+0800 GCD(四) dispatch_semaphore[34482:6102243] threadSyncTask end --- thread:<NSThread: 0x6000028aa7c0>{number = 1, name = main}
複製代碼
從log中咱們能夠看出,wait方法會阻塞主線程,直到異步任務完成調用signal方法,纔會繼續回到主線程執行後面的任務。
當一個資源能夠被多個線程讀取修改時,就會很容易出現多線程訪問修改數據出現結果不一致甚至崩潰的問題。爲了處理這個問題,咱們一般使用的辦法,就是使用NSLock
,@synchronized
給這個資源加鎖,讓它在同一時間只容許一個線程訪問資源。其實信號量也能夠當作一個鎖來使用,並且比NSLock
還有@synchronized
代價更低一些,接下來咱們來看看它的基本使用
第一步,定義2個宏,將wait
與signal
方法包起來,方便下面的使用
#ifndef ZED_LOCK
#define ZED_LOCK(lock) dispatch_semaphore_wait(lock, DISPATCH_TIME_FOREVER);
#endif
#ifndef ZED_UNLOCK
#define ZED_UNLOCK(lock) dispatch_semaphore_signal(lock);
#endif
複製代碼
第二步,聲明與建立共享資源與信號鎖
/* 須要加鎖的資源 **/
@property (nonatomic, strong) NSMutableDictionary *dict;
/* 信號鎖 **/
@property (nonatomic, strong) dispatch_semaphore_t lock;
複製代碼
//建立共享資源
self.dict = [NSMutableDictionary dictionary];
//初始化信號量,設置初始值爲1
self.lock = dispatch_semaphore_create(1);
複製代碼
第三步,在即將使用共享資源的地方添加ZED_LOCK
宏,進行信號量減一操做,在共享資源使用完成的時候添加ZED_UNLOCK
,進行信號量加一操做。
- (IBAction)resourceLockTask:(UIButton *)sender {
NSLog(@"resourceLockTask start --- thread:%@",[NSThread currentThread]);
//使用異步執行併發任務會開闢新的線程的特性,來模擬開闢多個線程訪問貢獻資源的場景
for (int i = 0; i < 3; i++) {
NSLog(@"異步添加任務:%d",i);
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
ZED_LOCK(self.lock);
//模擬對共享資源處理的耗時
[NSThread sleepForTimeInterval:1];
NSLog(@"i:%d --- thread:%@ --- 將要處理共享資源",i,[NSThread currentThread]);
[self.dict setObject:@"semaphore" forKey:@"key"];
NSLog(@"i:%d --- thread:%@ --- 共享資源處理完成",i,[NSThread currentThread]);
ZED_UNLOCK(self.lock);
});
}
NSLog(@"resourceLockTask end --- thread:%@",[NSThread currentThread]);
}
複製代碼
在這一步中,咱們使用異步執行併發任務會開闢新的線程的特性,來模擬開闢多個線程訪問貢獻資源的場景,同時使用了線程休眠的API來模擬對共享資源處理的耗時。這裏咱們開闢了3個線程來併發訪問這個共享資源,代碼運行的log以下:
2019-04-27 18:36:25.275060+0800 GCD(四) dispatch_semaphore[35944:6315957] resourceLockTask start --- thread:<NSThread: 0x60000130e940>{number = 1, name = main}
2019-04-27 18:36:25.275312+0800 GCD(四) dispatch_semaphore[35944:6315957] 異步添加任務:0
2019-04-27 18:36:25.275508+0800 GCD(四) dispatch_semaphore[35944:6315957] 異步添加任務:1
2019-04-27 18:36:25.275680+0800 GCD(四) dispatch_semaphore[35944:6315957] 異步添加任務:2
2019-04-27 18:36:25.275891+0800 GCD(四) dispatch_semaphore[35944:6315957] resourceLockTask end --- thread:<NSThread: 0x60000130e940>{number = 1, name = main}
2019-04-27 18:36:26.276757+0800 GCD(四) dispatch_semaphore[35944:6316211] i:0 --- thread:<NSThread: 0x6000013575c0>{number = 3, name = (null)} --- 將要處理共享資源
2019-04-27 18:36:26.277004+0800 GCD(四) dispatch_semaphore[35944:6316211] i:0 --- thread:<NSThread: 0x6000013575c0>{number = 3, name = (null)} --- 共享資源處理完成
2019-04-27 18:36:27.282099+0800 GCD(四) dispatch_semaphore[35944:6316212] i:1 --- thread:<NSThread: 0x600001357800>{number = 4, name = (null)} --- 將要處理共享資源
2019-04-27 18:36:27.282357+0800 GCD(四) dispatch_semaphore[35944:6316212] i:1 --- thread:<NSThread: 0x600001357800>{number = 4, name = (null)} --- 共享資源處理完成
2019-04-27 18:36:28.283769+0800 GCD(四) dispatch_semaphore[35944:6316214] i:2 --- thread:<NSThread: 0x600001369280>{number = 5, name = (null)} --- 將要處理共享資源
2019-04-27 18:36:28.284041+0800 GCD(四) dispatch_semaphore[35944:6316214] i:2 --- thread:<NSThread: 0x600001369280>{number = 5, name = (null)} --- 共享資源處理完成
複製代碼
從屢次log中咱們能夠看出:
添加信號鎖以後,每一個線程對於共享資源的操做都是有序的,並不會出現2個線程同時訪問鎖中的代碼區域。
我把上面的實現代碼簡化一下,方便分析這種鎖的實現原理:
//step_1
ZED_LOCK(self.lock);
//step_2
NSLog(@"執行任務");
//step_3
ZED_UNLOCK(self.lock);
複製代碼
經過上面的分析,咱們能夠知道,若是咱們使用信號量來進行線程同步時,咱們須要把信號量的初始值設爲0,若是要對資源加鎖,限制同時只有n個線程能夠訪問的時候,咱們就須要把信號量的初始值設爲n。
在咱們日常的開發過程當中,若是對semaphore使用不當,就會在它釋放的時候遇到奔潰問題。
首先咱們來看2個例子:
- (IBAction)crashScene1:(UIButton *)sender {
dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
//在使用過程當中將semaphore置爲nil
semaphore = nil;
}
複製代碼
- (IBAction)crashScene2:(UIButton *)sender {
dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
//在使用過程當中對semaphore進行從新賦值
semaphore = dispatch_semaphore_create(3);
}
複製代碼
咱們打開測試代碼,找到semaphore對應的target,而後運行一下代碼,而後點擊後面2個按鈕調用一下上面的代碼,而後咱們能夠發現,代碼在運行到semaphore = nil;
與semaphore = dispatch_semaphore_create(3);
時奔潰了。而後咱們使用lldb
的bt
命令查看一下調用棧。
(lldb) bt
* thread #1, queue = 'com.apple.main-thread', stop reason = EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0)
frame #0: 0x0000000111c31309 libdispatch.dylib`_dispatch_semaphore_dispose + 59
frame #1: 0x0000000111c2fb06 libdispatch.dylib`_dispatch_dispose + 97
* frame #2: 0x000000010efb113b GCD(四) dispatch_semaphore`-[ZEDDispatchSemaphoreViewController crashScene1:](self=0x00007fdcfdf0add0, _cmd="crashScene1:", sender=0x00007fdcfdd0a3d0) at ZEDDispatchSemaphoreViewController.m:117
frame #3: 0x0000000113198ecb UIKitCore`-[UIApplication sendAction:to:from:forEvent:] + 83
frame #4: 0x0000000112bd40bd UIKitCore`-[UIControl sendAction:to:forEvent:] + 67
frame #5: 0x0000000112bd43da UIKitCore`-[UIControl _sendActionsForEvents:withEvent:] + 450
frame #6: 0x0000000112bd331e UIKitCore`-[UIControl touchesEnded:withEvent:] + 583
frame #7: 0x00000001131d40a4 UIKitCore`-[UIWindow _sendTouchesForEvent:] + 2729
frame #8: 0x00000001131d57a0 UIKitCore`-[UIWindow sendEvent:] + 4080
frame #9: 0x00000001131b3394 UIKitCore`-[UIApplication sendEvent:] + 352
frame #10: 0x00000001132885a9 UIKitCore`__dispatchPreprocessedEventFromEventQueue + 3054
frame #11: 0x000000011328b1cb UIKitCore`__handleEventQueueInternal + 5948
frame #12: 0x0000000110297721 CoreFoundation`__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ + 17
frame #13: 0x0000000110296f93 CoreFoundation`__CFRunLoopDoSources0 + 243
frame #14: 0x000000011029163f CoreFoundation`__CFRunLoopRun + 1263
frame #15: 0x0000000110290e11 CoreFoundation`CFRunLoopRunSpecific + 625
frame #16: 0x00000001189281dd GraphicsServices`GSEventRunModal + 62
frame #17: 0x000000011319781d UIKitCore`UIApplicationMain + 140
frame #18: 0x000000010efb06a0 GCD(四) dispatch_semaphore`main(argc=1, argv=0x00007ffee0c4efc8) at main.m:14
frame #19: 0x0000000111ca6575 libdyld.dylib`start + 1
frame #20: 0x0000000111ca6575 libdyld.dylib`start + 1
(lldb)
複製代碼
從上面的調用棧咱們能夠看出,奔潰的地方都處於libdispatch
庫調用dispatch_semaphore_dispose
方法釋放信號量的時候,爲何在信號量使用過程當中對信號量進行從新賦值或置空操做會crash呢,這個咱們就須要從GCD的源碼層面來分析了,GCD的源碼庫libdispatch
在蘋果的開源代碼庫能夠下載,我在本身的Github
也放了一份libdispatch-187.10版本的,下面的源碼分析都是基於這個版本的。
首先咱們來看一下dispatch_semaphore_t
的結構體dispatch_semaphore_s
的結構體定義
struct dispatch_semaphore_s {
DISPATCH_STRUCT_HEADER(dispatch_semaphore_s, dispatch_semaphore_vtable_s);
long dsema_value; //當前的信號值
long dsema_orig; //初始化的信號值
size_t dsema_sent_ksignals;
#if USE_MACH_SEM && USE_POSIX_SEM
#error "Too many supported semaphore types"
#elif USE_MACH_SEM
semaphore_t dsema_port; //當前mach_port_t信號
semaphore_t dsema_waiter_port; //休眠時mach_port_t信號
#elif USE_POSIX_SEM
sem_t dsema_sem;
#else
#error "No supported semaphore type"
#endif
size_t dsema_group_waiters;
struct dispatch_sema_notify_s *dsema_notify_head;//鏈表頭部
struct dispatch_sema_notify_s *dsema_notify_tail;//鏈表尾部
};
複製代碼
這裏咱們須要關注2個值的變化,dsema_value
與dsema_orig
,它們分別表明當前的信號值與初始化時的信號值。
當咱們調用dispatch_semaphore_create
方法建立信號量時,這個方法內部會把傳入的參數存儲到dsema_value
(當前的value)和dsema_orig
(初始value)中,條件是value的值必須大於或等於0。
dispatch_semaphore_t
dispatch_semaphore_create(long value)
{
dispatch_semaphore_t dsema;
// If the internal value is negative, then the absolute of the value is
// equal to the number of waiting threads. Therefore it is bogus to
// initialize the semaphore with a negative value.
if (value < 0) {//初始值不能小於0
return NULL;
}
dsema = calloc(1, sizeof(struct dispatch_semaphore_s));//申請信號量的內存
if (fastpath(dsema)) {//信號量初始化賦值
dsema->do_vtable = &_dispatch_semaphore_vtable;
dsema->do_next = DISPATCH_OBJECT_LISTLESS;
dsema->do_ref_cnt = 1;
dsema->do_xref_cnt = 1;
dsema->do_targetq = dispatch_get_global_queue(
DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dsema->dsema_value = value;//當前的值
dsema->dsema_orig = value;//初始值
#if USE_POSIX_SEM
int ret = sem_init(&dsema->dsema_sem, 0, 0);//內存空間映射
DISPATCH_SEMAPHORE_VERIFY_RET(ret);
#endif
}
return dsema;
}
複製代碼
而後調用dispatch_semaphore_wait
與dispatch_semaphore_signal
時會對dsema_value
作加一或減一操做。當咱們對信號量置空或者從新賦值操做時,會調用dispatch_semaphore_dispose
釋放信號量,咱們來看看對應的源碼
static void
_dispatch_semaphore_dispose(dispatch_semaphore_t dsema)
{
if (dsema->dsema_value < dsema->dsema_orig) {//當前的信號值若是小於初始值就會crash
DISPATCH_CLIENT_CRASH(
"Semaphore/group object deallocated while in use");
}
#if USE_MACH_SEM
kern_return_t kr;
if (dsema->dsema_port) {
kr = semaphore_destroy(mach_task_self(), dsema->dsema_port);
DISPATCH_SEMAPHORE_VERIFY_KR(kr);
}
if (dsema->dsema_waiter_port) {
kr = semaphore_destroy(mach_task_self(), dsema->dsema_waiter_port);
DISPATCH_SEMAPHORE_VERIFY_KR(kr);
}
#elif USE_POSIX_SEM
int ret = sem_destroy(&dsema->dsema_sem);
DISPATCH_SEMAPHORE_VERIFY_RET(ret);
#endif
_dispatch_dispose(dsema);
}
複製代碼
從源碼中咱們能夠看出,當dsema_value
小於dsema_orig
時,即信號量還在使用時,會直接調用DISPATCH_CLIENT_CRASH
讓APP奔潰。
因此,咱們在使用信號量的時候,不能在它還在使用的時候,進行賦值或者置空的操做。
若是文中有錯誤的地方,或者與你的想法相悖的地方,請在評論區告知我,我會繼續改進,若是你以爲這個篇文章總結的還不錯,麻煩動動小手,給個人文章與Git代碼樣例
點個✨