线程安全
线程安全是多线程编程时的计算机程序代码中的一个概念。在拥有共享数据的多条线程并行执行的程序中,线程安全的代码会通过同步机制保证各个线程都可以正常且正确的执行,不会出现数据污染等意外情况。 在iOS中,UIKit是绝对线程安全的,因为UIKit都是在主线程操作的,单线程没有线程当然没有线程安全问题,但除此之外,其他都要考虑线程安全问题。
iOS解决线程安全的途径其原理大同小异,都是通过锁来使关键代码保证同步执行,从而确保线程安全性,这一点和多线程的异步执行任务是不冲突的。类似骑自行车去酒吧,该省省,该花花。
经典案例
经典的买票问题,假设德云色的相声演出,有50张门票,为方便大家买票,在北京分了5个点售票,一直到售完为止。
#pragma mark - 买票问题,
//卖票演示
- (void)ticketTest{
self.ticketsCount = 50;
self.sellCount = 0;
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
for (NSInteger i = 0; i < 5; i++) {
dispatch_async(queue, ^{
while(self.ticketsCount > 0) {
[self sellingTickets];
}
});
}
//过了2s,老板来查账
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 2*NSEC_PER_SEC), dispatch_get_main_queue(), ^{
NSLog(@"卖出的票数-> %lu", (unsigned long)self.sellCount);
});
}
//卖票
- (void)sellingTickets{
NSUInteger oldCount = self.ticketsCount;
//模拟耗时操作
usleep(arc4random_uniform(30));
oldCount -= 1;
self.ticketsCount = oldCount;
//都在一条线程,确保安全性
dispatch_async(dispatch_get_main_queue(), ^{
self.sellCount ++;
});
NSLog(@"当前剩余票数-> %lu", (unsigned long)oldCount);
}
执行结果如下:
可以看到,最后统计出来居然是63,显然这样直接的去操作是线程不安全的。
接下来就用iOS中的各式各样的锁🔒来解决这个问题。下面个别锁的初始化:
- (void)initLock {
_spinLock = OS_SPINLOCK_INIT;
_unfairLock = OS_UNFAIR_LOCK_INIT;
_nsLock = [[NSLock alloc] init];
_nsCondition = [[NSCondition alloc] init];
_nsConditionLock = [[NSConditionLock alloc] init];
_semaphore = dispatch_semaphore_create(1);
}
OSSpinLock
OSSpinLock
是在libkern库中,使用之前需要引入头文件<libkern/OSAtomic.h>
,使用时会出现警告⚠️
这是因为
OSSpinLock
存在缺陷,从iOS10开始已经不建议使用了。官方建议使用os_unfair_lock
来替代。
#pragma mark -spinlock
/**
OSSpinLock 自旋锁,隶属于 #import <libkern/OSAtomic.h>
有bug,优先级反转
iOS10 就已经弃用
*/
- (void)sellingTicketsBySpinLock {
//_spinLock已经提前初始化
//_spinLock = OS_SPINLOCK_INIT;
OSSpinLockLock(&_spinLock);
NSUInteger oldCount = self.ticketsCount;
if (oldCount == 0) {
OSSpinLockUnlock(&_spinLock);
return;
}
sleep(arc4random_uniform(30));
oldCount -= 1;
self.ticketsCount = oldCount;
OSSpinLockUnlock(&_spinLock);
dispatch_async(dispatch_get_main_queue(), ^{
self.sellCount ++;
});
NSLog(@"当前剩余票数-> %lu", (unsigned long)oldCount);
}
执行打印结果:
os_unfair_lock
os_unfair_lock
在os库中,使用之前需要导入头文件<os/lock.h>
。
- (void)sellingTicketsByUnfairLock {
os_unfair_lock_lock(&_unfairLock);
NSInteger oldCount = self.ticketsCount;
if (oldCount == 0) {
os_unfair_lock_unlock(&_unfairLock);
}
usleep(arc4random_uniform(30));
oldCount -= 1;
self.ticketsCount = oldCount;
os_unfair_lock_unlock(&_unfairLock);
NSLog(@"当前剩余票数-> %lu", (unsigned long)oldCount);
dispatch_async(dispatch_get_main_queue(), ^{
self.sellCount ++;
});
}
执行后结果:
可以看到非常的安全🔐,关于这个不公平锁的分类,网上产生了很多分歧,这里查阅官方文档并翻译:
可以看到这里的解释是,不是旋转(忙等),而是休眠,等待被唤醒,所以个人认为os_unfair_lock
是互斥锁
。
NSLock
当然我们Foundation框架
内部也是有一把🔒-NSLock
,使用起来非常方便,基于pthroad_mutex
封装而来,是一把互斥非递归锁
。
- (void)sellingTicketsByNSLock {
[_nsLock lock];
NSUInteger oldCount = self.ticketsCount;
if (oldCount == 0) {
[_nsLock unlock];
return;
}
usleep(arc4random_uniform(30));
oldCount -= 1;
self.ticketsCount = oldCount;
[_nsLock unlock];
NSLog(@"当前剩余票数-> %lu", (unsigned long)oldCount);
dispatch_async(dispatch_get_main_queue(), ^{
self.sellCount ++;
});
}
同样可以解决问题。
可以通过swift foundation 源码来看一看NSLock
的实现,从而更深层次的了解NSLock
。
直接找到NSLock.swift
文件。
- 构造方法
init()
就是调用了pthread的pthread_mutex_init(mutex, nil)
方法 - 析构方法
deinit
就是调用了pthread的pthread_mutex_destroy(mutex)
方法 - 加锁方法
lock()
就是调用了pthread的pthread_mutex_lock(mutex)
方法 - 解锁方法
unlock()
就是调用了pthread的pthread_mutex_unlock(mutex)
方法 其根本是对pthread_mutex
的封装,在pthread_mutex
中可以通过pthread_mutexattr_settype(attrs, Int32(PTHREAD_MUTEX_RECURSIVE))
来设置锁为递归锁,这里并没有设置,所以在使用的时候需要注意NSLock
并不能在递归函数中使用。
NSCondition
#pragma mark - NSCondition
- (void)sellingTicketsByNSCondition {
[_nsCondition lock];
NSUInteger oldCount = self.ticketsCount;
if (oldCount == 0) {
[_nsCondition unlock];
return;
}
usleep(arc4random_uniform(30));
oldCount -= 1;
self.ticketsCount = oldCount;
[_nsCondition unlock];
NSLog(@"当前剩余票数-> %lu", (unsigned long)oldCount);
dispatch_async(dispatch_get_main_queue(), ^{
self.sellCount ++;
});
}
执行结果:
同样在
NSLock.swift
文件可以找到NSCondition
的实现:
可以看到一样是基于
pthroad_mutex
封装。
相比于NSLock
,NSCondition
多了几个API:
(void)wait
阻塞当前线程,使线程进入休眠,等待唤醒信号。调用前必须已加锁。(void)waitUntilDate
阻塞当前线程,使线程进入休眠,等待唤醒信号或者超时。调用前必须已加锁。(void)signal
唤醒一个正在休眠的线程,如果要唤醒多个,需要调用多次。如果没有线程在等待,则什么也不做。调用前必须已加锁。(void)broadcast
唤醒所有在等待的线程。如果没有线程在等待,则什么也不做。调用前必须已加锁。
注意⚠️:pthread_mutex
存在虚假唤醒
的情况,一个signl
唤醒多个wait
。
在编码过程中可以通过while条件判断
,使被唤醒的线程,陷入while
循环中,从而解决此问题。一般在生产者问题中出现。
NSConditionLock
#pragma mark - NSConditionLock
- (void)sellingTicketsByNSConditionLock {
[_nsConditionLock lock];
NSUInteger oldCount = self.ticketsCount;
if (oldCount == 0) {
[_nsConditionLock unlock];
return;
}
usleep(arc4random_uniform(30));
oldCount -= 1;
self.ticketsCount = oldCount;
[_nsConditionLock unlock];
NSLog(@"当前剩余票数-> %lu", (unsigned long)oldCount);
dispatch_async(dispatch_get_main_queue(), ^{
self.sellCount ++;
});
}
执行结果:
在
NSLock.swift
文件可以找到NSConditionLock
的实现:
这里是对
NSCondition
二次封装。
其实NSConditionLock
的功能不止于此,NSConditionLock
在init
、lock
、unlock
中都可以传入value
。例如:
- (void)test {
self.conditionLock = [[NSConditionLock alloc] initWithCondition:3];
dispatch_queue_t globalQ = dispatch_get_global_queue(0, 0);
dispatch_async(globalQ, ^{
[self.conditionLock lockWhenCondition:3];
NSLog(@"任务1");
[self.conditionLock unlockWithCondition:2];
});
dispatch_async(globalQ, ^{
[self.conditionLock lockWhenCondition:2];
NSLog(@"任务2");
[self.conditionLock unlockWithCondition:1];
});
dispatch_async(globalQ, ^{
[self.conditionLock lockWhenCondition:1];
NSLog(@"任务3");
[self.conditionLock unlockWithCondition:0];
});
}
如此执行,可保证任务执行顺序总是1⃣️2⃣️3⃣️:
NSRecusiveLock
英语小课堂: recursive -- 递归
#pragma mark NSRecursiveLock
- (void)sellingTicketsByNSRecursiveLock {
[_recursiveLock lock];
NSUInteger oldCount = self.ticketsCount;
if (oldCount == 0) {
[_recursiveLock unlock];
return;
}
usleep(arc4random_uniform(30));
oldCount -= 1;
self.ticketsCount = oldCount;
[_recursiveLock unlock];
NSLog(@"当前剩余票数-> %lu", (unsigned long)oldCount);
dispatch_async(dispatch_get_main_queue(), ^{
self.sellCount ++;
});
}
执行结果:
可以看到,这里并没有起到作用。我们先在源码中看其实现:
通过
PTHREAD_MUTEX_RECURSIVE
来设置锁为递归锁。当锁为递归锁的时候,它的使用场景为单个线程
中的递归
调用,显然我们这里的需求并不适合,所以会出问题。
@synchronized
@synchronized
也是可以保证代码同步:
#pragma mark -@synchronized
- (void)sellingTicketsBySynchronized {
@synchronized (self) {
NSUInteger oldCount = self.ticketsCount;
if (oldCount == 0) {
return;
}
usleep(arc4random_uniform(30));
oldCount -= 1;
self.ticketsCount = oldCount;
NSLog(@"当前剩余票数-> %lu", (unsigned long)oldCount);
dispatch_async(dispatch_get_main_queue(), ^{
self.sellCount ++;
});
}
}
结果也是线程安全:
Semaphore信号量
同样的信号量也可以解决线程安全问题:
#pragma mark - samaphore
- (void)sellingTicketsBySemaphore {
//信号量的初始化
//_semaphore = dispatch_semaphore_create(1);
dispatch_wait(self.semaphore, DISPATCH_TIME_FOREVER);
NSInteger oldCount = self.ticketsCount;
if (oldCount == 0) {
dispatch_semaphore_signal(self.semaphore);
return;
}
usleep(arc4random_uniform(30));
oldCount -= 1;
self.ticketsCount = oldCount;
dispatch_semaphore_signal(self.semaphore);
NSLog(@"当前剩余票数-> %lu", (unsigned long)oldCount);
dispatch_async(dispatch_get_main_queue(), ^{
self.sellCount ++;
});
}
关于信号量的使用和原理,在之前的文章中有提及。
锁的分类
锁按照等待师的状态可以分为互斥锁和自旋锁。
互斥锁
在线程获取锁但没有获取到时,线程会进入休眠状态,等锁被释放时线程会被唤醒,例如spinlock
自旋锁
的线程则会一直处于等待状态(忙等待)不会进入休眠,NSLock
、NSCondition
、NSConditionLock
、NSRecursievLock
。
锁的运行效率
锁在运行的时候的效率也各不相同,以下是对比。其中@synchronized
在真机条件下效率会有所提升。在开发时,我们可以根据不同情况,选择适合的锁,来保证我们的代码线程安全。