iOS常用锁的研究

iOS常用锁的研究

背景

iOS并发编程除了常用的多线程技术外,线程间同步的方法也是另外一个重要的点. 公司的项目时间跨度比较长,多线程的实现方式从早期的NSThread到现在GCD和NSOperation+NSOperationQueue的方式都有,加锁方式也是多种多样.之前并不清楚各种锁的使用场景和效率,这里做了简单的研究把总结的一点心得记录下来.

OSSpinLock

OSSpinLock为什么第一个提起呢?因为其效率是最高的.本人是较晚才接触到这个锁的,最早是从facebook的KVOController里面看到的(不过现在已经用pthread_mutex重写,原因下面会提到). 自旋锁不会引起调用者睡眠,如果自旋锁已经被别的执行单元保持,调用者就一直循环在那里看是否该自旋锁的保持者已经释放了锁,”自旋”一词就是因此而得名. 因为少了状态切换,所以效率也会高一些,但是如果执行的代码本身比较耗时且调用的频次也比较频繁的话,那么CPU的压力会陡增且效率也会大打折扣. 所以一般的锁住部分的代码段执行的操作比较轻量级那么OSSpinLock还是非常高效的,比如访问一些可变集合类型的变量. 下面是维基百科关于SpinLock的说明,同样适用于OSSpinLock. 后来偶然读到一篇博客[<不再安全的 OSSpinLock="">](http://blog.ibireme.com/2016/01/16/spinlock_is_unsafe_in_ios/),iOS中使用OSSpinLock已经不再安全了,包括Apple自己也对代码中的OSSpinLock做了替换,Google protobuf的objective-c的源码中对OSSpinLock也用dispatch_semaphore进行了替换,另外一种替换方式是采用pthread_mutex如上面提到的KVOController. 示例代码:

[OSSpinLock lock = OS_SPINLOCK_INIT;
OSSpinLockLock(&lock);
// do something
OSSpinLockUnlock(&lock);

pthread_mutex

互斥锁,这个是C语言形式的加锁方式,最早是在里面接触到的. 互斥锁的使用过程中,主要有pthread_mutex_init, pthread_mutex_destory, pthread_mutex_lock, pthread_mutex_unlock这几个函数以完成锁的初始化,锁的销毁,上锁和释放锁操作. 示例代码:

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_lock(&mutex);
// do something
pthread_mutex_unlock(&mutex);

小结1

Mutex和SpinLock的区别(以下是网上一篇博客的摘录,个人认为解释的比较好): 实现原理上来讲,Mutex属于sleep-waiting类型的锁。例如在一个双核的机器上有两个线程(线程A和线程B),它们分别运行在Core0和 Core1上。假设线程A想要通过pthread_mutex_lock操作去得到一个临界区的锁,而此时这个锁正被线程B所持有,那么线程A就会被阻塞 (blocking),Core0 会在此时进行上下文切换(Context Switch)将线程A置于等待队列中,此时Core0就可以运行其他的任务(例如另一个线程C)而不必进行忙等待。而Spin lock则不然,它属于busy-waiting类型的锁,如果线程A是使用pthread_spin_lock操作去请求锁,那么线程A就会一直在 Core0上进行忙等待并不停的进行锁请求,直到得到这个锁为止。

自旋锁与互斥锁有点类似,只是自旋锁不会引起调用者睡眠,如果自旋锁已经被别的执行单元保持,调用者就一直循环在那里看是 否该自旋锁的保持者已经释放了锁,”自旋”一词就是因此而得名。其作用是为了解决某项资源的互斥使用。因为自旋锁不会引起调用者睡眠,所以自旋锁的效率远 高于互斥锁。虽然它的效率比互斥锁高,但是它也有些不足之处: 1、自旋锁一直占用CPU,他在未获得锁的情况下,一直运行--自旋,所以占用着CPU,如果不能在很短的时 间内获得锁,这无疑会使CPU效率降低。 2、在用自旋锁时有可能造成死锁,当递归调用时有可能造成死锁,调用有些其他函数也可能造成死锁,如 copy_to_user()、copy_from_user()、kmalloc()等。

dispatch_semaphore

信号量是基于计数器的一种线程同步机制. 一般的如果只允许一个线程执行代码块的话,那么可以使用如下方式创建,信号量的计数为1. 示例代码:

dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);

dispatch_semaphore支持两个操作:等待和通知. 等待(dispatch_semaphore_wait): 让信号量减1,如果信号量小于0时则会一直等待,直到接收到另一个信号量通知. 通知(dispatch_semaphore_signal): 让信号量加1, 如果之前的信号量小于0,此方法会唤起正在wait的线程,然后return. 以上是从头文件摘录的两段注释简单翻译过来的,原注释应该描述的更为清楚. 以下是通过wait和signal方式来加锁 示例代码:

dispatch_semaphore_wait(semaphore,DISPATCH_TIME_FOREVER);
// do something
dispatch_semaphore_signal(semaphore);

Objective-C中的锁

Objective-C有一些常用的锁, 他们的接口实际上都是通过NSLocking协议定义的,它定义了lock和unlock方法.你使用这些方法来获取和释放该锁. 常用的锁有:NSLock,NSRecursiveLock,NSCondition,NSConditionLock. 以NSLock为例:

NSLock *lock = [[NSLock alloc] init];
[lock lock];
// do something
[lock unlock];

上面苹果的文档已经有比较详细的描述了.

使用GCD进行线程同步

除了上面提到的dispatch_semaphore以外,dispatch_queue也是线程同步的一种手段. 通常的我们可以使用串行队列来进行线程同步,比较典型的使用就是FMDB中FMDatabaseQueue的实现. 示例代码:

dispatch_queue_t queue = dispatch_queue_create("test", DISPATCH_QUEUE_SERIAL);
dispatch_sync(queue, ^{
  // do something
});

Dispatch Barrier API,也提供线程同步的一种手段. 此类API更适合实现读写锁的机制. 示例代码:

dispatch_queue_t queue = dispatch_queue_create("test", DISPATCH_QUEUE_CONCURRENT);

dispatch_sync(queue, ^{
  // read;
});

dispatch_barrier_async(queue, ^{
  // write;
});

原子操作

iOS平台下的原子操作函数都以OSAtomic开头的,在<libkern/OSAtomic.h>里面.不同线程如果通过原子操作函数对同一变量进行操作,可以保证一个线程的操作不会影响到其他线程内对此变量的操作,因为这些操作都是原子式的.因为原子操作只能对内置类型进行操作,所以原子操作能够同步的线程只能位于同一个进程的地址空间内. 原子操作快速高效,常用的加/减/自增/自减等都有对应的API实现. 主要可以应用到一些计数的同步中.

@synchronized关键字

为什么最后说它,因为这个关键字是最方便使用,也是最经常被误用的一种加锁方式. 因为@synchronized默认添加异常处理机制,所以效率上面会有所牺牲. 这里有篇博客<More than you want to know about @synchronized>对@synchronized做了比较深入的阐述. 通常的如果不需要异常处理机制,不建议使用@synchronized,它的效率确实比较低.

小结2

至此对于iOS中常用的一些线程同步手段就介绍完了. 可能有些人会问我,我会常用哪些锁? 个人比较偏向使用 dispatch_queue 串行队列, 因为它可以简单的把代码包起来, 以免漏写了unlock. 其他的早期偶尔会使用OSSpinLock(后来发现其缺陷后改用dispatch_semaphore). @synchronized会在遗留代码中使用, 为了保持一致. 以下是读到的一些开源代码中一些锁的实现.

facebook AsyncDisplayKit 中 ASThread 的实现; CocoaAsyncSocket 中 GCDAsyncSocket 的实现; AFNetworking 中 AFURLSessionManager 的实现;

附上自己关于上述常用锁执行效率的统计Demo,github仓库中是一个workspace,本文中的相关内容在Demo_Locks工程中.

对1,000,000次自增操作的加锁结果统计,正序排序 2016-09-25 02:48:59.593688 Demo_Locks[4110:655534] lock : NoLock, time : 2502520 ns, 0.002503 s 2016-09-25 02:48:59.593905 Demo_Locks[4110:655534] lock : SpinLock, time : 10154576 ns, 0.010155 s 2016-09-25 02:48:59.593933 Demo_Locks[4110:655534] lock : Atomic, time : 11869955 ns, 0.011870 s 2016-09-25 02:48:59.593958 Demo_Locks[4110:655534] lock : Semaphore, time : 14753828 ns, 0.014754 s 2016-09-25 02:48:59.593981 Demo_Locks[4110:655534] lock : Mutex, time : 25518736 ns, 0.025519 s 2016-09-25 02:48:59.594002 Demo_Locks[4110:655534] lock : GCD, time : 35667374 ns, 0.035667 s 2016-09-25 02:48:59.594024 Demo_Locks[4110:655534] lock : NSCondition, time : 38879077 ns, 0.038879 s 2016-09-25 02:48:59.594046 Demo_Locks[4110:655534] lock : RWLock, time : 47595821 ns, 0.047596 s 2016-09-25 02:48:59.594068 Demo_Locks[4110:655534] lock : NSLock, time : 54020795 ns, 0.054021 s 2016-09-25 02:48:59.594089 Demo_Locks[4110:655534] lock : NSRecursiveLock, time : 61731904 ns, 0.061732 s 2016-09-25 02:48:59.594112 Demo_Locks[4110:655534] lock : Synchronized, time : 109095555 ns, 0.109096 s 2016-09-25 02:48:59.594168 Demo_Locks[4110:655534] lock : NSConditionLock, time : 128421367 ns, 0.128421 s



Previous     Next
Smart Cai /
Published under (CC) BY-NC-SA in categories iOS  tagged with lock