iOS开发进阶:线程同步技术-锁

多线程.png

在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);
}

注意:卖票和取钱不要共用一把锁。这里创建了两把锁sellLockmoneyLock

自旋锁现在不再安全,因为可能出现优先级反转问题。如果等待锁的线程优先级较高,他会一直占用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
// 头文件 #import <pthread.h>
- (void)__initLock:(pthread_mutex_t *)lock {
// 初始化属性
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
// 设置为普通锁,PTHREAD_MUTEX_RECURSIVE表示递归锁
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用于保证属性settergetter的原子性操作,相当于对settergetter内部加了同步锁。它并不能保证使用属性的使用过程是线程安全的。

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_semaphorepthread_mutex两个。因为OSSpinLock性能最好但是不安全,os_unfair_lock在iOS10才出现低版本不支持不推荐。

自旋锁、互斥锁的选择

自旋锁预计线程等待锁的时间很短,加锁经常被调用但竞争情况很少出现。常用于多核处理器。
互斥锁预计等待锁的时间较长,单核处理器。临界区有IO操作,例如文件读写。

示例代码:锁实例代码-Github

小结

  • 怎样用GCD实现多读单写?
  • iOS提供几种多线程技术各自的特点?
  • NSOperation对象在Finished之后是怎样从队列中移除的?
  • 你都用过哪些锁?结合实际谈谈你是怎样使用的?

参考

小码哥底层班视频
正确使用多线程同步锁@synchronized()
深入理解iOS开发中的锁
Object-C 多线程中锁的使用-NSLock