深入浅出iOS多线程(一)——线程的概念
深入浅出iOS多线程(二)——pthraed和NSThread的使用
深入浅出iOS多线程(三)——GCD多线程
深入浅出iOS多线程(四)——NSOperation多线程
深入浅出iOS多线程(五)——多线程锁
线程锁
使用多线程能提高程序的执行效率,但也同时也给程序带来一些线程安全上面的问题,比如是数据竞争关系
(data race).
最常见的线程/进程同步的方法
- 临界资源
- 各进程(线程)采取互斥的方式,实现共享的资源称作临界资源,也可以说是临界区所要访问的资源
- 临界区
- 访问临界资源的一块代码
- 自旋锁
- 用于多线程同步的一种锁,线程反复的去检测锁变量是否可用
- 由于这一过程中一直执行,属于忙等待
- 这种锁在阻塞时间短的场合下使用
- 互斥锁
- 防止两条线程同时对同一资源进行读写的机制
- 通常是将代码切成一个一个的临界区
- 递归锁、非递归锁
- 读写锁
- 读操作可以并发读取
- 写操作是互斥的
- 通常是使用互斥锁、条件变量、信号量实现
- 信号量锁
- 更高级的一种锁,在semahpore去值为0/1的时候为互斥锁,
- 通过取值的范围,来实现更加复杂的同步效果
- 条件锁:
- 条件变量,当某些资源要求不满足的时候进入休眠,
- 当资源要求满足的时候条件锁打开,
iOS中的线程锁
解释锁的时候,首先来实现一个简单数据竞态代码:
dispatch_queue_t q = dispatch_get_global_queue(0, 0);
__block int count = 0;
for (int i = 0; i<10000; i++) {
dispatch_async(q, ^{
count ++;
NSLog(@"%d",count);
});
}
打印结果:
9995
9996
9997
9998
最终结果应该是10000,最后的结果是9998,少+了2,说明线程之间有数据竞态的情况
一、互斥锁
1. NSLock<NSLocking>
NSLock
是Foundation框架中一种锁,代码如下:
@protocol NSLocking
- (void)lock;//加锁
- (void)unlock;//解锁
@end
@interface NSLock : NSObject <NSLocking> {
@private
void *_priv;
}
//尝试加锁,不会阻塞线程。YES则加锁成功,NO则失败,说明其他线程在加锁中这个方法无论如何都会立即返回。
- (BOOL)tryLock;
//尝试在指定NSDate之前加锁,会阻塞线程。YES则加锁成功,NO则失败,说明其他线程在加锁中这个方法无论如何都会立即返回。在拿不到锁时不会一直在那等待。
- (BOOL)lockBeforeDate:(NSDate *)limit;
//name 是用来标识用的
@property (nullable, copy) NSString *name API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
@end
一个简单的加锁实例代码;
- (void)createLock{
/*
* NSLock加锁互斥锁
*
**/
__block int count = 0;
//NSLock 初始化
NSLock *lock = [[NSLock alloc]init];
dispatch_queue_t q = dispatch_get_global_queue(0, 0);
for (int i = 0; i<10000; i++) {
dispatch_async(q, ^{
//
//加锁
[lock lock];
count ++ ;
NSLog(@"%d",count);
//解锁
[lock unlock];
});
}
//尝试加锁,返回YES加锁成功,返回NO获取锁失败,加锁失败
// [lock tryLock];
//在nsdate之前加锁,加锁成功返回YES,加锁失败返回NO
// [lock lockBeforeDate:[[NSDate new]dateByAddingTimeInterval:10]];
}
打印代码:
9995
9996
9997
9998
9999
10000
2. pthread_mutex
pthread_mutex
是跨平台,iOS系统自带多线程技术pthread
的线程锁,它的简单使用方法如下:
pthread_mutex_t pMutex;
pthread_mutex_init(&pMutex, NULL); //初始化pthread_mutex_t
pthread_mutex_lock(&pMutex); //加锁
pthread_mutex_unlock(&pMutex); //解锁
代码例子:
pthread_mutex_t pthreadmutex;
/*
#define PTHREAD_MUTEX_NORMAL 0 默认
#define PTHREAD_MUTEX_ERRORCHECK 1 检错锁
#define PTHREAD_MUTEX_RECURSIVE 2 递归锁
#define PTHREAD_MUTEX_DEFAULT PTHREAD_MUTEX_NORMAL 默认
*/
pthread_mutex_init(&pthreadmutex, PTHREAD_MUTEX_NORMAL);
dispatch_queue_t q = dispatch_get_global_queue(0, 0);
__block int count = 0;
for (int i = 0; i<10000; i++) {
dispatch_async(q, ^{
pthread_mutex_lock(&pthreadmutex);
count ++;
NSLog(@"%d",count);
pthread_mutex_unlock(&pthreadmutex);
});
}
pthread_mutex_destroy(&pthreadmutex);
打印结果:
9995
9996
9997
9998
9999
10000
3. @synchronized
synchronized
属于互斥锁中的另一个变种:递归锁(重入锁),上面有递归锁的概念介绍,synchronized
在苹果已经开源了有关代码,可以这里查看synchronized,还有
__block int count = 0;
dispatch_queue_t q = dispatch_get_global_queue(0, 0);
for (int i = 0; i<10000; i++) {
dispatch_async(q, ^{
//递归锁
@synchronized (self) {
count ++ ;
NSLog(@"%d",count);
}
});
}
打印内容:
9995
9996
9997
9998
9999
10000
递归锁
递归锁有一个特点,就是同一个线程可以加锁N次而不会引发死锁。重入锁
首先来尝试一个重入死锁的代码:
__block int count = 0;
dispatch_queue_t q = dispatch_queue_create("struggle3g", DISPATCH_QUEUE_CONCURRENT);
NSLock *lock1 = [[NSLock alloc]init];
dispatch_async(q, ^{
for (int j = 0; j<5; j++) {
[lock1 lock];
int num = count;
sleep(1);
num ++;
count = num;
NSLog(@"%d",count);
}
});
上述内容就是造成死锁的条件
1. NSRecursiveLock<NSLocking>
NSRecursiveLock *lock = [[NSRecursiveLock alloc]init];
__block int count = 0;
dispatch_queue_t q = dispatch_queue_create("struggle3g", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(q, ^{
for (int j = 0; j<5; j++) {
[lock lock];
int num = count;
sleep(1);
num ++;
count = num;
NSLog(@"%d",count);
}
});
//[lock unlock]; //解锁
//尝试加锁,返回YES加锁成功,返回NO获取锁失败,加锁失败
// [lock tryLock];
//在nsdate之前加锁,加锁成功返回YES,加锁失败返回NO
// [lock lockBeforeDate:[[NSDate new]dateByAddingTimeInterval:10]];
上述代码发现不会死锁。
2. pthread_mutex(recursive)
pthread_mutex锁也支持递归,只需要设置PTHREAD_MUTEX_RECURSIVE即可
pthread_mutex_t lock;
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
pthread_mutex_init(&lock, &attr);
pthread_mutexattr_destroy(&attr);
pthread_mutex_lock(&lock);
pthread_mutex_unlock(&lock);
pthread_mutex实现递归锁代码如下:
- (void)doing{
// for (int j = 0; j<5; j++) {
int num = _Mycount;
sleep(1);
num ++;
_Mycount = num;
NSLog(@"%d",_Mycount);
// }
}
_Mycount = 0;
__block pthread_mutex_t lock;
//初始化锁属性
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
//设置该锁为递归锁
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
//初始化锁
pthread_mutex_init(&lock, &attr);
dispatch_queue_t q = dispatch_queue_create("struggle3g", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(q, ^{
for (int j = 0; j<5; j++) {
pthread_mutex_lock(&lock);
[self doing];
}
pthread_mutex_unlock(&lock); //如果加了递归锁,不释放锁,同一个线程还是可以使用锁。
NSLock *lock2 = [[NSLock alloc]init];
dispatch_async(q, ^{
for (int j = 0; j<5; j++) {
[lock2 lock];
[self doing];
[lock2 unlock];
}
});
});
//销毁锁
pthread_mutexattr_destroy(&attr);
上述代码:我在一个循环之中一直加锁,但是并没有死锁。
二 自旋锁
何时使用自旋锁,何时使用互斥锁:
- 当预计线程等待锁的时间很短,或者加锁的代码(临界区)经常被调用,但竞争情况很少发生,再或者CPU资源不紧张,拥有多核处理器的时候使用自旋锁比较合适。
- 而当预计线程等待锁的时间较长,CPU是单核处理器,或者临界区有IO操作,或者临界区代码复杂或者循环量大,临界区竞争非常激烈的时候使用互斥锁比较合适
OSSpinLock自旋锁,因为不在安全,优先级反转。在iOS10以后被弃用,因为有可能会因为优先级反转导致线程不在安全。 苹果给出的替代方案是os\_unfair\_lock
,
os\_unfair\_lock
猜测:
- 在优先级反转的时候,应该是做了一些相应的处理,比如说直接释放低优先级的锁,在重新加入到线程当中。
- 或者直接使用互斥锁代替,也可能是优化后的互斥锁
os_unfair_lock
__block os_unfair_lock _osunfairLock;
// 初始化os_unfair_lock
_osunfairLock = OS_UNFAIR_LOCK_INIT;
dispatch_queue_t q = dispatch_queue_create("struggle3g", DISPATCH_QUEUE_CONCURRENT);
__block int count = 0;
for (int i = 0; i<10000; i++) {
dispatch_async(q, ^{
// 加锁
os_unfair_lock_lock(&(_osunfairLock));
count ++;
NSLog(@"%d",count);
// 解锁
os_unfair_lock_unlock(&(_osunfairLock));
});
}
/*
*如果锁当前由调用线程拥有,这个函数返回。
*
*如果锁被其他线程解锁或拥有,这个函数断言和终止进程。
**/
// void os_unfair_lock_assert_owner
/*
*如果锁被其他线程解锁或拥有,这个函数
*返回。
*
*如果锁当前由当前线程拥有,则此函数断言
并终止进程。
**/
// void os_unfair_lock_assert_not_owner
/*
尝试获取锁如果其他线程持有锁,返回NO,如果获取到锁,返回YES
*/
// bool os_unfair_lock_trylock
三、读写锁
是计算机程序的并发控制的一种同步机制,也称“共享-互斥锁”、多读者-单写者锁) 用于解决多线程对公共资源读写问题。读操作可并发重入,写操作是互斥的。 读写锁通常用互斥锁、条件变量、信号量实现。
pthread的API:
pthread_rwlock
//加读锁
pthread_rwlock_rdlock(&rwlock);
//解锁
pthread_rwlock_unlock(&rwlock);
//加写锁
pthread_rwlock_wrlock(&rwlock);
//解锁
pthread_rwlock_unlock(&rwlock);
四、条件锁
1. NSCondition<NSLocking>
//条件锁NSCondition的API
wait //等待
waitUntilDate:(NSDate *)limit //
signal
broadcast
遵循NSLocking协议,使用的时候同样是lock,unlock加解锁,wait是傻等,waitUntilDate:方法是等一会,都会阻塞掉线程,signal是唤起一个在等待的线程,broadcast是广播全部唤起,使用的时候感觉跟信号量类似。
dispatch_queue_t q = dispatch_queue_create("struggle3g", DISPATCH_QUEUE_CONCURRENT);
NSCondition *lock = [[NSCondition alloc] init];
//第一个线程
__block BOOL finished = NO;
dispatch_async(q, ^{
[lock lock];
while (!finished) {
[lock wait];
NSLog(@"第一个线程得到第二个线程的通知");
}
[lock unlock];
NSLog(@"第一个线程使用完毕");
});
//第二个线程
dispatch_async(q, ^{
[lock lock];
sleep(2);
finished = YES;
NSLog(@"我做了一些事情,告诉第一个线程");
[lock signal];
[lock unlock];
NSLog(@"第二个线程使用完毕");
});
2. NSConditionLock<NSLocking>
条件锁的API
@property (readonly) NSInteger condition;
//初始化时必须放入条件condition
- (instancetype)initWithCondition:(NSInteger)condition;
//加锁时添加条件
- (void)lockWhenCondition:(NSInteger)condition;
//尝试加锁
- (BOOL)tryLock;
//尝试加锁的条件
- (BOOL)tryLockWhenCondition:(NSInteger)condition;
//解锁附加条件
- (void)unlockWithCondition:(NSInteger)condition;
//limit时间之前加锁
- (BOOL)lockBeforeDate:(NSDate *)limit;
//limit时间之前加锁附加条件
- (BOOL)lockWhenCondition:(NSInteger)condition beforeDate:(NSDate *)limit;
这个条件锁,跟NSLock
使用类似,是不过API附加了condition
五、信号量
1. dispatch_semaphore_t
- 创建信号量
dispatch_semaphore_create(value)
,value
代表信号量的值 -
dispatch_semaphore_wait(<dispatch_semaphore_t>,DISPATCH_TIME_FOREVER)
, -
dispatch_semaphore_signal(<dispatch_semaphore_t>)
- 上面两个方法可以理解为一个完整的信号量,
dispatch_semaphore_signal
+1
信号量的值,而dispatch_semaphore_wait-1
信号量的值 - 信号量的值如果等于0
dispatch_semaphore_wait
就会阻塞该方法以下的内容,当调用dispatch_semaphore_signal
,信号量的值+1
, dispatch_semaphore_wait就会收到信号,信号量的值大于0就继续向下执行,直到信号量的值为0位置。
- 从上述的两个解释可以得出结论:
- dispatch_semaphore_create的信号量的值必须大于等于0,信号量的值为0时,dispatch_semaphore_wait 阻塞,必须调用
dispatch_semaphore_signal
信号量值+1
,不能再次调用dispatch_semaphore_wait
让信号量小于0
- dispatch_semaphore_create的信号量的值必须大于等于0,信号量的值为0时,dispatch_semaphore_wait 阻塞,必须调用
- 上面两个方法可以理解为一个完整的信号量,
所以通过信号量的值设置为0,在异步方法之后添加dispatch_semaphore_wait
,信号量的值设置为0阻塞,这时异步方法执行完成执行dispatch_semaphore_signal
,会执行dispatch_semaphore_wait
信号量-1
,达到同步线程的目的。
验证代码如下:
__block int count = 0;
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
dispatch_queue_t q = dispatch_queue_create("struggle3g", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(q, ^{
count ++;
NSLog(@"%d",count);
dispatch_semaphore_signal(sema);
});
NSLog(@"第一次wait");
dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
dispatch_async(q, ^{
count ++;
NSLog(@"%d",count);
dispatch_semaphore_signal(sema);
});
NSLog(@"第二次wait");
dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
dispatch_async(q, ^{
count ++;
NSLog(@"%d",count);
dispatch_semaphore_signal(sema);
});
NSLog(@"第三次wait");
dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
dispatch_async(q, ^{
count ++;
NSLog(@"%d",count);
dispatch_semaphore_signal(sema);
});
NSLog(@"第四次wait");
dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
打印内容:
第一次wait
1
第二次wait
2
第三次wait
3
第四次wait
4
线程锁遇到的问题
1. 死锁
所谓死锁,通常指有两个线程A和B都卡住了,并等待对方完成某些操作。A不能完成是因为它在等待B完成,B不能完成,因为它在等待A完成,于是就是死锁了。
2. 产生死锁的四个必要条件
四个必要条件
- 互斥条件:一个锁每次只能被一个线程获取
- 请求与保持条件:一个线程因请求获取锁而阻塞时,对已获得的锁保持
- 不可剥夺条件:线程已获得的锁,在未使用完之前,不能强行释放
- 环路等待条件:必然存在一个获取锁的环形链,即A等待获取b的锁,B等待获取C的锁,而C等待获取A的锁。这就形成了一个环
3. 死锁的方式
一般死锁
- 如何造成这种死锁
- 线程A1,A2都需要同时获取锁B1、B2锁才能正常地完成功能
- 但是由于线程A1先持有了B1锁,而线程A2先获取了B2的锁
- 线程A1等待A2释放B2的锁才能完成任务解锁,而线程A2等待A1释放A1的锁才能完成任务解锁
- 这就造成了死锁
- 解决方法:
- 等其中一条线程完全执行完之后再执行另外一条线程。
- 设置优先级,如果运行多条线程出现死锁,优先级低的回退,优先级高的先执行这样即可解决死锁问题。
递归死锁(重入死锁)
-
如何造成这种死锁
- 一个线程持有一个对象的锁,在没有释放这个锁之前又获取了一次锁,这也就造成了线程的死锁
-
解决方法:
- 在第二次回去锁的时候先讲第一次获取的锁匙放
- 使用递归锁,进行加锁
总结
性能总结
OSSpinLock(弃用) 0.097348s
dispatch_semaphore 0.155043s
os_unfair_lock 0.171789s
pthread_mutex 0.262592s
NSLock 0.283196s
pthread_mutex(recursive) 0.372398s
NSRecursiveLock 0.473536s
NSConditionLock 0.950285s
@synchronized 1.101924s
网友评论