在iOS多线程开发环境中,我们往往会用信号量Semaphore解决一些特别的问题,它不仅高效而且也易于理解。这里我总结了加锁、异步返回、控制线程并发数这三个用途,下面通过一些例子进行解释。
一、加锁
代码形式:
dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);
for (int i = 0; i < 10000; i++) {
dispatch_async(queue, ^{
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
//临界区,即待加锁的代码区域
dispatch_semaphore_signal(semaphore);
});
}
在进行多线程任务之前,首先创建一个计数为1的信号量,这样可以保证同一时刻只有一个线程在访问临界区,dispatch_semaphore_create()
会为我们完成。
在要访问临界区之前,通过 dispatch_semaphore_wait()
函数,我们可以在信号量为 0 时,让临界区外的线程进入等待状态。
在这里,当第一条线程访问临界区时,信号量计数为初始值1,
dispatch_semaphore_wait()
函数判断到计数大于0,于是将计数减1,从而线程允许访问临界区。其它线程因为信号量等于0,就在临界区外等待。
在第一条线程访问完临界区后,这条线程需要发出一个信号,来表明我已经用完临界区的资源了,下个正在等待的线程可以去访问了。
dispatch_semaphore_signal()
会将信号量计数加1,就好像发出了一个信号一样,下个在临界区前等待的线程会去接收它。接收到了信号的线程判断到信号量计数大于零了,于是访问临界区。
通过重复这个过程,所有线程都会安全地访问一遍临界区。
贴一段YYKit中的简单的加锁代码:
- (instancetype)init {
self = [super init];
_lock = dispatch_semaphore_create(1);
return self;
}
- (NSURL *)imageURL {
dispatch_semaphore_wait(_lock, DISPATCH_TIME_FOREVER);
NSURL *imageURL = _imageURL;
dispatch_semaphore_signal(_lock);
return imageURL;
}
二、异步任务,同步返回
- (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) {
//task赋值,代码有点长,就不贴了
dispatch_semaphore_signal(semaphore);
}];
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
return tasks;
}
上面是 AFNetworking 的一段代码,我且称之为异步返回。这段代码的功能是通过异步的请求取得键路径为 keyPath 的任务数组 tasks,然后返回它。这个方法虽然是异步的,但是执行时间较短。
碰到这种情况,我们肯定最先想到的是用代码块 block 或者代理 delegate 来实现,然后我们就得去声明一个代理,写一个协议方法,或者写一个带有一个参数的代码块,这里AFNetworking巧妙地通过信号量解决了。
我们跟之前的加锁对比,可以发现,信号量在创建时计数是0,
dispatch_semaphore_signal() 函数在 dispatch_semaphore_wait() 函数之前。
AFNetworking 把 dispatch_semaphore_wait() 函数放在返回语句之前,同时信号量计数初始为0,是为了让线程在 tasks 有值之前一直等待。获取 tasks 的异步操作结束之后,这时候 tasks 赋值好了,于是通过 dispatch_semaphore_signal() 函数发出信号,外面的线程就知道不用等待,可以返回 tasks 了。
其实信号量进行了隐式的线程间通信,仔细想想,信号量本身是否线程安全呢?
三、控制线程并发数
在 GCD 中,dispatch_async()
异步操作可以产生新的线程,但是方法本身没办法限制线程的最大并发数,线程的创建和销毁是由 GCD 底层管理的。
了解 NSOperationQueue 的同学肯定知道,通过 maxConcurrentOperationCount 属性可以设置它的最大并发数。那么在GCD中,对应的解决方法就是使用信号量。
dispatch_semaphore_t semaphore = dispatch_semaphore_create(5);
for (int i = 0; i < 1000; ++i) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
//多线程代码
dispatch_semaphore_signal(semaphore);
});
}
其实跟加锁代码非常相似,区别在于,在初始化信号量时,将计数赋值为最大并发数。在应用场景上,限制线程并发数是为了性能考虑,而加锁是为了安全而考虑。