信号量使用篇
操作GCD信号量的函数
在iOS中有三个函数可以操作GCD信号量:
dispatch_semaphore_t dispatch_semaphore_create(long value); 创建一个信号量。
long dispatch_semaphore_signal(dispatch_semaphore_t dsema); 发送一个信号.
long dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout); 等待一个信号。
函数说明
dispatch_semaphore_t semaphoreTask1 = dispatch_semaphore_create(0);
创建一个信号量,并赋予初始值(必须传一个>=0的值)。eg:传入2则表示同时最多两个线程可以访问临界区。long signalRs = dispatch_semaphore_signal(semaphoreTask1);
发出一个信号,将信号量加1。如果之前的信号量小于0,说明有等待dispatch semaphore的计数值增加的线程,那么该函数在返回前将唤醒最先处于等待状态的线程。返回值:This function returns non-zero if a thread is woken. Otherwise, zero is returned。如果有线程被唤醒则返回一个非0值,否则返回0。
long waitRs = dispatch_semaphore_wait(semaphoreTask1, DISPATCH_TIME_FOREVER);
将信号量-1。减去1后如果得到的值<0,则该函数在返回前将一直等待信号的发出,线程会被阻塞。减去1后如果得到的值>=0,则正常返回0,不阻塞线程。timeout:指定等待的时间。如果线程要被阻塞该值表明将阻塞该线程多久。传DISPATCH_TIME_FOREVER意味着该线程永久等待一个信号的发出。 传一个其他值如:dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)),表明将阻塞该线程2s,2s后函数将返回,线程继续执行(不一定就是执行临界区的代码,可以判断如果返回的是非0表明是等待超时了可以做其他处理)。
返回值:Returns zero on success, or non-zero if the timeout occurred。
eg:使用信号量控制多线程添加数组元素。
1 | - (void)testSemaphoreLock { |
打印:
1 | 2020-06-04 22:26:02.043402+0800 multithreadDemo[71599:7993600] 线程:<NSThread: 0x604000468580>{number = 3, name = (null)} 执行 i = 1 |
从打印可以看到dispatch_semaphore_create(1)保证了同一时刻仅有一个线程可以访问临界区。不过上述数组中添加的元素顺序不是依次的。
应用场景
进行线程同步、控制线程的并发数量。
eg:控制网络请求的执行顺序。
Q:想让请求1完成之后,再进行网络请求2,然后进行网络请求N,要求不能阻塞主线程。
1 | - (void)testSemaphoreLock { |
不过上面的情景也可以直接在请求完成block里再发起请求,信号量都不需要了。可以看一个稍微复杂点的:
Q:请求A和请求B同时发起,但需要二者都完成后再发起请求C。要求不能阻塞主线程,仅使用信号量完成。
这个如果使用dispatch_group_notify再配合dispatch_group_enter/leave就很简单,但如果仅使用信号量该怎么实现呢?
只需要在请求C发起前,wait两次阻塞住子线程,请求AB完成后发出信号即可。
1 | - (void)testSemaphoreLock { |
注意事项:
- dispatch_semaphore_signal() 与 dispatch_semaphore_wait() 必须配对使用,否则在信号量销毁时容易导致
EXC_BAD_INSTRUCTION崩溃。
释疑
1. 信号量会减为负数吗?会减为-2,-3…吗?
结论:会,如果很多线程调用wait的话,信号量的值会一直减。但对于wait超时返回的情况,系统会进行+1操作。
以前一直以为信号量减到0就不会再减了,其实不然。
通过po可以查看信号量的值:
1 | (lldb) po self.semaphoreLock |
可以设计一个测试代码:
1 | self.semaphoreLock = dispatch_semaphore_create(1); |
理解了信号的实现原理,上述代码就很容易理解。
2. dispatch_semaphore_signal() 与 dispatch_semaphore_wait() 不配对会导致什么问题?
从底层实现来看
1 | signal时的判断 |
1.不能过度wait,导致计数值变成LONG_MIN,这个一般不会出现。
2.保证signal的次数要>=wait的次数,否则信号量在销毁时会产生崩溃。
为了避免上述崩溃,应该让signal和wait保持配对。这里多signal貌似也没啥问题。
3. 多线程调用dispatch_semaphore_signal/wait为啥不会导致线程安全问题?
因为dispatch_semaphore_signal/wait里面操作信号量计数值时是原子操作的。
1 | long value = os_atomic_inc2o(dsema, dsema_value, release); //signal |
4. 信号量变为-3 后,另一个线程发送信号还能唤醒一个等待的线程吗?
可以。dispatch_semaphore_signal的作用就是将信号量+1,如果加1后信号量还是<=0,说明有wait的线程,那就唤醒最先等待的线程。
5.信号量与互斥锁的区别
- 互斥锁的加锁和解锁必须由同一线程分别对应使用,信号量可以由一个线程释放,另一个线程得到。
- 互斥量值只能为0/1,信号量值可以为非负整数。锁是服务于共享资源的;而semaphore是服务于多个线程间的执行的逻辑顺序的。semaphore的本质就是调度线程。
一个最典型的使用semaphore的场景: a源自一个线程,b源自另一个线程,计算c = a + b也是一个线程。(即一共三个线程)
显然,第三个线程必须等第一、二个线程执行完毕它才能执行。 在这个时候,我们就需要调度线程了:让第一、二个线程执行完毕后,再执行第三个线程。 此时,就需要用semaphore了。
1 | int a, b, c; |
参考:信号量与互斥锁的区别