移动端iOS锁iOS开发进阶:线程同步技术-锁
镇长
在iOS多线程中,经常会出现资源竞争和死锁的问题。本节将学习iOS中不同的锁。
线程同步方案
常见的两个问题:多线程买票和存取钱问题。
示例:存取钱问题
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| // 示例:存取钱问题 - (void)moneyTest { self.moneyCount = 100; dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); dispatch_async(queue, ^{ for (int i = 0; i < 5; i++) { [self saveMoney]; } }); dispatch_async(queue, ^{ for (int i = 0; i < 5; i++) { [self takeMoney]; } }); } - (void)saveMoney { int oldCount = self.moneyCount; sleep(0.2); oldCount += 50; self.moneyCount = oldCount; NSLog(@"存50,还剩%d钱", self.moneyCount); }
- (void)takeMoney { int oldCount = self.moneyCount; sleep(0.2); oldCount -= 20; self.moneyCount = oldCount; NSLog(@"取20,还剩%d钱", self.moneyCount); }
|
示例:卖票问题
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| // 示例:买票 - (void)sellTest { self.count = 15; dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); dispatch_async(queue, ^{ for (int i = 0; i < 5; i++) { [self printTest2]; } }); dispatch_async(queue, ^{ for (int i = 0; i < 5; i++) { [self printTest2]; } }); dispatch_async(queue, ^{ for (int i = 0; i < 5; i++) { [self printTest2]; } }); } - (void)printTest2 { NSInteger oldCount = self.count; sleep(0.2); oldCount --; self.count = oldCount; NSLog(@"还剩%ld张票 - %@", (long)oldCount, [NSThread currentThread]); }
|
解决上面这种资源共享问题,就需要使用线程同步技术。线程同步技术的核心是:锁。下面学习iOS中不同锁的使用,比较不同锁之间的优缺点。
示例代码:演示购票和存取钱问题:Demo
iOS当中有哪些锁?
1 2 3 4 5 6 7 8
| @synchronized 常用于单例 atomic 原子性 OSSpinLock 自旋锁 NSRecursiveLock 递归锁 NSLock dispatch_semaphore_t 信号量 NSCondition 条件 NSConditionLock 条件锁
|
简介:
@synchronized
使用场景:一般在创建单例对象时使用,保证对象在多线程中是唯一的。
atomic
属性关键字原子性,保证赋值操作是线程安全的,读取操作不能保证线程安全。
OSSpinLock
自旋锁。特点:循环等待访问,不释放当前资源。常用于轻量级数据访问,简单的int值+1/-1操作。
* NSLock
某个线程A调用lock方法。这样,NSLock将被上锁。可以执行“关键部分”,完成后,调用unlock方法。如果,在线程A 调用unlock方法之前,另一个线程B调用了同一锁对象的lock方法。那么,线程B只有等待。直到线程A调用了unlock。
1 2 3
| [lock lock]; //加锁 // 关键部分 [lock unlock]; // 解锁
|
NSRecursiveLock
递归锁,特点:递归锁在被同一线程重复获取时不会产生死锁。
dispatch_semaphore_t
信号量1 2 3 4 5 6
| // 创建信号量结构体对象,含有一个int成员 dispatch_semaphore_create(1); // 先对value减一,如果小于零表示没有资源可以访问。通过主动行为进行阻塞。 dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER); // value加1,小于等零表示有队列在排队,通过被动行为进行唤醒 dispatch_semaphore_signal(semaphore);
|
OSSpinLock
自旋锁,等待锁的线程会处于忙等状态,一直占用着CPU资源。
常用API:
1 2 3 4 5 6 7 8 9 10
| 导入头文件 #import <libkern/OSAtomic.h>
OSSpinLock lock = OS_SPINLOCK_INIT;
OSSpinLockTry(&lock);
OSSpinLockLock(&lock);
OSSpinLockUnlock(&lock);
|
使用OSSpinLock
解决卖票问题
1 2 3 4 5 6 7 8 9 10 11 12 13
| // 自旋锁:#import <libkern/OSAtomic.h> // 定义一个全局的自旋锁对象 lock 。 - (void)printTest2 { // 加锁 OSSpinLockLock(&_lock); NSInteger oldCount = self.count; sleep(0.2); oldCount --; self.count = oldCount; NSLog(@"还剩%ld张票 - %@", (long)oldCount, [NSThread currentThread]); // 解锁 OSSpinLockUnlock(&_lock); }
|
使用OSSpinLock
解决存取钱问题
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| - (void)saveMoney { OSSpinLockLock(&_moneyLock); int oldCount = self.moneyCount; sleep(0.2); oldCount += 50; self.moneyCount = oldCount; NSLog(@"存50,还剩%d钱", self.moneyCount); OSSpinLockUnlock(&_moneyLock); } - (void)takeMoney { OSSpinLockLock(&_moneyLock); int oldCount = self.moneyCount; sleep(0.2); oldCount -= 20; self.moneyCount = oldCount; NSLog(@"取20,还剩%d钱", self.moneyCount); OSSpinLockUnlock(&_moneyLock); }
|
注意:卖票和取钱不要共用一把锁。这里创建了两把锁sellLock
和moneyLock
。
自旋锁现在不再安全,因为可能出现优先级反转问题。如果等待锁的线程优先级较高,他会一直占用CPU资源,优先级低的线程就无法获取CPU资源完成任务并释放锁。可以查看这篇文章不再安全的OSSpinLock。
本节示例代码:线程同步解决方案Demo
os_unfair_lock
自旋锁已经不再安全,存在优先级反转问题。苹果在iOS10开始使用os_unfair_lock
取代了OSSpinLock
。从底层调用来看,自旋锁和os_unfair_lock
的区别,前者等待线程处于忙等,而后者等待线程处于休眠状态。
常用API:
1 2 3 4 5 6 7 8 9 10
| 导入头文件 #import <os/lock.h> // 初始化 os_unfair_lock lock = OS_UNFAIR_LOCK_INIT; // 尝试加锁 os_unfair_lock_trylock(&_lock); // 加锁 os_unfair_lock_lock(&_lock); // 解锁 os_unfair_lock_unlock(&_lock);
|
pthread_mutex
互斥锁,等待锁的线程处于休眠状态。
常用API:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| - (void)__initLock:(pthread_mutex_t *)lock { pthread_mutexattr_t attr; pthread_mutexattr_init(&attr); pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_DEFAULT); pthread_mutex_init(lock, &attr); pthread_mutexattr_destroy(&attr); }
pthread_mutex_lock(&lock);
pthread_mutex_unlock(&lock);
pthread_cond_init(&cond, NULL)
pthread_cond_wait(&cond, &lock);
pthread_cond_signal(&cond);
pthread_cond_broadcast(&cond);
pthread_mutex_destory(&lock); pthread_cond_destory(&cond);
|
其中PTHREAD_MUTEX_DEFAULT
设置的是锁的类型,还有另一种类型PTHREAD_MUTEX_RECURSIVE
表示递归锁。递归锁允许同一个线程对一把锁进行重复加锁。
NSLock&NSRecursiveLock&NSCondition
NSLock
是对mutex
普通锁的封装。
1 2 3 4 5 6 7 8 9 10
| @protocol NSLocking - (void)lock; - (void)unlock; @end
@interface NSLock : NSObject <NSLocking> { - (BOOL)tryLock; // 尝试加锁 - (BOOL)lockBeforeDate:(NSDate *)limit; //在时间之前获取锁并返回,YES表示成功。 } @end
|
NSRecursiveLock
是对mutex
递归锁的封装,API同NSLock
相似。
NSCondition
是对mutex
条件的封装。
1 2 3 4 5 6 7 8 9 10
| @protocol NSLocking - (void)lock; - (void)unlock; @end @interface NSCondition : NSObject <NSLocking> { - (void)wait; // 等待 - (BOOL)waitUntilDate:(NSDate *)limit; // 等待某一个时间段 - (void)signal; // 唤醒 - (void)broadcast; // 唤醒所有睡眠线程 }
|
以上可以查看pthread_mutex
使用。
atomic
atomic
用于保证属性setter
和getter
的原子性操作,相当于对setter
和getter
内部加了同步锁。它并不能保证使用属性的使用过程是线程安全的。
NSConditionLock
NSConditionLock
是对NSCondition
的进一步封装。可以设置具体的条件值。
1 2 3 4 5 6 7 8 9 10 11 12
| // 遵循NSLocking协议。 @interface NSConditionLock : NSObject <NSLocking> { - (instancetype)initWithCondition:(NSInteger)condition NS_DESIGNATED_INITIALIZER; // 初始化,传入一个条件值
@property (readonly) NSInteger condition; - (void)lockWhenCondition:(NSInteger)condition; // 条件值符合加锁 - (BOOL)tryLock; //尝试加锁 - (BOOL)tryLockWhenCondition:(NSInteger)condition; - (void)unlockWithCondition:(NSInteger)condition; - (BOOL)lockBeforeDate:(NSDate *)limit; - (BOOL)lockWhenCondition:(NSInteger)condition beforeDate:(NSDate *)limit; @end
|
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| // 删除 - (void)__one {
// 当锁内部条件值为1时,加锁。 // [self.condition lockWhenCondition:1]; [self.condition lock]; // 直接使用lock也可以 sleep(1); NSLog(@"%s ①", __func__); [self.condition unlockWithCondition:2]; // 解锁,并且条件设置为2 } // 添加 - (void)__two { [self.condition lockWhenCondition:2]; //条件值为2时,加锁。 sleep(1); NSLog(@"%s ②", __func__); [self.condition unlockWithCondition:3]; }
// 添加 - (void)__three { [self.condition lockWhenCondition:3]; //条件值为2时,加锁。 sleep(1); NSLog(@"%s ③", __func__); [self.condition unlock]; }
- (void)otherTest { // ① [[[NSThread alloc] initWithTarget:self selector:@selector(__one) object:nil] start]; // ② [[[NSThread alloc] initWithTarget:self selector:@selector(__two) object:nil] start]; // ③ [[[NSThread alloc] initWithTarget:self selector:@selector(__three) object:nil] start]; // 通过设置条件值,可以决定线程的执行顺序。 }
|
输出结果:
-[LENSConditionLock __one] ①
-[LENSConditionLock __two] ②
-[LENSConditionLock __three] ③
信号量
常用API:
1 2 3 4 5 6 7
| // 初始化 dispatch_semaphore_t semaphore = dispatch_semaphore_create(5); // 如果信号量的值<=0,当前线程就会进入休眠等待,直到信号量的值>0 // 如果信号量的值>0,就减1,然后往下执行后面的代码 dispatch_semaphore_wait(self.semaphore, DISPATCH_TIME_FOREVER); // 让信号量的值增加1,信号量值不等于零时,前面的等待的代码会执行。 dispatch_semaphore_signal(self.semaphore);
|
dispatch_semaphore
信号量的初始值,控制线程的最大并发访问数量。
信号量的初始值为1,代表同时只允许1条线程访问资源,保证线程同步。
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| // 设置信号量初始值为5。 - (void)otherTest { for (int i = 0; i < 20; i ++) { [[[NSThread alloc] initWithTarget:self selector:@selector(test) object:nil] start]; } } - (void)test { // 如果信号量的值<=0,当前线程就会进入休眠等待,直到信号量的值>0 // 如果信号量的值>0,就减1,然后往下执行后面的代码 dispatch_semaphore_wait(self.semaphore, DISPATCH_TIME_FOREVER); sleep(2); NSLog(@"test - %@", [NSThread currentThread]); // 让信号量的值增加1 dispatch_semaphore_signal(self.semaphore); }
|
@synchronized
@synchronized
是对mutex
递归锁的封装。
不推荐使用,性能比较差。
1 2 3
| // 源码:objc4中的objc-sync.mm @synchronized (obj) { }
|
性能比较
不再安全的OSSpinLock中对比了不同锁的性能。
推荐使用dispatch_semaphore
和pthread_mutex
两个。因为OSSpinLock
性能最好但是不安全,os_unfair_lock
在iOS10才出现低版本不支持不推荐。
自旋锁、互斥锁的选择
自旋锁预计线程等待锁的时间很短,加锁经常被调用但竞争情况很少出现。常用于多核处理器。
互斥锁预计等待锁的时间较长,单核处理器。临界区有IO操作,例如文件读写。
示例代码:锁实例代码-Github
小结
- 怎样用GCD实现多读单写?
- iOS提供几种多线程技术各自的特点?
- NSOperation对象在Finished之后是怎样从队列中移除的?
- 你都用过哪些锁?结合实际谈谈你是怎样使用的?
参考
小码哥底层班视频
正确使用多线程同步锁@synchronized()
深入理解iOS开发中的锁
Object-C 多线程中锁的使用-NSLock