一.前言
在开发中我们经常会使用多线程,多线程为我们带来了很大便利,提高了程序的执行效率,但同时也带来了data race。
data race 的定义很简单:当至少有两个线程同时访问同一个变量,而且至少其中一个是写操作时,就发生了data race。通过XCode来检测项目中存在的data race。
所以这就要利用一些同步机制来确保数据的准确性,锁就是同步机制中的一种。
二.互斥锁和自选锁的概念
1.临界区段(Critical section): 一个访问共享资源(例如:共享设备或是共享存储器)的程序片段,而这些共享资源有无法同时被多个线程访问的特性。
2.互斥锁(Mutex): 一种用于多线程编程中,防止两条线程同时对同一公共资源进行读写的机制,该目的通过将代码切成一个一个的临界区而达成。当获取失败时,线程会进入睡眠,等待锁释放时被唤醒。互斥锁又分为递归锁和非递归锁。
递归锁:可重入锁,同一个线程在锁释放前,可再次获取锁,即可以递归调用。
非递归锁:不可重入,必须等锁释放后才能再次获取锁。
3.自旋锁(Spinlocks):多线程同步的一种锁,线程反复检查锁变量是否可用。由于线程在这一过程中保持执行,因此是一种忙等待。一旦获取了自旋锁,线程会一直保持该锁,直至显示释放自旋锁。
4.互斥锁和自选锁的区别
互斥锁的起始开销要高于自旋锁,但是基本是一劳永逸,临界区持锁时间的大小并不会对互斥的开销造成影响,不占用cpu资源,而自旋锁是死循环检测,加锁全程消耗cpu,起始开销虽然低于互斥锁,但是随着持锁时间,加锁的开销是线性增长。
5.锁的应用
互斥锁用于临界区持锁时间比较长的操作,比如下面这些情况可以考虑
5.1 临界区IO操作
5.2 临界区代码复杂或者循环量大
5.3 临界区竞争非常激烈
5.4 单核处理器
至于自旋锁就主要用在临界区持锁时间非常短且CPU不紧张的情况下,自旋锁一般用于多核的服务器。
三.互斥锁
os_unfair_lock
os_unfair_lock 用于取代不安全的 OSSpinLock,从 iOS10 开始才支持 从底层调用看,等待 os_unfair_lock 锁的线程会处于休眠状态,并非忙等 需要导入头文件 #import <os/lock.h>。
__block os_unfair_lock lock = OS_UNFAIR_LOCK_INIT;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
os_unfair_lock_lock(&lock);
NSLog(@"第一个线程同步操作开始");
sleep(8);
NSLog(@"第一个线程同步操作结束");
os_unfair_lock_unlock(&lock);
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
os_unfair_lock_lock(&lock);
NSLog(@"第二个线程同步操作开始");
sleep(1);
NSLog(@"第二个线程同步操作结束");
os_unfair_lock_unlock(&lock);
});
pthread_mutex
pthread_mutex 是 C 语言下多线程加互斥锁的方式,那来段 C 风格的示例代码,需要 #import <pthread.h>
int pthread_mutex_init(pthread_mutex_t * __restrict, const pthread_mutexattr_t * __restrict);
首先是第一个方法,这是初始化一个锁,__restrict 为互斥锁的类型,传 NULL 为默认类型,一共有 4 类型。
PTHREAD_MUTEX_NORMAL 缺省类型,也就是普通锁。当一个线程加锁以后,其余请求锁的线程将形成一个等待队列,并在解锁后先进先出原则获得锁。
PTHREAD_MUTEX_ERRORCHECK 检错锁,如果同一个线程请求同一个锁,则返回 EDEADLK,否则与普通锁类型动作相同。这样就保证当不允许多次加锁时不会出现嵌套情况下的死锁。
PTHREAD_MUTEX_RECURSIVE 递归锁,允许同一个线程对同一个锁成功获得多次,并通过多次 unlock 解锁。
PTHREAD_MUTEX_DEFAULT 适应锁,动作最简单的锁类型,仅等待解锁后重新竞争,没有等待队列。
代码示例如下:
__block pthread_mutex_t theLock;
pthread_mutex_init(&theLock, NULL);
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
pthread_mutex_lock(&theLock);
NSLog(@"需要线程同步的操作1 开始");
sleep(3);
NSLog(@"需要线程同步的操作1 结束");
pthread_mutex_unlock(&theLock);
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
sleep(1);
pthread_mutex_lock(&theLock);
NSLog(@"需要线程同步的操作2");
pthread_mutex_unlock(&theLock);
});
通过pthread_mutex方式起到递归锁的效果
__block pthread_mutex_t theLock;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
static void (^RecursiveMethod)(int);
RecursiveMethod = ^(int value) {
pthread_mutex_lock(&theLock);
if (value > 0) {
NSLog(@"value = %d", value);
sleep(1);
RecursiveMethod(value - 1);
}
pthread_mutex_unlock(&theLock);
};
RecursiveMethod(5);
});
注意:
pthread_mutex_trylock 和 tryLock 的区别在于,tryLock 返回的是 YES 和 NO,pthread_mutex_trylock 加锁成功返回的是 0,失败返回的是错误提示码。
NSLock
NSLock:是Foundation框架中以对象形式暴露给开发者的一种锁,(Foundation框架同时提供了NSConditionLock,NSRecursiveLock,NSCondition)NSLock定义如下:
@protocol NSLocking
- (void)lock;
- (void)unlock;
@end
@interface NSLock : NSObject <NSLocking> {
@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
示例代码可参考如下:
//主线程中
NSLock *lock = [[NSLock alloc] init];
//线程1
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[lock lock];
NSLog(@"线程1");
sleep(2);
[lock unlock];
NSLog(@"线程1解锁成功");
});
//线程2
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
sleep(1);//以保证让线程2的代码后执行
[lock lock];
NSLog(@"线程2");
[lock unlock];
});
tryLock 和 lock 方法都会请求加锁,唯一不同的是trylock在没有获得锁的时候可以继续做一些任务和处理。lockBeforeDate方法也比较简单,就是在limit时间点之前获得锁,没有拿到返回NO。
NSLock *lock = [[NSLock alloc] init];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
//[lock lock];
[lock lockBeforeDate:[NSDate date]];
NSLog(@"需要线程同步的操作1 开始");
sleep(2);
NSLog(@"需要线程同步的操作1 结束");
[lock unlock];
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
sleep(1);
if ([lock tryLock]) {//尝试获取锁,如果获取不到返回NO,不会阻塞该线程
NSLog(@"锁可用的操作");
[lock unlock];
}else{
NSLog(@"锁不可用的操作");
}
NSDate *date = [[NSDate alloc] initWithTimeIntervalSinceNow:3];
if ([lock lockBeforeDate:date]) {//尝试在未来的3s内获取锁,并阻塞该线程,如果3s内获取不到恢复线程, 返回NO,不会阻塞该线程
NSLog(@"没有超时,获得锁");
[lock unlock];
}else{
NSLog(@"超时,没有获得锁");
}
});
NSCondition
NSCondition 是一个条件锁, 条件对象同时充当给定线程中的锁和检查点,锁在测试条件并执行条件触发的任务时保护代码。检查点行为要求在线程执行其任务之前条件为 true,当条件不为真时,线程阻塞,它保持阻塞状态,直到另一个线程发出条件对象的信号。
NSCondition的对象实际上作为一个锁和一个线程检查器,锁上之后其他线程也能上锁,而之后根据条件决定是否继续运行线程,即线程是否要进入waiting状态,经测试,NSCondition直接进入waiting状态,当其它线程中的该锁执行signal或者broadcast方法时,线程被唤醒,继续运行之后的方法。
定义如下:
@interface NSCondition : NSObject <NSLocking> {
@private
void *_priv;
}
- (void)wait;
- (BOOL)waitUntilDate:(NSDate *)limit;
- (void)signal;
- (void)broadcast;
注意:
其中 signal 和 broadcast 方法的区别在于,signal 只是一个信号量,只能唤醒一个等待的线程,想唤醒多个就得多次调用,而 broadcast 可以唤醒所有在等待的线程。如果没有等待的线程,这两个方法都没有作用。
NSCondition *lock = [[NSCondition alloc] init];
NSMutableArray *array = [[NSMutableArray alloc] init];
//线程1
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[lock lock];
while (!array.count) {
[lock wait];
NSLog(@"数组为空");
}
[array removeAllObjects];
NSLog(@"array removeAllObjects");
[lock unlock];
});
//线程2
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
sleep(1);//以保证让线程2的代码后执行
[lock lock];
[array addObject:@1];
NSLog(@"array addObject:@1");
[lock signal];
NSLog(@"发送signal");
[lock unlock];
});
NSConditionLock
NSConditionLock 是对 NSCondition 的进一步封装,可以设置具体的条件值对象。NSConditionLock 可以确保只有在满足特定条件时,线程才能获得锁。一旦获得了锁并执行了代码的关键部分,线程就可以放弃锁并将相关条件设置为新的内容。
定义:
@interface NSConditionLock : NSObject <NSLocking> {
@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;
NSConditionLock 可以称为条件锁,只有 condition 参数与初始化时候的 condition 相等,lock 才能正确进行加锁操作。而 unlockWithCondition: 并不是当 Condition 符合条件时才解锁,而是解锁之后,修改 Condition 的值,这个结论可以从下面的例子中得出。
//主线程中
NSConditionLock *lock = [[NSConditionLock alloc] initWithCondition:0];
//线程1
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[lock lockWhenCondition:1];
NSLog(@"线程1");
sleep(2);
[lock unlock];
});
//线程2
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
sleep(1);//以保证让线程2的代码后执行
if ([lock tryLockWhenCondition:0]) {
NSLog(@"线程2");
[lock unlockWithCondition:2];
NSLog(@"线程2解锁成功");
} else {
NSLog(@"线程2尝试加锁失败");
}
});
//线程3
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
sleep(2);//以保证让线程2的代码后执行
if ([lock tryLockWhenCondition:2]) {
NSLog(@"线程3");
[lock unlock]; // [lock unlock]; 解锁成功且不改变 Condition 值。
NSLog(@"线程3解锁成功");
} else {
NSLog(@"线程3尝试加锁失败");
}
});
//线程4
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
sleep(3);//以保证让线程2的代码后执行
if ([lock tryLockWhenCondition:2]) {
NSLog(@"线程4");
[lock unlockWithCondition:1]; // 解锁时将 Condition 改成 1
NSLog(@"线程4解锁成功");
} else {
NSLog(@"线程4尝试加锁失败");
}
});
NSRecursiveLock
NSRecursiveLock是递归锁,它和NSLock的区别在于, NSRecursiveLock可以在一个线程中重复添加锁, NSRecursiveLock会记录上锁和解锁的次数,当二者平衡的时候,才会释放锁,其它线程才可以上锁成功。
定义如下:
@interface NSRecursiveLock : NSObject <NSLocking> {
@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
NSRecursiveLock *recursiveLock = [[NSRecursiveLock alloc] init];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
static void (^RecursiveMethod)(int);
RecursiveMethod = ^(int value) {
[recursiveLock lock];
if (value > 0) {
NSLog(@"value = %d", value);
sleep(1);
RecursiveMethod(value - 1);
}
[recursiveLock unlock];
};
RecursiveMethod(5);
});
// 在给定的时间之前去尝试请求一个锁
- (BOOL)lockBeforeDate:(NSDate *)limit
// 尝试去请求一个锁,并会立即返回一个布尔值,表示尝试是否成功
- (BOOL)tryLock
NSRecursiveLock *lock = [[NSRecursiveLock alloc] init];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
static void (^RecursiveMethod)(int);
RecursiveMethod = ^(int value) {
[lock lock];
if (value > 0) {
NSLog(@"value = %d", value);
sleep(2);
RecursiveMethod(value - 1);
}
[lock unlock];
};
RecursiveMethod(5);
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
sleep(2);
BOOL flag = [lock lockBeforeDate:[NSDate dateWithTimeIntervalSinceNow:1]];
if (flag) {
NSLog(@"lock before date");
[lock unlock];
} else {
NSLog(@"fail to lock before date");
}
});
@synchronized
实际项目中:AFNetworking中
@synchronized (self) {
[_taskDict setObject:downloader forKey:urlString];
}
四.自旋锁
atomic 属性修饰符
一般我们在开发中,大部分属性的声明都会加上 nonatomic, 以提高数据的读取效率(即不使用同步锁),那么为什么属性即使声明为 atomic 依然不能保证线程安全呢?我来到 atomic 的源码中就能发现其中奥秘。
void objc_setProperty_atomic(id self, SEL _cmd, id newValue, ptrdiff_t offset)
{
reallySetProperty(self, _cmd, newValue, offset, true, false, false);
}
static inline void reallySetProperty(id self, SEL _cmd, id newValue, ptrdiff_t offset, bool atomic, bool copy, bool mutableCopy)
{
// ...省略其他修饰符对属性的操作代码
if (!atomic) {
// 不是 atomic 修饰
oldValue = *slot;
*slot = newValue;
} else {
// 如果是 atomic 修饰,加一把同步锁,保证 setter 的安全
spinlock_t& slotlock = PropertyLocks[slot];
slotlock.lock();
oldValue = *slot;
*slot = newValue;
slotlock.unlock();
}
}
id objc_getProperty(id self, SEL _cmd, ptrdiff_t offset, BOOL atomic) {
// ...省略无关紧要的代码
// 非原子属性,直接返回值
if (!atomic) return *slot;
// 原子属性,加同步锁,保证 getter 的安全
spinlock_t& slotlock = PropertyLocks[slot];
slotlock.lock();
id value = objc_retain(*slot);
slotlock.unlock();
}
通过上述源码可以得出:属性在调用 getter 和 setter 方法时,会加上同步锁, 即在属性在调用 getter 和 setter 方法时,保证同一时刻只能有一个线程调用属性的读/写方法。保证了读和写的过程是可靠的,但并不能保证数据一定是可靠的。
举例:定义属性 NSInteger i 是原子的,对 i 进行 i = i + 1; 操作就是不安全的。
该表达式需要三步操作:
1.读取 i 的值存入寄存器;
2.将 i 加 1;
3.修改 i 的值;
如果在第1步完成的时候,i 被其他线程修改了,那么表达式执行的结果就会与预期的不一样,也就是不安全的。
读写锁
读写锁实际是一种的自旋锁,它把对共享资源的访问者划分成读者和写者,读者只对共享资源进行读访问,写者则需要对共享资源进行写操作。这种锁相对于自旋锁而言,能提高并发性,因为在多处理系统中,它允许同时有多个读者来访问共享资源,最大可能的读者数为实际CPU数。
读写锁具有以下特点:
同一时间,只能有一个线程进行写的操作。
同一时间,允许有多个线程进行读的操作。
同一时间,不允许既有写的操作,又有读的操作。
pthread_rwlock_t使用很简单,只需要在读之前使用pthread_rwlock_rdlock,读完解锁pthread_rwlock_unlock,写前pthread_rwlock_wrlock,写入完成之后pthread_rwlock_unlock,任务都执行完了可以选择销毁pthread_rwlock_destroy或者等待下次使用。
读写锁的 API 使用:
// 需要导入头文件
#include <pthread.h>
pthread_rwlock_t lock;
// 初始化锁
pthread_rwlock_init(&lock, NULL);
// 读-加锁
pthread_rwlock_rdlock(&lock);
// 读-尝试加锁
pthread_rwlock_tryrdlock(&lock);
// 写-加锁
pthread_rwlock_wrlock(&lock);
// 写-尝试加锁
pthread_rwlock_trywrlock(&lock);
// 解锁
pthread_rwlock_unlock(&lock);
// 销毁
pthread_rwlock_destroy(&lock);
- (void)initReadWriteLock {
//初始化读写锁
pthread_rwlock_init(&_rwlock, NULL);
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
for (NSInteger i = 0; i < 5; i ++) {
dispatch_async(queue, ^{
[[[NSThread alloc]initWithTarget:self selector:@selector(readPthreadRWLock) object:nil]start];
[[[NSThread alloc]initWithTarget:self selector:@selector(writePthreadRWLock) object:nil]start];
});
}
}
- (void)readPthreadRWLock {
// NSLog(@"readPthreadRWLock -->%@", [NSThread currentThread]);
pthread_rwlock_rdlock(&_rwlock);
NSLog(@"读文件");
sleep(10);
pthread_rwlock_unlock(&_rwlock);
}
- (void)writePthreadRWLock {
// NSLog(@"writePthreadRWLock -->%@", [NSThread currentThread]);
pthread_rwlock_wrlock(&_rwlock);
NSLog(@" 写入文件");
sleep(1);
pthread_rwlock_unlock(&_rwlock);
}
- (void)dealloc {
pthread_rwlock_destroy(&_rwlock);//销毁锁
}