当我们写下@property (weak) id obj时,编译器默认会给obj这个属性加atomic关键字,也就是说,默认的setter和getter方法里是加了锁的。

atomic加锁

在这个系列之前的文章中说过,属性的attribute关键字大部分都是作用在了setter和getter的代码实现中,像weak和strong是直接作用在了成员变量上,而atomic,nonatomic,copy等等这些关键字则是在setter和getter中实现。所以,一旦需要重写setter和getter方法,那么在@property中声明的这几个关键字,当然也就不再有用了,需要你自己来实现效果,例如@property (nonatomic, copy) NSString *name的setter方法重写,就需要写成:

- (void)setName:(NSString *)name {
_name = [name copy];
}

那么atomic是应该怎么在setter和getter中实现呢,事实上,仅需要给self加锁,比如@property (atomic, copy) NSString *name重写setter,getter:

@synthesize name = _name;
- (void)setName:(NSString *)name {
@synchronized(self) {
_name = [name copy];
}
}
- (NSString *)name {
@synchronized(self) {
return _name;
}
}

不过这么写,也依然不能保证线程安全。如果线程A调用了getter,同时线程B和线程C都调用了setter,那么A线程getter得到的值,可能是BC在set之前的原始值,也可能是B set的值或者C set的值。同时这个属性的值,也可能是B set的值或者C set的值。
所以,保证数据完整性不能简单靠一把锁来完成,毕竟这个是多线程编程最大的难点。

虽然一把锁不能保证数据完整性,但是我们还是有必要弄清楚OC究竟都可以用哪些锁,他们都是什么特征。

多线程加锁底层知识

1.时间片轮转调度算法

了解多线程加锁必须知道时间片轮转调度算法,才能深切理解其原理、性能瓶颈。

现代操作系统在管理普通线程时,通常采用时间片轮转算法(Round Robin,简称 RR)。每个线程会被分配一段时间片(quantum),通常在 10-100 毫秒左右。当线程用完属于自己的时间片以后,就会被操作系统挂起,放入等待队列中,直到下一次被分配时间片,如果线程在时间片结束前阻塞或结束,则CPU当即进行切换。由于线程切换需要时间,如果时间片太短,会导致大量CPU时间浪费在切换上;而如果这个时间片如果太长,会使得其它线程等待太久。

2.原子操作

狭义上的原子操作表示一条不可打断的操作,也就是说线程在执行操作过程中,不会被操作系统挂起,而是一定会执行完(理论上拥有CPU时间片无限长)。在单处理器环境下,一条汇编指令显然是原子操作,因为中断也要通过指令来实现,但一句高级语言的代码却不是原子的,因为它最终是由多条汇编语言完成,CPU在进行时间片切换时,大多都会在某条代码的执行过程中。

但在多核处理器下,则需要硬件支持。

3.自旋锁和互斥锁

都属于CPU时间分片算法下的实现保护共享资源的一种机制。都实现互斥操作,加锁后仅允许一个访问者。

却别在于自旋锁不会使线程进入wait状态,而通过轮训不停查看是否该自旋锁的持有者已经释放的锁;对应的,互斥锁在出现锁已经被占用的情况会进入wait状态,CPU会当即切换时间片。

OC中的同步锁

1.自旋锁 OSSpinLock

__block OSSpinLock oslock = OS_SPINLOCK_INIT;
//线程2
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
NSLog(@"线程2 befor lock");
OSSpinLockLock(&oslock);
NSLog(@"线程2");
OSSpinLockUnlock(&oslock);
NSLog(@"线程2 unlock");
});
//线程1
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
NSLog(@"线程1 befor lock");
OSSpinLockLock(&oslock);
NSLog(@"线程1 sleep");
sleep(4);
NSLog(@"线程1");
OSSpinLockUnlock(&oslock);
NSLog(@"线程1 unlock");
});

OSSpinLock效率奇高,主要原因是:并没有进入系统kernel,使用它可以节省系统调用和上下文切换。

YY大神在自己的博客中说,OSSpinLock不再安全(链接:不再安全的 OSSpinLock
但是在GCD多线程实际使用中,并不会发现什么问题。并行线程只要获取oslock,其他线程一律阻塞。

多线程中往往会遇到另一个概念:优先级翻转

低优先级线程拿到锁时,高优先级线程进入忙等(busy-wait)状态,消耗大量 CPU 时间,从而导致低优先级线程拿不到 CPU 时间,也就无法完成任务并释放锁。这种问题被称为优先级反转。

YY大神说OSSpinLock不安全实际上就是因为这个原因,具体可以看他的文章。

2.信号量 dispatch_semaphore

YY大神推荐使用信号量dispatch_semaphore作为自旋锁的替代方案。

dispatch_semaphore_t signal = dispatch_semaphore_create(1);
dispatch_time_t timeout = dispatch_time(DISPATCH_TIME_NOW, 5.0f * NSEC_PER_SEC);
//线程1
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"线程1 holding");
dispatch_semaphore_wait(signal, timeout); //signal 值 -1
NSLog(@"线程1 sleep");
sleep(4);
NSLog(@"线程1");
dispatch_semaphore_signal(signal); //signal 值 +1
NSLog(@"线程1 post singal");
});
//线程2
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"线程2 holding");
dispatch_semaphore_wait(signal, timeout);
NSLog(@"线程2 sleep");
sleep(4);
NSLog(@"线程2");
dispatch_semaphore_signal(signal);
NSLog(@"线程2 post signal");
});

dispatch_semaphore_create(1)为创建信号,()中数字表示可以同时几个线程使用信号。为1表示同步使用。上述代码如果此处标2就和没设置信号量一样,并发自行运行。如果设置为0,则一律等待overTime时自动释放,所有代码都不执行,理论上也具有同步作用。

dispatch_semaphore_wait中传入的timeout表示最长加锁时间,自动释放锁后,其它线程可以获取信号并继续运行。

3.pthread_mutex锁

pthread表示的是POSIX thread,定义的是一组跨平台线程相关的API。

pthread_mutex互斥锁是一个非递归锁,如果同一线程重复调用加锁会造成死锁。

用法比较简单

static pthread_mutex_t pmutexLock;
pthread_mutex_init(&pLock, NULL);
//1.线程2
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
NSLog(@"线程2 befor lock");
pthread_mutex_lock(&pLock);
NSLog(@"线程2");
pthread_mutex_unlock(&pLock);
});
//2.线程1
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"线程1 before lock");
pthread_mutex_lock(&pLock);
sleep(3);
NSLog(@"线程1");
pthread_mutex_unlock(&pLock);
});

pthread_mutex(recursive) 递归锁,比较安全,同一线程有且仅有一次加锁,重复加锁不会死锁。无论加锁几次,只需解锁一次。

static pthread_mutex_t pLock;
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr); //初始化attr赋初值
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE); //设置锁类型为递归锁
pthread_mutex_init(&pLock, &attr);
pthread_mutexattr_destroy(&attr);
//1.线程1
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
static void (^RecursiveBlock)(int);
RecursiveBlock = ^(int value) {
pthread_mutex_lock(&pLock);
if (value > 0) {
NSLog(@"value: %d", value);
RecursiveBlock(value - 1);
}
};
NSLog(@"线程1 before lock");
RecursiveBlock(5);
NSLog(@"线程1");
pthread_mutex_unlock(&pLock);
NSLog(@"线程1 unlock");
});
//2.线程2
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"线程2 before lock");
pthread_mutex_lock(&pLock);
NSLog(@"线程2");
pthread_mutex_unlock(&pLock);
NSLog(@"线程2 unlock");
});

4.Foundation框架NSLock NSRecursiveLock

NS开头类都是对CoreFoundation的封装,会更易用。
NSRecursiveLock顾名思义是递归锁。

NSLock 只是在内部封装了一个 pthread_mutex,属性为PTHREAD_MUTEX_ERRORCHECK

NSRecursiveLock 与 NSLock 的区别在于内部封装的 pthread_mutex_t

对象的类型不同,NSRecursiveLock 的类型为 PTHREAD_MUTEX_RECURSIVE。

5.条件锁 NSConditionLock

和NSLock主要区别是增加了一个NSInteger类型的condition参数,api很简单,也很少。condition就是一个条件标识。在加锁和解锁时对NSConditionLock做条件判断和修改,相当于if语句。

实际的实现原理就是里面封装了一个NSCondition对象。

NSCondition它通常用于标明共享资源是否可被访问或者确保一系列任务能按照指定的执行顺序执行。如果一个线程试图访问一个共享资源,而正在访问该资源的线程将其条件设置为不可访问,那么该线程会被阻塞,直到正在访问该资源的线程将访问条件更改为可访问状态或者说给被阻塞的线程发送信号后,被阻塞的线程才能正常访问这个资源。

NSConditionLock在lock时判断NSCondition对象的条件是否满足,不满足则wait,unlock时对发送NSCondition的broadcast,属于一个常见的生产者–消费者模型。

6.简单易用的@synchronized

@synchronized 实际上是把修饰对象当做锁来使用。这是通过一个哈希表来实现的,OC 在底层使用了一个互斥锁的数组(你可以理解为锁池),通过对对象去哈希值来得到对应的互斥锁。


以上各种锁的实现原理可以参考深入理解 iOS 开发中的锁
他们的性能可以参考YY大神的这张图。

lock_benchmark.png

各种锁单从效率上来看dispatch_barrier_async和@synchronized差的比较多,不建议使用,其它整体相差不大;相同类型的锁递归锁和普通锁效率相差接近一倍,如果不会在循环或者递归中频繁使用加锁和解锁,不建议使用递归锁;OSSpinlock各路大神都说有问题,从效率上讲,建议用互斥锁pthread_mutex(YYKit方案)或者信号量dispatch_semaphore(CoreFoundation和protobuf方案)作为替代。