线程锁
线程锁
是为了解决多个线程之间共享同一资源时,对资源的占用控制,防止多个线程之间同时修改同一资源信息,导致不可预知的问题。
锁的实现方式大致可以分为以下两种:
- 阻塞
- 忙等
阻塞:如果锁对象被其他线程所持有,那么请求访问的线程就会被加入到等待队列中,因而被阻塞。这就意味着被阻塞的线程放弃了时间片,调度器会将CPU让给下一个执行的的线程。当锁可用的时候,调度器会得到通知,然后根据情况将线程从等待队列取出来,并重新调度。
OSSpinLock(自旋锁)
#import <libkern/OSAtomic.h> @interface DrLock : NSObject { OSSpinLock _lock; } - (void)lock; - (int)tryLock; - (void)unlock; @end @implementation DrLock - (instancetype)init{ self = [super init]; if (self) { _lock = OS_SPINLOCK_INIT; } return self; } /// 上锁 - (void)lock { OSSpinLockLock(&_lock); } /// 上锁,失败返回:NO,成功返回:YES - (int)tryLock { return OSSpinLockTry(&_lock); } /// 解锁 - (void)unlock { OSSpinLockUnlock(&_lock); } @end
关于tryLock的使用,当返回NO时,表示当前锁已经被锁住(即:有一个线程在访问边界值时被当前锁锁住了),此时我们需要调用lock将新的线程锁住。因此我们可以通过这个方法来判断当前是否存在多个线程同时访问边界值。
os_unfair_lock(互斥锁)
#import <os/lock.h> @interface DrLock : NSObject { os_unfair_lock _lock; } - (void)lock; - (int)tryLock; - (void)unlock; @end @implementation DrLock - (instancetype)init{ self = [super init]; if (self) { _lock = OS_UNFAIR_LOCK_INIT; } return self; } /// 上锁 - (void)lock { os_unfair_lock_lock(&_lock); } /// 上锁,失败返回:NO,成功返回:YES - (int)tryLock { return os_unfair_lock_trylock(&_lock); } /// 解锁 - (void)unlock { os_unfair_lock_unlock(&_lock); } @end
关于tryLock的使用,当返回NO时,表示当前锁已经被锁住(即:有一个线程在访问边界值时被当前锁锁住了),此时我们需要调用lock将新的线程锁住。因此我们可以通过这个方法来判断当前是否存在多个线程同时访问边界值。
dispatch_semaphore_t(信号量)
@interface DrLock : NSObject { dispatch_semaphore_t _lock; } - (void)lock; - (void)unlock; @end @implementation DrLock - (instancetype)init{ self = [super init]; if (self) { _lock = dispatch_semaphore_create(1); } return self; } /// 上锁 - (void)lock { dispatch_semaphore_wait(_lock, DISPATCH_TIME_FOREVER); } /// 解锁 - (void)unlock { dispatch_semaphore_signal(_lock); } @end
pthread_mutex(互斥锁)
PTHREAD_MUTEX_NORMAL
、PTHREAD_MUTEX_ERRORCHECK
、PTHREAD_MUTEX_RECURSIVE,一般就使用PTHREAD_MUTEX_NORMAL就可以了。
#import <pthread.h> @interface DrLock : NSObject { pthread_mutex_t _lock; } - (void)lock; - (int)tryLock; - (void)unlock; @end @implementation DrLock - (void)dealloc{ pthread_mutex_destroy(&_lock); // 注意这里要销毁锁 } - (instancetype)init{ self = [super init]; if (self) { // pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;// 静态初始化方法 // _lock = lock; // 动态初始化 pthread_mutexattr_t attr; pthread_mutexattr_init(&attr); pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_NORMAL); // 设置锁的类型 pthread_mutex_init(&_lock, &attr); pthread_mutexattr_destroy(&attr); } return self; } /// 上锁 - (void)lock { pthread_mutex_lock(&_lock); } /// 上锁,返回值==0:成功;返回值!=0:失败(当互斥量已经被锁住时调用该函数将返回错误代码EBUSY:16,此时需要调用lock对当前线程上锁) - (int)tryLock { return pthread_mutex_trylock(&_lock); } /// 解锁 - (void)unlock { pthread_mutex_unlock(&_lock); } @end
关于tryLock的使用,当返回非0的值时,表示当前锁已经被锁住(即:有一个线程在访问边界值时被当前锁锁住了),此时我们需要调用lock将新的线程锁住。因此我们可以通过这个方法来判断当前是否存在多个线程同时访问边界值。
- (void)task:(int)ID { int res = [_lock tryLock]; if (res != 0) { NSLog(@"加锁失败: %@", @(res)); [_lock lock]; } NSLog(@"执行任务:%@", @(ID)); sleep(2); NSLog(@"执行任务:%@完成", @(ID)); [_lock unlock]; }
pthread_mutex(递归锁)
它与互斥锁类似,都是采用阻塞当前线程的方式实现加锁功能,区别在于它可以保证同一个线程可以多次进入同一块加锁区域一般情况下。一个线程只能申请一次锁,也只能在获得锁的情况下才能释放锁,多次申请锁或释放未获得的锁都会导致崩溃。假设在已经获得锁的情况下再次申请锁,线程会因为等待锁的释放而进入睡眠状态,因此就不可能再释放锁,从而导致死锁。
然而这种情况经常会发生,比如某个函数申请了锁,在临界区内又递归调用了自己(出现于递归调用的情况)。因此我们需要一个递归锁解决这种情况的调用。
#import <pthread.h> @interface DrLock : NSObject { pthread_mutex_t _lock; } - (void)lock; - (int)tryLock; - (void)unlock; @end @implementation DrLock - (void)dealloc{ pthread_mutex_destroy(&_lock); // 注意这里要销毁锁 } - (instancetype)init{ self = [super init]; if (self) { pthread_mutexattr_t attr; pthread_mutexattr_init(&attr); pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE); // 这里设置的类型为递归锁 pthread_mutex_init(&_lock, &attr); pthread_mutexattr_destroy(&attr); } return self; } /// 上锁 - (void)lock { pthread_mutex_lock(&_lock); } /// 上锁,返回值==0:成功;返回值!=0:失败(当互斥量已经被锁住时调用该函数将返回错误代码EBUSY:16,此时需要调用lock对当前线程上锁) - (int)tryLock { return pthread_mutex_trylock(&_lock); } /// 解锁 - (void)unlock { pthread_mutex_unlock(&_lock); } @end
递归锁与互斥锁的区别,仅仅在于设置的类型不一样,递归锁的类型为:PTHREAD_MUTEX_RECURSIVE。下面我们举个例子,介绍一下这个递归锁的用法。
- (void)task:(int)ID count:(int)count { int i = [_lock tryLock]; if (i != 0) { NSLog(@"加锁失败: %@", @(i)); [_lock lock]; } if (count == 0) { NSLog(@"执行任务:%@完成", @(ID)); sleep(1); [_lock unlock]; return; } NSLog(@"执行任务:%@,倒计时:%@", @(ID), @(count)); [self task:ID count:count-1]; [_lock unlock]; }
需要注意的一点,lock与unlock一定要成对执行,即递归前后,需要保持lock与unlock的调用次数一致,否则会出现未解锁的情况,导致线程一直处于阻塞状态,后续的任务无法进行。
当然递归锁也可以用于非递归的调用方法中,只要确保lock与unlock调用次数一致即可。而在递归调用的方法中我们必须要使用递归锁了。
NSLock(互斥锁)
- (BOOL)lockBeforeDate:(NSDate *)limit;
- (void)task:(int)ID { if (![_lock lockBeforeDate:[NSDate dateWithTimeIntervalSinceNow:2]]) { NSLog(@"加锁失败"); [_lock lock]; } NSLog(@"执行任务:%@", @(ID)); sleep(2); NSLog(@"执行任务:%@完成", @(ID)); [_lock unlock]; }
以上任务会执行2秒钟,我们在加锁的地方采用lockBeforeDate方法,设置一个锁的超时时间2秒,当2秒后未收到unlock,该方法将不再阻塞线程,返回NO,因此我们增加了一个lock方法,防止多个线程同时调用该方法,导致lockBeforeDate超过2秒。
以上代码一旦同时存在两个或以上线程同时调用,就会出现lockBeforeDate超时,导致加锁失败。
NSRecursiveLock(递归锁)
- (BOOL)lockBeforeDate:(NSDate *)limit;
NSCondition(条件锁)
- (void)wait; // 阻塞线程 - (BOOL)waitUntilDate:(NSDate *)limit; // 阻塞线程,并设置一个超时时间 - (void)signal; // 唤醒一个阻塞中的线程 - (void)broadcast; // 唤醒全部阻塞中的线程
该锁适用于生产者与消费者模式,例如:一个公司负责生产商品,一些消费者负责购买这些商品
/// 生产商品 - (void)production:(NSString *)goodsName { [_condition lock]; [_goodsList addObject:goodsName]; [_condition unlock]; // [_condition signal]; // 通知一个正在排队的消费者,有货了,可以购买了 [_condition broadcast]; // 通知所有正在排队的消费者,有货了,可以购买了 } /// 购买商品 - (void)buy { [_condition lock]; while (_goodsList.count == 0) { NSLog(@"%@ 正在排队......", [NSThread currentThread].name); // 没商品了,排队等待 // [_condition wait];// 这里会让当前线程阻塞,并休眠,直到发出signal或broadcast被唤醒,继续向下执行 // if (![_condition waitUntilDate:[NSDate dateWithTimeIntervalSinceNow:6]]) { // 最多等6秒,我就不等了 // break; // 等待超时waitUntilDate会返回NO,而通过signal或broadcast唤醒,它会返回YES。因此如果是超时了,那就break跳出循环,向下执行 // } } if (_goodsList.count == 0) { NSLog(@"%@ 没有买到商品,回家了......", [NSThread currentThread].name); }else { NSString *goods = [_goodsList lastObject]; [_goodsList removeLastObject]; NSLog(@"%@ 买到了 %@", [NSThread currentThread].name, goods); } [_condition unlock]; } // 消费者购买商品 NSThread *thread1 = [[NSThread alloc] initWithTarget:self selector:@selector(buy) object:nil]; thread1.name = @"jack"; [thread1 start]; NSThread *thread2 = [[NSThread alloc] initWithTarget:self selector:@selector(buy) object:nil]; thread2.name = @"bob"; [thread2 start]; NSThread *thread3 = [[NSThread alloc] initWithTarget:self selector:@selector(buy) object:nil]; thread3.name = @"drbox"; [thread3 start]; // 工人生产商品 dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ dispatch_queue_t queue = dispatch_queue_create("workers1", DISPATCH_QUEUE_SERIAL); dispatch_async(queue, ^{ [self production:@"苹果iPhoneX 编号:1001"]; }); }); dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(10 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ dispatch_queue_t queue = dispatch_queue_create("workers2", DISPATCH_QUEUE_SERIAL); dispatch_async(queue, ^{ [self production:@"苹果iPhoneX 编号:1002"]; }); }); dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ dispatch_queue_t queue = dispatch_queue_create("workers3", DISPATCH_QUEUE_SERIAL); dispatch_async(queue, ^{ [self production:@"苹果iPhoneX 编号:1003"]; }); });
以上为典型的生产者与消费者模式,当生产者生产出一件商品,通知一个或全部排队中的消费者购买。注意,这里有两点注意事项:
- 由于生产商品为多线程,因此需要对goodsList操作前加锁。
- 通知消费者时调用signal与broadcast的不同,signal只会唤起一个休眠中的线程,而broadcast则会唤起全部休眠中的线程,因此我们在buy方法中增加了一个while循环来判断,防止broadcast唤起全部休眠的线程,导致有些线程错误的处理产品是否为空的情况
pthread_mutex_t + pthread_cond_t(实现条件锁)
#import <pthread.h> @interface DrLock : NSObject { pthread_mutex_t _lock; pthread_cond_t _cond; // 条件 } - (void)lock; - (void)unlock; @end @implementation DrLock - (void)dealloc{ pthread_mutex_destroy(&_lock); // 注意这里要销毁锁 pthread_cond_destroy(&_cond); } - (instancetype)init{ self = [super init]; if (self) { 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_condattr_t conAttr; pthread_condattr_init(&conAttr); pthread_condattr_setpshared(&conAttr, PTHREAD_PROCESS_PRIVATE); pthread_cond_init(&_cond, &conAttr); } return self; } /// 上锁 - (void)lock { pthread_mutex_lock(&_lock); } /// 解锁 - (void)unlock { pthread_mutex_unlock(&_lock); } /// 阻塞当前线程 - (void)wait { pthread_cond_wait(&_cond, &_lock); } /// 阻塞当前线程 - (BOOL)waitUntilDate:(NSDate *)date { struct timespec time; time.tv_sec = date.timeIntervalSince1970; time.tv_nsec = 0; // 这里必须设置一个数,否则计时不起作用,具体设置多少我不知道这里如何设置,有清楚的可以回复我,谢谢! return pthread_cond_timedwait(&_cond, &_lock, &time) == 0; } /// 唤醒一个被阻塞的线程 - (void)signal { pthread_cond_signal(&_cond); } /// 唤醒全部阻塞中的线程 - (void)broadcast { pthread_cond_broadcast(&_cond); } @end
这里我们用到了一个pthread_cond_t条件(记得需要手动释放资源),它在初始化时,需要设置一个属性值,这里我们设置成PTHREAD_PROCESS_PRIVATE,实际上这个也是缺省值。
关于pthread_condattr_setpshared的值范围有以下说明:
因为pthread_mutex属于 POSIX 线程下的互斥锁,因此它的取值包括:PTHREAD_PROCESS_SHARED和PTHREAD_PROCESS_PRIVATE
NSConditionLock(升级版的条件锁)
/// 生产商品 - (void)production:(NSString *)goodsName { [_conditionLock lockWhenCondition:1]; // 只有当前锁的条件为1时,当前线程才可以获得该锁来访问资源,否则会被阻塞。 [_goodsList addObject:goodsName]; [_conditionLock unlockWithCondition:2]; // 解除锁,并设置锁的条件为2 } /// 购买商品 - (void)buy { NSLog(@"%@ 排队中......", [NSThread currentThread].name); // [_conditionLock lockWhenCondition:2]; // 只有当前锁的条件为2时,当前线程才可以获得该锁来访问资源,否则会被阻塞。 // 我们还可以通过下面方法,来指定一个等待时间,当时间到了,如果当前线程依然没有获得锁,该线程会自动被唤起 [_conditionLock lockWhenCondition:2 beforeDate:[NSDate dateWithTimeIntervalSinceNow:6]]; if (_goodsList.count == 0) { NSLog(@"%@ 没有买到商品,回家了......", [NSThread currentThread].name); }else { NSString *goods = [_goodsList lastObject]; [_goodsList removeLastObject]; NSLog(@"%@ 买到了 %@", [NSThread currentThread].name, goods); } [_conditionLock unlockWithCondition:1]; // 解锁,并设置锁的条件为1,目的是将获取锁的权限交给了生产商品的线程 }
我们在初始化条件锁的时候,设置了一个条件值为1
if (!_conditionLock) { _conditionLock = [[NSConditionLock alloc] initWithCondition:1]; }
从这个例子可知,我们可以通过设置一个条件值,来控制哪个线程可以获得锁,从而获取资源。这样一来,我们就可以通过这个条件值来保持多个线程的执行顺序了。
下面是该条件锁的api方法,如下:
- (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; - (void)lock; - (void)unlock;
我们需要注意的是,lock与unlock只是对当前线程进行的加锁与解锁,并不受条件值的影响。因此我们在使用lockWhenCondition加锁的时候,如果调用unlock解锁,那么只会解锁当前线程,并不会改变条件值,此时你需要再次调用unlockWithCondition来改变条件值,在实际使用中我想lockWhenCondition与unlockWithCondition应该是成对使用的,除非你有特殊的需求。
@synchronized(递归锁)
这个锁我们在开发中也经常看到,也实际使用过,实际上它只是一种语法糖,底层依然是我们上面用到的锁。
我们知道 @synchronized 后面需要紧跟一个 OC 对象,它实际上是把这个对象当做锁来使用。这是通过一个哈希表来实现的,OC 在底层使用了一个递归锁的数组(你可以理解为锁池),通过对对象取哈希值来得到对应的递归锁。(注意:有些文章中提到的这里是一个互斥锁,实际上是错误的,它是采用pthread_mutex实现的递归锁,为啥是递归锁,我们在实际使用中也可以发现,@synchronized是可以嵌套的,嵌套中的OC对象可以是相同的,既然是相同的,那么其对应的锁肯定也是一样的,我们知道,同一个锁不能被同一个线程捕获,而只有递归锁允许)
其内部实现实则调用如下两个函数:
// 加锁 func objc_sync_enter(_ obj: Any) -> Int32 // 解锁 func objc_sync_exit(_ obj: Any) -> Int32
Atomic(原子操作)
狭义上的原子操作表示一条不可打断的操作,也就是说线程在执行操作过程中,不会被操作系统挂起,而是一定会执行完。在单处理器环境下,一条汇编指令显然是原子操作,因为中断也要通过指令来实现。
然而在多处理器的情况下,能够被多个处理器同时执行的操作任然算不上原子操作。因此,真正的原子操作必须由硬件提供支持,比如 x86 平台上如果在指令前面加上 “LOCK” 前缀,对应的机器码在执行时会把总线锁住,使得其他 CPU不能再执行相同操作,从而从硬件层面确保了操作的原子性。
我们在iOS开发中经常使用的@property (atomic, assign)声明属性,其中atomic就是指定当前属性为原子操作。
还有#import <libkern/OSAtomic.h>这个库中的函数,也是原子操作的,如下:
int32_t res = OSAtomicIncrement32(&_count); // 对count进行原子+1的操作,返回操作后的结果
不过这个在iOS10之后被废弃了,改用#import<stdatomic.h>下的如下调用(具体可以看这里):
@property (nonatomic, assign) _Atomic(int) count; int32_t res = atomic_fetch_add(&_count, 2); // 对count进行原子+2的操作,返回操作前的结果
pthread_rwlock_t(读写锁)
- 读模式下加锁状态(读锁)
- 写模式下加锁状态(写锁)
- 不加锁状态
#import <pthread.h> @interface DrLock : NSObject { pthread_rwlock_t _lock; } - (void)rdLock; - (int)tryRdLock; - (void)wrLock; - (int)tryWrLock; - (void)unlock; @end @implementation DrLock - (void)dealloc{ pthread_rwlock_destroy(&_lock); } - (instancetype)init{ self = [super init]; if (self) { // pthread_rwlock_t lock = PTHREAD_RWLOCK_INITIALIZER; // 静态初始化方法 // _lock = lock; pthread_rwlockattr_t attr; pthread_rwlockattr_init(&attr); pthread_rwlockattr_setpshared(&attr, PTHREAD_PROCESS_PRIVATE); // 这里设置为仅适用于当前进程所创建的线程 pthread_rwlock_init(&_lock, &attr); pthread_rwlockattr_destroy(&attr); } return self; } /// 读模式加锁,当前线程无法获取锁时,将会阻塞当前线程 - (void)rdLock{ pthread_rwlock_rdlock(&_lock); }
/// 读模式锁,当前线程无法获得锁时,不会阻塞当前线程,直接返回EBUSY - (int)tryRdLock{ return pthread_rwlock_tryrdlock(&_lock); } /// 写模式加锁,当前线程无法获取锁时,将会阻塞当前线程 - (void)wrLock{ pthread_rwlock_wrlock(&_lock); }
/// 写模式锁,当前线程无法获得锁时,不会阻塞当前线程,直接返回EBUSY - (int)tryWrLock{ return pthread_rwlock_trywrlock(&_lock); } /// 解锁 - (void)unlock{ pthread_rwlock_unlock(&_lock); } @end
当一个线程拥有了读锁时,执行读操作,此时仍然可以有多个线程拥有读锁。但是此时一个线程尝试拥有写锁时,这个线程将被阻塞,直到读锁被解锁为止。
当一个线程拥有写锁时,执行写操作,此时其他线程将不能获得写锁,并且其他线程也不能拥有读锁。
这就是写独占、读共享。
从上面的api中我们可以看到读锁和写锁拥有两种方法,一个是带try的,一个是不带try的。带try的方法在不能获得锁的情况下,不会阻塞当前线程,而是立刻返回一个EBUSY错误。而不带try的方法,在当前线程无法获得锁时,将会阻塞当前线程,直到其他线程解了锁。
dispatch_barrier_async(GCD栅栏实现读写锁)
// 并行队列 dispatch_queue_t queue = dispatch_queue_create("CONCURRENT", DISPATCH_QUEUE_CONCURRENT); dispatch_async(queue, ^{ NSLog(@"执行任务1"); sleep(1); }); dispatch_async(queue, ^{ NSLog(@"执行任务2"); sleep(1); }); dispatch_barrier_async(queue, ^{ // 一个异步栅栏,它不会阻塞当前线程 NSLog(@"执行栅栏操作1"); sleep(1); }); dispatch_barrier_sync(queue, ^{ // 一个同步栅栏,它会阻塞当前线程 NSLog(@"执行栅栏操作2"); sleep(1); }); dispatch_async(queue, ^{ NSLog(@"执行任务3"); sleep(1); }); dispatch_async(queue, ^{ NSLog(@"执行任务4"); sleep(1); }); NSLog(@"代码执行完毕");
输入结果如下:
执行任务1 执行任务2 执行栅栏操作1 执行栅栏操作2 代码执行完毕 // 这里被同步栅栏阻塞了,因此在同步栅栏之后执行的 执行任务3 执行任务4
经过上面的事例,我们已经对栅栏有了了解,接下来我们举个实际应用:
@interface MessageManager : NSObject{ NSMutableArray *_msgList; dispatch_queue_t _queue; } - (NSString *)getMessage; - (void)setMessage:(NSString *)msg; @end @implementation MessageManager - (instancetype)init { self = [super init]; if (self) { _msgList = [@[] mutableCopy]; _queue = dispatch_queue_create("lock", DISPATCH_QUEUE_CONCURRENT); // 为了提高读的效率,这里采用并行队列 } return self; } - (NSString *)getMessage{ __block NSString *msg = nil; dispatch_sync(_queue, ^{ // 采用同步读操作 msg = [self -> _msgList componentsJoinedByString:@", "]; }); return msg; } - (void)setMessage:(NSString *)msg{ dispatch_barrier_async(_queue, ^{ // 采用异步栅栏,执行写操作 [self -> _msgList addObject:msg]; }); } @end
上面是一个利用GCD栅栏实现的一个读写消息的操作
- 为了提高读的效率,我们采用并行队列实现。
- 读消息是立即返回的,因此采用队列的同步操作。
- 写消息可能会花费一定时间,因此我们采用异步栅栏,这样不会阻塞当前线程
那么除了GCD的栅栏可以实现读写锁,可不可以采用GCD的group实现呢?答案是不可以。我们要清楚一点,读写锁的特点是什么?写独占,读共享,也就是多读少写。明白了这一点,我们就会知道为什么不能用group实现了。group是采用dispatch_group_wait与dispatch_group_leave来实现同步操作的,这也就导致不能实现写独占,读共享了。