我的上一家公司有个引以为豪的技术:多核无锁,不仅避免了各种由锁带来的问题,还极大的提高了性能,所以产品性能能够在业界数一数二。 在这样的氛围影响下,我在开发的时候也很少用锁,能不用就不用。 后来去面试 iOS 开发的时候,面试官总是喜欢问有关于锁的问题,最近趁有时间就整理了一下,算是补充一下技能树吧。
1. 互斥锁(Mutex)
互斥锁是比较常用的一种锁,当一个线程试图获取被另一个线程占用的锁时,它将会被挂起,让出 CPU,直到该锁被释放。
在 iOS 中,互斥锁有多种实现方式:
1.1 POSIX Api
POSIX 方式的优点是比较通用,对那些需要跨平台的 library 来说再合适不过了。
POSIX 中与互斥锁有关的主要有 5 个函数:
pthread_mutex_init
初始化锁pthread_mutex_lock
加锁pthread_mutex_tylock
加锁,当锁被占用时,返回 busy,不挂起线程。pthread_mutex_unlock
释放锁pthread_mutex_destroy
销毁锁
例子:
#include <pthread.h>
pthread_mutex_t mutex;
pthread_mutex_init(&mutex, NULL);
void mutiThreadMethod()
{
pthread_mutex_lock(&mutex);
// Do something
pthread_mutex_unlock(&mutex);
}
void destroyLock()
{
pthread_mutex_destroy(&mutex);
}
1.2 @synchronized
@synchronized 应该是用起来最简单的方式了,例如:
- (void)mutiThreadMethod2
{
@synchronized(self) {
// Do something
}
}
用 clang 改写一下就可以发现,其实编译器为这个语法糖做了很多工作,大致如下:
//...
objc_sync_enter
objc_exception_try_enter
setjmp
objc_exception_extract
// Do something
objc_exception_try_exit
objc_sync_exit
// ...
objc_exception_throw
// ...
可以看到做了很多与锁有关的操作,其性能不如 POSIX 方式,尽管后者难看些。
1.3 NSLock
NSLock *lock = [[NSLock alloc] init];
// ...
- (void)mutiThreadMethod3
{
if ([lock tryLock]) {
// Do something
// ...
[lock unlock];
}
}
2. 递归锁(Recursive Lock)
递归锁是互斥锁的变体,它允许一个线程在释放它之前多次获取它,并且只有在释放相同次数之后其它线程才能获取它。
NSRecursiveLock *theLock = [[NSRecursiveLock alloc] init];
void MyRecursiveFunction(int value)
{
[theLock lock];
if (value != 0)
{
--value;
MyRecursiveFunction(value);
}
[theLock unlock];
}
MyRecursiveFunction(5);
3. 读写锁(Read-write Lock)
读写锁把访问对象划分为读者和写者,当读写锁在读加锁状态时,所有的试图以读加锁方式对其进行加锁时,都会获得访问权限。 所有的试图以写加锁方式对其加锁的线程都将阻塞,直到所有的读锁释放。 当在写加锁状态时,所有试图对其加锁的线程都将阻塞。
读写锁适合读操作远大于写操作的情况。
在 iOS 上,读写锁得用 POSIX 方式实现。POSIX 提供的相关函数如下:
pthread_rwlock_init
初始化读写锁pthread_rwlock_rdlock
读加锁pthread_rwlock_wrlock
写加锁pthread_rwlock_unlock
释放锁pthread_rwlock_destroy
销毁锁
例子:
#include <pthread.h>
pthread_rwlock_t rwlock;
pthread_rwlock_init(&rwlock, NULL);
void mutiThreadWritting()
{
pthread_rwlock_wrlock(&rwlock);
// write
pthread_rwlock_unlock(&rwlock);
}
void mutiThreadReadding()
{
pthread_rwlock_rdlock(&rwlock);
// read
pthread_rwlock_unlock(&rwlock);
}
4. 自旋锁(Spin Lock)
自旋锁与互斥锁不同的地方在于,自旋锁是非阻塞的,当一个线程无法获取自旋锁时,会自旋,直到该锁被释放,等待的过程中线程并不会挂起。
它的优点是效率高,不用进行线程切换。缺点是如果一个线程霸占锁的时间过长,自旋会消耗 CPU 资源。
#import <libkern/OSAtomic.h>
static OSSpinLock lock = OS_SPINLOCK_INIT;
void mutiThreadMethod4
{
OSSpinLockLock(&lock);
// Do something
OSSpinLockUnlock(&lock);
}
5. 分布锁(Distributed Lock)
严格来说,分布锁是进程间同步的工具,有点像 Unix 下的各种 lock 文件,比如 apt-get 的 “/var/lib/apt/lists/lock”。
它并不强制进程休眠,只是起到告知的作用。具体如何处理资源被占,完全由进程自己决定。
iOS 上几本用不上分布锁,在 OS X 中,可以用 NSDistributedLock 实现:
NSDistributedLock *lock = [NSDistributedLock lockWithPath:path];
// ...
if ([lock tryLock]) {
// Do something
[lock unlock];
}
或者,可以直接通过写 lock 文件的方式来实现。
6. 条件变量(Condition Variable)
如果一个线程需要等待某一条件才能继续执行,而这个条件是由别的线程产生的,这时候只用锁就有点捉襟见肘了。要么不停的轮询,消耗资源,要么每隔一段时间查询一次,丧失了及时性。 条件变量就是为了满足这种场景而生的,它可以让一个线程等待某一条件,当条件满足时,会收到通知。 在获取条件变量并等待条件发生的过程中,也会产生多线程的竞争,所以条件变量通常会和互斥锁一起工作。
iOS 中,条件变量有两种实现方式:
6.1 POSIX
POSIX 提供的相关函数如下:
pthread_cond_init
初始化pthread_cond_wait
等待条件pthread_cond_broadcast
发送广播,唤醒所有正在等待的线程pthread_cond_signal
发送信号,唤醒第一个线程pthread_cond_destroy
销毁
例子:
#include <pthread.h>
static pthread_mutex_t mutex;
static pthread_cond_t condition;
// ...
pthread_mutex_init(&mutex, NULL);
pthread_cond_init(&condition, NULL);
// ...
void waitCondition()
{
pthread_mutex_lock(&mutex);
while (value == 0) {
pthread_cond_wait(&condition, &mutex);
}
pthread_mutex_unlock(&mutex);
}
void triggerCondition()
{
pthread_mutex_lock(&mutex);
value = 1;
pthread_mutex_unlock(&mutex);
pthread_cond_broadcast(&condition);
}
// ...
pthread_mutex_destroy(&mutex);
pthread_cond_destroy(&condition);
6.2 NSCondition
例子摘自 Threading Programming Guide
[cocoaCondition lock];
while (timeToDoWork <= 0)
[cocoaCondition wait];
timeToDoWork--;
// Do real work here.
[cocoaCondition unlock];
发送信号:
[cocoaCondition lock];
timeToDoWork++;
[cocoaCondition signal];
[cocoaCondition unlock];
7. NSConditionLock
NSConditionLock 跟 NSCondition 类似,但是实现机制是不一样的,所以单独列了出来。
例子:
生产者
id condLock = [[NSConditionLock alloc] initWithCondition:NO_DATA];
while(true)
{
[condLock lock];
// Add data to the queue.
[condLock unlockWithCondition:HAS_DATA];
}
消费者
while (true)
{
[condLock lockWhenCondition:HAS_DATA];
// Remove data from the queue.
[condLock unlockWithCondition:(isEmpty ? NO_DATA : HAS_DATA)];
// Process the data locally.
}
8. 信号量(Semaphore)
信号量可以看成是一种特殊的互斥锁,不同的是,它可以不只有两个状态,它可以是资源的计数器。还记得《操作系统》中学过的 PV 操作么?
iOS 中,信号量有两种实现方式:
8.1 POSIX
POSIX 提供的相关函数如下:
sem_init
初始化sem_post
给信号量的值加一(V 操作)sem_wait
给信号量的值减一(P 操作)sem_getvalue
返回信号量的值sem_destroy
销毁
8.2 GCD 信号量
GCD 提供的函数如下:
dispatch_semaphore_create
创建信号量dispatch_semaphore_signal
发送信号(信号量加一,V 操作)dispatch_semaphore_wait
等待信号(信号量减一,P 操作)
例子:
dispatch_semaphore_t semaphore = dispatch_semaphore_create(10);
// ...
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
// Do something
dispatch_semaphore_signal(semaphore);
9. 栅栏/屏障(Barrier)
如果一个线程需要等待另一个线程的某些操作之后才能继续执行,可以用上面所说的条件变量来实现,还有一种优雅的实现方式 —— Barrier。 形象点说,就是把线程挡在同一个 Barrier 之前,所有的线程都达到 Barrier 之后,统一放行。
同样,iOS 中有两种实现方式:
9.1 POSIX
相关函数如下:
- pthread_barrier_init
创建 barrier
- pthread_barrier_wait
告知当前线程已经到达 barrier,等所有线程都告知后,会继续往下执行
- pthread_barrier_destroy
销毁
9.2 Dispatch Barrier
Dispatch Barrier 的概念跟 POSIX 类似,不同的是它是针对于 GCD 异步任务的。它可以让在它之前提交的异步任务都执行完成之后再执行。
例子:
dispatch_async(async_queue, block1);
dispatch_async(async_queue, block2);
// block3 会在 block1 和 block2 执行完成之后再执行
dispatch_barrier_async(async_queue, block3);
// block4 和 block5 会在 block3 之后执行
dispatch_async(async_queue, block4);
dispatch_async(async_queue, block5);
后记
锁 这个东西可谓 “小用怡情,滥用伤身”,用的时候一不小心就会有各种各样的问题,比如死锁,我曾经就这样写过:
void func()
{
LOCK;
//...
if (someCondition) {
return;
}
UNLOCK;
}
在 iOS 中,很多时候都可以用 GCD 的串行队列来避免使用锁:
dispatch_async(serialQueue, block);
因为串行队列中的任务一次只能执行一个,所以就不存在资源的竞争,还能有效的避免死锁问题。
-----更新-----
最近发现很多同事,以及各种博客都在测试各种加锁方式的性能,比如连续加解锁几千次取总时间等。想通过这种比较来选取一种所谓高效率的锁。
还有,面试的时候,有些自认为懂得多的面试官总是想让你说一下常用的线程同步方式。要是回答 @synchronized,就会各种受鄙视。
🤷♀️🤷♀️🤷♀️🤷♀️🤷♀️🤷♀️🤷♀️🤷♀️🤷♀️🤷♀️🤷♀️🤷♀️
iOS,或者各平台的客户端,都不是一个高并发的环境,用锁的时候通常是为了解决两个线程偶尔发生的同步性问题。一种锁自身的性能再好,也不会对整个应用带来多大的性能提升,临界区的大小才是关键。
比如,A 线程,加锁用了 1ms,然后在临界区内呆了 100ms,解锁(1ms),总共用了 102ms。
在 A 处于临界区时,B 线程试图加锁,发生竞争,等待 A 结束,需要等待 0~102ms。
及时用了超级 NB 的锁,加解锁只需 0.0000000001ms,对于 B 来说也无济于事。
我曾见过一个横跨了几百行代码的锁,类似于这样:
LOCK
// 中间省略几百行
x = y; // 真正需要加锁的代码
// 又省略几百行
UNLOCK
写代码的同学还特意用了信号量,感觉能提高效率……
所以,就 iOS 开发而言,能用 @synchronized
就用吧,简单、支持嵌套,还能避免各种死锁问题,何乐而不为。