多线程之锁同步

之前的文章我们详细介绍了iOS中使用多线程的方式,但是一直没有细致的讲解如何在多线程的情况下保证线程的安全,今天我们就介绍下iOS中多线程中保证线程安全的方式之一锁机制

在具体说这些锁之前,先来说几个概念定义:(参考维基百科)

  1. 临界区:指的是一块对公共资源进行访问的代码,并非一种机制或是算法。
  2. 自旋锁:是用于多线程同步的一种锁,线程反复检查锁变量是否可用。由于线程在这一过程中保持执行,因此是一种忙等待。一旦获取了自旋锁,线程会一直保持该锁,直至显式释放自旋锁。 自旋锁避免了进程上下文的调度开销,因此对于线程只会阻塞很短时间的场合是有效的。
  3. 互斥锁(Mutex):是一种用于多线程编程中,防止两条线程同时对同一公共资源(比如全局变量)进行读写的机制。该目的通过将代码切片成一个一个的临界区而达成。
  4. 读写锁(共享-互斥锁):是计算机程序的并发控制的一种同步机制,也称“共享-互斥锁”、多读者-单写者锁) 用于解决多线程对公共资源读写问题。读操作可并发重入,写操作是互斥的。 读写锁通常用互斥锁、条件变量、信号量实现。
  5. 信号量(semaphore):是一种更高级的同步机制,互斥锁可以说是semaphore在仅取值0/1时的特例。信号量可以有更多的取值空间,用来实现更加复杂的同步,而不单单是线程间互斥。
  6. 条件锁:就是条件变量,当进程的某些资源要求不满足时就进入休眠,也就是锁住了。当资源被分配到了,条件锁打开,进程继续运行。

本文主要介绍以下11种锁的使用方式,对原理性质介绍较少,针对每种锁的工作机制,大家可以自行科普。

  • 互斥锁
    • NSLock
    • pthread_mutex
    • @synchronized
  • 自旋锁
    • OSSpinLock
    • os_unfair_lock
  • 读写锁(共享-互斥锁)
    • pthread_rwlock
  • 递归锁
    • NSRecursiveLock
    • pthread_mutex(recursive)
  • 条件锁
    • NSCondition
    • NSConditionLock
  • 信号量
    • dispatch_semaphore
  • 性能对比

互斥锁

NSLock

NSLock:是Foundation框架中以对象形式暴露给开发者的一种锁,(Foundation框架同时提供了NSConditionLock,NSRecursiveLock,NSCondition)NSLock定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
@protocol NSLocking
- (void)lock;
- (void)unlock;
@end
@interface NSLock : NSObject {
@private
void *_priv;
}
- (BOOL)tryLock;
- (BOOL)lockBeforeDate:(NSDate *)limit;
@property (nullable, copy) NSString *name API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
@end

tryLock 和 lock 方法都会请求加锁,唯一不同的是trylock在没有获得锁的时候可以继续做一些任务和处理。lockBeforeDate方法也比较简单,就是在limit时间点之前获得锁,没有拿到返回NO。

实际项目中:NSLock在AFNetworking的AFURLSessionManager.m中应用如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
- (instancetype)initWithSessionConfiguration:(NSURLSessionConfiguration *)configuration {
...
self.lock = [[NSLock alloc] init];
self.lock.name = AFURLSessionManagerLockName;
...
}
- (void)setDelegate:(AFURLSessionManagerTaskDelegate *)delegate
forTask:(NSURLSessionTask *)task
{
...
[self.lock lock];
self.mutableTaskDelegatesKeyedByTaskIdentifier[@(task.taskIdentifier)] = delegate;
[delegate setupProgressForTask:task];
[self addNotificationObserverForTask:task];
[self.lock unlock];
}

pthread_mutex

实际项目中: YYKit的YYMemoryCach中可以看到

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
- (instancetype)init {
...
pthread_mutex_init(&_lock, NULL);
...
}
- (void)_trimToCost:(NSUInteger)costLimit {
BOOL finish = NO;
pthread_mutex_lock(&_lock);
if (costLimit == 0) {
[_lru removeAll];
finish = YES;
} else if (_lru->_totalCost <= costLimit) {
finish = YES;
}
pthread_mutex_unlock(&_lock);
if (finish) return;

NSMutableArray *holder = [NSMutableArray new];
while (!finish) {
if (pthread_mutex_trylock(&_lock) == 0) {
if (_lru->_totalCost > costLimit) {
_YYLinkedMapNode *node = [_lru removeTailNode];
if (node) [holder addObject:node];
} else {
finish = YES;
}
pthread_mutex_unlock(&_lock);
} else {
usleep(10 * 1000); //10 ms
}
}
...
}

@synchronized:

实际项目中:AFNetworking中 isNetworkActivityOccurring属性的getter方法

1
2
3
4
5
- (BOOL)isNetworkActivityOccurring {
@synchronized(self) {
return self.activityCount > 0;
}
}

关于 @synchronized推荐扩展阅读 关于 @synchronized,这儿比你想知道的还要多

自旋锁

OSSpinLock:

1
2
3
4
OSSpinLock lock = OS_SPINLOCK_INIT;
OSSpinLockLock(&lock);
...
OSSpinLockUnlock(&lock);

上面是OSSpinLock使用方式,编译会报警告,已经废弃了,OSSpinLock大家也已经不再用它了,因为它在某一些场景下已经不安全了,可以参考 YY大神的不再安全的 OSSpinLock,在Protocol Buffers项目中你可以看到这样的注释,大家已经用新的方案替换了。

1
2
3
4
5
// NOTE: OSSpinLock may seem like a good fit here but Apple engineers have
// pointed out that they are vulnerable to live locking on iOS in cases of
// priority inversion:
// http://mjtsai.com/blog/2015/12/16/osspinlock-is-unsafe/
// https://lists.swift.org/pipermail/swift-dev/Week-of-Mon-20151214/000372.html

os_unfair_lock:

os_unfair_lock 是苹果官方推荐的替换OSSpinLock的方案,但是它在iOS10.0以上的系统才可以调用。

1
2
3
4
os_unfair_lock_t unfairLock;
unfairLock = &(OS_UNFAIR_LOCK_INIT);
os_unfair_lock_lock(unfairLock);
os_unfair_lock_unlock(unfairLock);

读写锁(共享-互斥锁)

pthread_rwlock:

1
2
3
4
5
6
7
8
//加读锁
pthread_rwlock_rdlock(&rwlock);
//解锁
pthread_rwlock_unlock(&rwlock);
//加写锁
pthread_rwlock_wrlock(&rwlock);
//解锁
pthread_rwlock_unlock(&rwlock);

递归锁

递归锁有一个特点,就是同一个线程可以加锁N次而不会引发死锁。

NSRecursiveLock:

NSRecursiveLock在YYKit中YYWebImageOperation.m中有用到:

1
2
3
4
5
6
7
_lock = [NSRecursiveLock new];
- (void)dealloc {
[_lock lock];
...
...
[_lock unlock];
}

pthread_mutex(recursive):

pthread_mutex锁也支持递归,只需要设置PTHREAD_MUTEX_RECURSIVE即可

1
2
3
4
5
6
7
8
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);

条件锁

NSCondition:

1
2
3
4
5
6
7
8
@interface NSCondition : NSObject  {
@private
void *_priv;
}
- (void)wait;
- (BOOL)waitUntilDate:(NSDate *)limit;
- (void)signal;
- (void)broadcast;

遵循NSLocking协议,使用的时候同样是lock,unlock加解锁,wait是傻等,waitUntilDate:方法是等一会,都会阻塞掉线程,signal是唤起一个在等待的线程,broadcast是广播全部唤起。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
NSCondition *lock = [[NSCondition alloc] init];
//Son 线程
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[lock lock];
while (No Money) {
[lock wait];
}
NSLog(@"The money has been used up.");
[lock unlock];
});

//Father线程
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[lock lock];
NSLog(@"Work hard to make money.");
[lock signal];
[lock unlock];
});

NSConditionLock:

1
2
3
4
5
6
7
8
9
10
11
12
@interface NSConditionLock : NSObject  {
@private
void *_priv;
}
- (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;

信号量

dispatch_semaphore:

dispatch_semaphore在YYKit中的YYThreadSafeArray.m有所应用,YY大神有这样一句注释:

1
2
@discussion Generally, access performance is lower than NSMutableArray, 
but higher than using @synchronized, NSLock, or pthread_mutex_t.
1
2
3
#define LOCK(...) dispatch_semaphore_wait(_lock, DISPATCH_TIME_FOREVER); \
__VA_ARGS__; \
dispatch_semaphore_signal(_lock);

性能对比

参考网上另外一篇文章中所测的性能测试结果

1.png

LockPerformance.jpg

  • 注:运行手机: iphone6s plus ,系统版本:11.2.2,Xcode9.2;数字的单位为ns(得出的具体数值是跑了多次取的均值)。

本人对性能的看法是,在锁使用不太频繁的情况不用太在意这些性能之间的微小差异,在使用锁非常频繁的场景下,需要按照特定场景选择适合该场景的锁,既然这些锁都被一直沿用,肯定是在某些特殊场景下有最适合它发挥的情况。