iOS 中的锁(4)
不想篇幅太长,再开一篇继续探究iOS中的锁。
注:本文主要通过Objective-C
语言进行体现,其实跟Swift
也差不多。
本文介绍一种特殊的锁dispatch_semaphore信号量,然后对锁这个篇章的分析做个总结。
1. dispatch_semaphore
关于dispatch_semaphore
的应用在我的这篇文章中已经做了一些介绍。dispatch_semaphore
属于GCD
模块,源码实现自libdispatch
库中,我们可以在Apple Open Source中下载各个版本的libdispatch源码。本文使用的是libdispatch-1173.40.5
。
信号量作为锁是一种特例,当信号量的value
传0和1时可以具有锁的特性。
信号量主要有下列几个函数:
-
dispatch_semaphore_create
创建一个信号量 -
dispatch_semaphore_wait
等待信号量 -
dispatch_semaphore_signal
发送信号量
1.1 创建信号量 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(intptr_t value);
```*当两个线程需要协调时,传递0作为值非常有用
*完成某一特定事件。传递一个大于零的值是
*用于管理有限的资源池,其中池大小相等
*的值。
根据注释我们可以知道:
* 信号量是根据初始值`value`创建一个新的计数信号量
* 当两个线程需要协调完成一个特定的事件时,传0作为值非常有用
* 传递一个大于0的值值对于管理有限的资源池非常有用,其中池大小等于该值。
* 如果创建时传的值小于0则创建失败,对于创建失败的信号量会返回`NULL`
**dispatch_semaphore_create实现代码:**
```C
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) {
return DISPATCH_BAD_INPUT;
}
dsema = _dispatch_object_alloc(DISPATCH_VTABLE(semaphore),
sizeof(struct dispatch_semaphore_s));
dsema->do_next = DISPATCH_OBJECT_LISTLESS;
dsema->do_targetq = _dispatch_get_default_queue(false);
dsema->dsema_value = value;
_dispatch_sema4_init(&dsema->dsema_sema, _DSEMA4_POLICY_FIFO);
dsema->dsema_orig = value;
return dsema;
}
创建代码分析:
- 初始化dsema局部信号量变量
- 判断值是否小于0,如果小于0,则返回
DISPATCH_BAD_INPUT
:#define DISPATCH_BAD_INPUT ((void *_Nonnull)0)
- 为
dsema
开辟内存并赋值 - 返回
dsema
1.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);
根据注释我们可以知道:
- 等待信号量会导致信号量递减,如果结果值小于0,函数不会返回,直到等到一个信号发送(signal)
- 第一个参数:在此参数中传递NULL的结果是未定义的。也就是说第一个参数不能为空
- 第二个参数:超时时间,有
DISPATCH_TIME_NOW
和DISPATCH_TIME_FOREVER
两个常量可以用,也可以自定义。 - 返回值:成功返回0,超时返回非0
dispatch_semaphore_wait
函数实现:
long
dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout)
{
long value = os_atomic_dec2o(dsema, dsema_value, acquire);
if (likely(value >= 0)) {
return 0;
}
return _dispatch_semaphore_wait_slow(dsema, timeout);
}
等待信号量代码分析:
- 首先通过
os_atomic_dec2o
去获取一个value
,os_atomic_dec2o
是一个一连串的宏定义,是对系统底层的一种封装。这里会对信号量-1
- 根据取到的值进行判断,如果
>=0
函数直接返回0
,即成功 - 如果上面没有返回则调用
_dispatch_semaphore_wait_slow
函数进行处理并返回它的返回值
_dispatch_semaphore_wait_slow函数实现
static long
_dispatch_semaphore_wait_slow(dispatch_semaphore_t dsema,
dispatch_time_t timeout)
{
long orig;
_dispatch_sema4_create(&dsema->dsema_sema, _DSEMA4_POLICY_FIFO);
switch (timeout) {
default:
if (!_dispatch_sema4_timedwait(&dsema->dsema_sema, timeout)) {
break;
}
// Fall through and try to undo what the fast path did to
// dsema->dsema_value
case DISPATCH_TIME_NOW:
orig = dsema->dsema_value;
while (orig < 0) {
if (os_atomic_cmpxchgvw2o(dsema, dsema_value, orig, orig + 1,
&orig, relaxed)) {
return _DSEMA4_TIMEOUT();
}
}
// Another thread called semaphore_signal().
// Fall through and drain the wakeup.
case DISPATCH_TIME_FOREVER:
_dispatch_sema4_wait(&dsema->dsema_sema);
break;
}
return 0;
}
- 该函数首先会创建一个锁来保护我们的
dsema->dsema_sema
- 接下来就是一个
switch
分三种情况处理- 这里默认会通过
_dispatch_sema4_timedwait
函数去休眠等待线程被唤醒,如果失败会贯穿到now
分支。 - 对于
now
会循环判断信号量的值是否小于0,如果是,则进一步处理,否则就会跳出,返回0,即成功 - 对于
forever
,也是我们常用的,这里就是直接等待,直到线程被唤醒
- 这里默认会通过
1.3 发送信号量 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);
根据注释我们可以知道:
- 该函数是对信号量计数进行增加
+1
,如果之前的值小于0,则这个函数在返回前会唤醒一个等待的线程 - 参数一:
dispatch_semaphore_t
,不能为空 - 返回值:如果该函数唤醒了一个线程则返回非0,否则返回0
dispatch_semaphore_signal函数实现:
long
dispatch_semaphore_signal(dispatch_semaphore_t dsema)
{
long value = os_atomic_inc2o(dsema, dsema_value, release);
if (likely(value > 0)) {
return 0;
}
if (unlikely(value == LONG_MIN)) {
DISPATCH_CLIENT_CRASH(value,
"Unbalanced call to dispatch_semaphore_signal()");
}
return _dispatch_semaphore_signal_slow(dsema);
}
- 该函数首先释放锁,对信号量计数
+1
- 如果计数大于0,直接返回
- 如果计数
value
等于LONG_MIN
则说明出现了指针异常,直接Crash报错 - 其他情况也就是
<=0
,则调用_dispatch_semaphore_signal_slow
函数发出一个信号唤醒一个等待中的线程。
_dispatch_semaphore_signal_slow源码:
long
_dispatch_semaphore_signal_slow(dispatch_semaphore_t dsema)
{
_dispatch_sema4_create(&dsema->dsema_sema, _DSEMA4_POLICY_FIFO);
_dispatch_sema4_signal(&dsema->dsema_sema, 1);
return 1;
}
1.3 小结
- 信号量是
GCD
的dispatch_semaphore
就是一个计数信号量,通过信号量的数量来管理线程,当计数大于0时线程会去执行任务,当计数小于0就会使线程休眠等待,当等待到一个信号后,会唤醒一个等待的线程去执行任务。 -
GCD
信号量实际是调用了系统底层的信号量,对系统内核信号量的一层封装,感兴趣的可以去研究一下Linux
内核的信号量。 - 信号量的锁主要是在信号量计数为1或0时,通过
dispatch_semaphore_wait
去等待线程执行时产生的锁的效果 - 这里的等待指的是在调用
dispatch_semaphore_wait
的线程中等待,等待一个信号去唤醒这个线程 - 发送信号,是指在在其他线程中调用
dispatch_semaphore_signal
函数增加一个信号量计数,如果计数<=0
则会唤醒一个等待的线程
2. 总结
-
在iOS中锁的这几篇文章中我们一个介绍了
NSLock
、@synchronized
、NSCondition
、NSConditionLock
、NSRecursiveLock
、atomic
、dispatch_semaphore
七种锁。并且在@synchronized
篇章中还介绍了os_unfair_lock(OSSpinLock)
; -
在这七种锁中
NSLock
、NSCondition
、NSConditionLock
是对pthread_mutex
的封装。 -
NSRecursiveLock
是对pthread_mutex(recursive)
的封装 -
@synchronized
维护了一张哈希表,对同一对象加锁时采用lockCount
在递归锁中可以避免死锁的问题 -
dispatch_semaphore
是信号量为0和1时造成线程等待的锁现象 -
atomic
是OC特有的修饰声明属性的关键字,只保证在set
和get
方法内的线程安全,在使用过程中不是绝对的线程安全。 -
其实还有一种锁
NSDistributedLock
使用在Mac开发中,感兴趣的可以到Mac开发中探索一下
最后上一张大神的关于锁的性能图。这里OSSpinLock
是一把自旋锁,在iOS10以后已经被苹果废弃了,用于替代它的是os_unfair_lock
,原因是OSSpinLock
虽然耗时断,但是占用资源多,这是一个已空间换时间的锁。
以上这些锁没有什么好坏之分,只是使用的环境各有不同而已,在开发中我们可以根据业务需求选择合适的锁进行使用。
网友评论