GCD(四) dispatch_semaphore

本文是GCD多线程编程中dispatch_semaphore内容的小结,经过本文,你能够了解到:ios

  • 信号量的基本概念与基本使用
  • 信号量在线程同步与资源加锁方面的应用
  • 信号量释放时的小陷阱

今天我来说解一下dispatch_semaphore在咱们日常开发中的一些基本概念与基本使用,dispatch_semaphore俗称信号量,也称为信号锁,在多线程编程中主要用于控制多线程下访问资源的数量,好比系统有两个资源可使用,但同时有三个线程要访问,因此只能容许两个线程访问,第三个应当等待资源被释放后再访问,这时咱们就可使用dispatch_semaphoregit

dispatch_semaphore相关的共有3个方法,分别是dispatch_semaphore_create,dispatch_semaphore_wait,dispatch_semaphore_signal下面咱们逐一了解一下这三个方法。github

测试代码在这macos

semaphore的三个方法

dispatch_semaphore_create

/*! * @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

  1. 信号量初始值为0时:这种状况主要用于两个线程须要协调特定事件的完成时,即线程同步。
  2. 信号量初始值为大于0时:这种状况主要用于管理有限的资源池,其中池大小等于这个值,即资源加锁。

上面的2种状况(线程同步、资源加锁),咱们在后续的使用篇中会详细讲解。网络

dispatch_semaphore_wait

/*! * @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

  1. 当减一后的值小于0时,这个方法会一直等待,即阻塞当前线程,直到信号量+1或者直到超时。
  2. 当减一后的值大于或等于0时,这个方法会直接返回,不会阻塞当前线程。

上面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个的某一个的资源访问结束,这就是咱们很常见的资源加锁的状况。

dispatch_semaphore_signal

/*! * @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,那么这个方法还会唤醒先前等待的线程。

semaphore使用篇

线程同步

这种状况在咱们的开发中也是挺常见的,当主线程中有一个异步网络任务,咱们须要等这个网络请求成功拿到数据后,才能继续作后面的处理,这时咱们就可使用信号量这种方式来进行线程同步。

咱们首先看看完整测试代码:

- (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个宏,将waitsignal方法包起来,方便下面的使用

#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);
复制代码
  • 信号量初始化的值为1,当一个线程过来执行step_1的代码时,会调用信号量的值减一的方法,这时,信号量的值为0,它会直接返回,而后执行step_2的代码去完成去共享资源的访问,而后再使用step_3中的signal方法让信号量加一,信号量的值又会回归到初始值1。这就是一个线程过来访问的调用流程。
  • 当线程1过来执行到step_2的时候,这时又有一个线程2它也从step_1处来调用这段代码,因为线程1已经调用过step_1的wait方法将信号量的值减一,这时信号量的值为0。同时线程2进入而后调用了step_1的wait方法又将信号量的值减一,这时的信号量的值为-1,因为信号量的值小于0时会阻塞当前线程(线程2),因此,线程2就会一直等待,直到线程1执行完step_3中的方法,将信号量加一,才会唤醒线程2,继续执行下面的代码。这就是为何信号量能够对共享资源加锁的缘由,若是咱们能够容许n个线程同时访问,咱们就须要在初始化这个信号量时把信号量的值设为n,这样就限制了访问共享资源的线程数。

经过上面的分析,咱们能够知道,若是咱们使用信号量来进行线程同步时,咱们须要把信号量的初始值设为0,若是要对资源加锁,限制同时只有n个线程能够访问的时候,咱们就须要把信号量的初始值设为n。

semaphore的释放

在咱们日常的开发过程当中,若是对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);时奔溃了。而后咱们使用lldbbt命令查看一下调用栈。

(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_valuedsema_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_waitdispatch_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代码样例点个✨

相关文章
相关标签/搜索