自旋锁和互斥锁
共同点:都能保证同一时刻只能有一个线程操作锁住的代码。都能保证线程安全。
不同点:
- 互斥锁(mutex):当上一个线程的任务没有执行完毕的时候(被锁住),那么下一个线程会进入睡眠状态等待任务执行完毕(sleep-waiting),当上一个线程的任务执行完毕,下一个线程会自动唤醒然后执行任务。
- 自旋锁(Spin lock):当上一个线程的任务没有执行完毕的时候(被锁住),那么下一个线程会一直等待(busy-waiting),当上一个线程的任务执行完毕,下一个线程会立即执行。
- 由于自旋锁不会引起调用者睡眠,所以自旋锁的效率远高于互斥锁
- 自旋锁会一直占用CPU,也可能会造成死锁
原子操作
nonatomic:非原子属性,非线程安全,适合小内存移动设备
atomic:原子属性,default,线程安全(内部使用自旋锁),消耗大量资源,单写多读,只为setter加锁,不影响getter方法
static inline void reallySetProperty(id self, SEL _cmd, id newValue, ptrdiff_t offset, bool atomic, bool copy, bool mutableCopy)
{
if (offset == 0) {
object_setClass(self, newValue);
return;
}
id oldValue;
id *slot = (id*) ((char*)self + offset);
if (copy) {
newValue = [newValue copyWithZone:nil];
} else if (mutableCopy) {
newValue = [newValue mutableCopyWithZone:nil];
} else {
if (*slot == newValue) return;
newValue = objc_retain(newValue);
}
if (!atomic) {
oldValue = *slot;
*slot = newValue;
} else {
spinlock_t& slotlock = PropertyLocks[slot];
slotlock.lock();
oldValue = *slot;
*slot = newValue;
slotlock.unlock();
}
objc_release(oldValue);
}
void objc_setProperty(id self, SEL _cmd, ptrdiff_t offset, id newValue, BOOL atomic, signed char shouldCopy)
{
bool copy = (shouldCopy && shouldCopy != MUTABLE_COPY);
bool mutableCopy = (shouldCopy == MUTABLE_COPY);
reallySetProperty(self, _cmd, newValue, offset, atomic, copy, mutableCopy);
}
- @synchronized 关键字加锁
@synchronized是iOS中最常见的锁,也是性能最差的一种,具体用法如下:
- (void)synchronized {
NSObject * cjobj = [NSObject new];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
@synchronized(cjobj){
NSLog(@"线程1开始");
sleep(3);
NSLog(@"线程1结束");
}
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
sleep(1);
@synchronized(cjobj){
NSLog(@"线程2");
}
});
}
线程2会等待线程1执行完以后才会执行,这种锁在使用时要确保该锁的唯一标识符是相同的才行,即为同一个对象即可,如上述代码中的cjobj;
优点:就是我们不需要在代码中显式的创建锁对象,便可以实现锁的机制,但作为一种预防措施,@synchronized 块会隐式的添加一个异常处理例程来保护代码,该处理例程会在异常抛出的时候自动的释放互斥锁。
缺点:隐式的异常处理例程带来额外的开销,造成性能差。
2、NSLock(互斥锁)
iOS中NSLock类的.h文件,从代码中可以看出,该类分成了几个子类:NSLock、NSConditionLock、NSRecursiveLock、NSCondition,然后有一个 NSLocking 协议:
@protocol NSLocking
- (void)lock;
- (void)unlock;
@end
虽然 NSLock、NSConditionLock、NSRecursiveLock、NSCondition 都遵循的了 NSLocking 协议,但是它们并不相同。
2.1 NSLock 对象锁
NSLock实现了最基本的锁,遵循NSLoaking协议,通过lock和unlock来进行加锁和解锁
源码内容:
@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
使用方法如下:
- (void)lockTest {
NSLock *lock = [[NSLock alloc]init];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[lock lock];
NSLog(@"线程1加锁成功");
sleep(2);
[lock unlock];
NSLog(@"线程1解锁成功");
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[lock lock];
NSLog(@"线程2加锁成功");
sleep(2);
[lock unlock];
NSLog(@"线程2解锁成功");
});
}
除 lock 和 unlock 方法外,NSLock 还提供了 tryLock 和 lockBeforeDate:两个方法,这两个方法我就不给大家演示了,感兴趣的可以自己演示下,我把方法对应的作用高速大家。
tryLock 并不会阻塞线程,[cjlock tryLock] 能加锁返回 YES,不能加锁返回 NO,然后都会执行后续代码。
这里顺便提一下 trylock 和 lock 使用场景:当前线程锁失败,也可以继续其它任务,用 trylock 合适;当前线程只有锁成功后,才会做一些有意义的工作,那就 lock,没必要轮询 trylock。以下的锁都是这样。
lockBeforeDate: 方法会在所指定 Date 之前尝试加锁,会阻塞线程,如果在指定时间之前都不能加锁,则返回 NO,指定时间之前能加锁,则返回 YES。
由于是互斥锁,当一个线程进行访问的时候,该线程获得锁,其他线程进行访问的时候,将被操作系统挂起,直到该线程释放锁,其他线程才能对其进行访问,从而却确保了线程安全。但是如果连续锁定两次,则会造成死锁问题。
2.2、 NSRecursiveLock 递归锁
NSRecursiveLock是递归锁,可以被一个线程多次获得,而不会引起死锁。它记录了成功获得锁的次数,每一次成功的获得锁,必须有一个配套的释放锁和其对应,这样才不会引起死锁。NSRecursiveLock 会记录上锁和解锁的次数,当二者平衡的时候,才会释放锁,其它线程才可以上锁成功。
源码内容:
@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
使用方法如下:
- (void)nsrecursivelock{
NSRecursiveLock * cjlock = [[NSRecursiveLock alloc] init];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
static void (^RecursiveBlock)(int);
RecursiveBlock = ^(int value) {
[cjlock lock];
NSLog(@"%d加锁成功",value);
if (value > 0) {
NSLog(@"value:%d", value);
RecursiveBlock(value - 1);
}
[cjlock unlock];
NSLog(@"%d解锁成功",value);
};
RecursiveBlock(3);
});
}
2019-03-10 16:42:51.487615+0800 AllSuoUser[45654:1840908] 3加锁成功
2019-03-10 16:42:51.487773+0800 AllSuoUser[45654:1840908] value:3
2019-03-10 16:42:51.487866+0800 AllSuoUser[45654:1840908] 2加锁成功
2019-03-10 16:42:51.487963+0800 AllSuoUser[45654:1840908] value:2
2019-03-10 16:42:51.488056+0800 AllSuoUser[45654:1840908] 1加锁成功
2019-03-10 16:42:51.488157+0800 AllSuoUser[45654:1840908] value:1
2019-03-10 16:42:51.488256+0800 AllSuoUser[45654:1840908] 0加锁成功
2019-03-10 16:42:51.488350+0800 AllSuoUser[45654:1840908] 0解锁成功
2019-03-10 16:42:51.488468+0800 AllSuoUser[45654:1840908] 1解锁成功
2019-03-10 16:42:51.488577+0800 AllSuoUser[45654:1840908] 2解锁成功
2019-03-10 16:42:51.488681+0800 AllSuoUser[45654:1840908] 3解锁成功
由以上内容总结:
如果用 NSLock 的话,cjlock 先锁上了,但未执行解锁的时候,就会进入递归的下一层,而再次请求上锁,阻塞了该线程,线程被阻塞了,自然后面的解锁代码不会执行,而形成了死锁。而 NSRecursiveLock 递归锁就是为了解决这个问题。
2.3、NSConditionLock
NSConditionLock 对象所定义的互斥锁可以在使得在某个条件下进行锁定和解锁,它和 NSLock 类似,都遵循 NSLocking 协议,方法都类似,只是多了一个 condition 属性,以及每个操作都多了一个关于 condition 属性的方法,例如 tryLock、tryLockWhenCondition:,所以 NSConditionLock 可以称为条件锁。
- 只有 condition 参数与初始化时候的 condition 相等,lock 才能正确进行加锁操作。
- unlockWithCondition: 并不是当 condition 符合条件时才解锁,而是解锁之后,修改 condition 的值。
源码内容:
@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;
@property (nullable, copy) NSString *name API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
@end
使用方法介绍:
- (void)nsconditionlock {
NSConditionLock * cjlock = [[NSConditionLock alloc] initWithCondition:0];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[cjlock lock];
NSLog(@"线程1加锁成功");
sleep(1);
[cjlock unlock];
NSLog(@"线程1解锁成功");
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
sleep(1);
[cjlock lockWhenCondition:1];
NSLog(@"线程2加锁成功");
[cjlock unlock];
NSLog(@"线程2解锁成功");
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
sleep(2);
if ([cjlock tryLockWhenCondition:0]) {
NSLog(@"线程3加锁成功");
sleep(2);
[cjlock unlockWithCondition:2];
NSLog(@"线程3解锁成功");
} else {
NSLog(@"线程3尝试加锁失败");
}
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
if ([cjlock lockWhenCondition:2 beforeDate:[NSDate dateWithTimeIntervalSinceNow:10]]) {
NSLog(@"线程4加锁成功");
[cjlock unlockWithCondition:1];
NSLog(@"线程4解锁成功");
} else {
NSLog(@"线程4尝试加锁失败");
}
});
}
2017-10-19 15:09:44.010992+0800 Thread-Lock[39230:853946] 线程1加锁成功
2017-10-19 15:09:45.012045+0800 Thread-Lock[39230:853946] 线程1解锁成功
2017-10-19 15:09:46.012692+0800 Thread-Lock[39230:853947] 线程3加锁成功
2017-10-19 15:09:48.016536+0800 Thread-Lock[39230:853947] 线程3解锁成功
2017-10-19 15:09:48.016564+0800 Thread-Lock[39230:853944] 线程4加锁成功
2017-10-19 15:09:48.017039+0800 Thread-Lock[39230:853944] 线程4解锁成功
2017-10-19 15:09:48.017040+0800 Thread-Lock[39230:853945] 线程2加锁成功
2017-10-19 15:09:48.017215+0800 Thread-Lock[39230:853945] 线程2解锁成功
由以上内容总结:
- 在线程 1 解锁成功之后,线程 2 并没有加锁成功,而是继续等了 1 秒之后线程 3 加锁成功,这是因为线程 2 的加锁条件不满足,初始化时候的 condition 参数为 0,而线程 2
- 加锁条件是 condition 为 1,所以线程 2 加锁失败。
- lockWhenCondition 与 lock 方法类似,加锁失败会阻塞线程,所以线程 2 会被阻塞着。
- tryLockWhenCondition: 方法就算条件不满足,也会返回 NO,不会阻塞当前线程。
- lockWhenCondition:beforeDate:方法会在约定的时间内一直等待 condition 变为 2,并阻塞当前线程,直到超时后返回 NO。
- 锁定和解锁的调用可以随意组合,也就是说 lock、lockWhenCondition:与unlock、unlockWithCondition: 是可以按照自己的需求随意组合的。
2.4、 NSCondition
NSCondition 是一种特殊类型的锁,通过它可以实现不同线程的调度。一个线程被某一个条件所阻塞,直到另一个线程满足该条件从而发送信号给该线程使得该线程可以正确的执行。比如说,你可以开启一个线程下载图片,一个线程处理图片。这样的话,需要处理图片的线程由于没有图片会阻塞,当下载线程下载完成之后,则满足了需要处理图片的线程的需求,这样可以给定一个信号,让处理图片的线程恢复运行。
- NSCondition 的对象实际上作为一个锁和一个线程检查器,锁上之后其它线程也能上锁,而之后可以根据条件决定是否继续运行线程,即线程是否要进入 waiting 状态,如果进入 waiting 状态,当其它线程中的该锁执行 signal 或者 broadcast 方法时,线程被唤醒,继续运行之后的方法。
- NSCondition 可以手动控制线程的挂起与唤醒,可以利用这个特性设置依赖。
源码解析:
@interface NSCondition : NSObject <NSLocking> {
@private
void *_priv;
}
- (void)wait; //挂起线程
- (BOOL)waitUntilDate:(NSDate *)limit; //什么时候挂起线程
- (void)signal; // 唤醒一条挂起线程
- (void)broadcast; //唤醒所有挂起线程
@property (nullable, copy) NSString *name API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
@end
方法使用:
- (void)nscondition {
NSCondition * cjcondition = [NSCondition new];
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[cjcondition lock];
NSLog(@"线程1线程加锁");
[cjcondition wait];
NSLog(@"线程1线程唤醒");
[cjcondition unlock];
NSLog(@"线程1线程解锁");
});
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[cjcondition lock];
NSLog(@"线程2线程加锁");
if ([cjcondition waitUntilDate:[NSDate dateWithTimeIntervalSinceNow:10]]) {
NSLog(@"线程2线程唤醒");
[cjcondition unlock];
NSLog(@"线程2线程解锁");
}
});
dispatch_async(dispatch_get_global_queue(0, 0), ^{
sleep(2);
[cjcondition signal];//唤起第一条开启的线程
[cjcondition broadcast];//唤起所有线程
});
}
2017-10-19 17:15:48.410316+0800 Thread-Lock[40011:943638] 线程1线程加锁
2017-10-19 17:15:48.410757+0800 Thread-Lock[40011:943640] 线程2线程加锁
2017-10-19 17:15:50.414288+0800 Thread-Lock[40011:943638] 线程1线程唤醒
2017-10-19 17:15:50.414454+0800 Thread-Lock[40011:943638] 线程1线程解锁
由以上内容总结:
1、在加上锁之后,调用条件对象的 wait 或 waitUntilDate: 方法来阻塞线程,直到条件对象发出唤醒信号或者超时之后,再进行之后的操作。
2、signal 和 broadcast 方法的区别在于,signal 只是一个信号量,只能唤醒一个等待的线程,想唤醒多个就得多次调用,而 broadcast 可以唤醒所有在等待的线程。
3、 dispatch_semaphore 信号量实现加锁(GCD)
dispatch_semaphore 使用信号量机制实现锁,等待信号和发送信号。
- dispatch_semaphore 是 GCD 用来同步的一种方式,与他相关的只有三个函数,一个是创建信号量,一个是等待信号,一个是发送信号。
- dispatch_semaphore 的机制就是当有多个线程进行访问的时候,只要有一个获得了信号,其他线程的就必须等待该信号释放。
相关的API:
dispatch_semaphore_create(long value);
dispatch_semaphore_wait(dispatch_semaphore_t _Nonnull dsema, dispatch_time_t timeout);
dispatch_semaphore_signal(dispatch_semaphore_t _Nonnull dsema);
用法如下:
- (void)dispatch_semaphore {
//创建信号量,必须大于1,如果是N,表示同时可有N条线程执行
dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);
//给信号量设置最大等待时间,超过这个时间则不用等待
dispatch_time_t overTime = dispatch_time(DISPATCH_TIME_NOW, 6 * NSEC_PER_SEC);
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
dispatch_semaphore_wait(semaphore, overTime);
NSLog(@"线程1开始");
sleep(5);
NSLog(@"线程1结束");
dispatch_semaphore_signal(semaphore);
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
sleep(1);
dispatch_semaphore_wait(semaphore, overTime);
NSLog(@"线程2开始");
dispatch_semaphore_signal(semaphore);
});
}
2017-10-19 18:30:37.672490+0800 Thread-Lock[40569:993613] 线程1开始
2017-10-19 18:30:42.673845+0800 Thread-Lock[40569:993613] 线程1结束
2017-10-19 18:30:42.674165+0800 Thread-Lock[40569:993612] 线程2开始
以上总结:
- dispatch_semaphore 和 NSCondition 类似,都是一种基于信号的同步方式,但 NSCondition 信号只能发送,不能保存(如果没有线程在等待,则发送的信号会失效)。而 dispatch_semaphore 能保存发送的信号。dispatch_semaphore 的核心是 dispatch_semaphore_t 类型的信号量。
- dispatch_semaphore_create(1) 方法可以创建一个 dispatch_semaphore_t 类型的信号量,设定信号量的初始值为 1。注意,这里的传入的参数必须大于或等于 0,否则 dispatch_semaphore_create 会返回 NULL。
- dispatch_semaphore_wait(semaphore, overTime); 方法会判断 semaphore 的信号值是否大于 0。大于 0 不会阻塞线程,消耗掉一个信号,执行后续任务。如果信号值为 0,该线程会和 NSCondition 一样直接进入 waiting 状态,等待其他线程发送信号唤醒线程去执行后续任务,或者当 overTime 时限到了,也会执行后续任务。
- dispatch_semaphore_signal(semaphore); 发送信号,如果没有等待的线程接受信号,则使 signal 信号值加一(做到对信号的保存)。
- 一个 dispatch_semaphore_wait(semaphore, overTime); 方法会去对应一个 dispatch_semaphore_signal(semaphore); 看起来像 NSLock 的 lock 和 unlock,其实可以这样理解,区别只在于有信号量这个参数,lock unlock 只能同一时间,一个线程访问被保护的临界区,而如果 dispatch_semaphore 的信号量初始值为 x ,则可以有 x 个线程同时访问被保护的临界区。
4、pthread_mutex与 pthread_mutex(recursive) 互斥锁(C语言)
pthread 表示 POSIX thread,定义了一组跨平台的线程相关的 API,POSIX 互斥锁是一种超级易用的互斥锁,使用的时候:
- 只需要使用 pthread_mutex_init 初始化一个 pthread_mutex_t,
- pthread_mutex_lock 或者 pthread_mutex_trylock 来锁定 ,
- pthread_mutex_unlock 来解锁,
- 当使用完成后,记得调用 pthread_mutex_destroy 来销毁锁。
常用的相关的API:
//初始化
pthread_mutex_init(pthread_mutex_t *restrict _Nonnull, const pthread_mutexattr_t *restrict _Nullable);
//加锁
pthread_mutex_lock(pthread_mutex_t * _Nonnull);
//尝试是锁是否可用
pthread_mutex_trylock(pthread_mutex_t * _Nonnull);
//解锁
pthread_mutex_unlock(pthread_mutex_t * _Nonnull);
用法:
- (void)pthread_mutex {
__block pthread_mutex_t cjlock;
pthread_mutex_init(&cjlock, NULL);
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
pthread_mutex_lock(&cjlock);
NSLog(@"线程1开始");
sleep(3);
NSLog(@"线程1结束");
pthread_mutex_unlock(&cjlock);
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
sleep(1);
pthread_mutex_lock(&cjlock);
NSLog(@"线程2");
pthread_mutex_unlock(&cjlock);
});
}
5、OSSpinLock (暂不建议使用)
OSSpinLock 是一种自旋锁,和互斥锁类似,都是为了保证线程安全的锁。但二者的区别是不一样的,对于互斥锁,当一个线程获得这个锁之后,其他想要获得此锁的线程将会被阻塞,直到该锁被释放。但自选锁不一样,当一个线程获得锁之后,其他线程将会一直循环在哪里查看是否该锁被释放。所以,此锁比较适用于锁的持有者保存时间较短的情况下。
只有加锁,解锁,尝试加锁三个方法。
相关API:
typedef int32_t OSSpinLock;
// 加锁
void OSSpinLockLock( volatile OSSpinLock *__lock );
// 尝试加锁
bool OSSpinLockTry( volatile OSSpinLock *__lock );
// 解锁
void OSSpinLockUnlock( volatile OSSpinLock *__lock );
用法:
- (void)osspinlock {
__block OSSpinLock theLock = OS_SPINLOCK_INIT;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
OSSpinLockLock(&theLock);
NSLog(@"线程1开始");
sleep(3);
NSLog(@"线程1结束");
OSSpinLockUnlock(&theLock);
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
OSSpinLockLock(&theLock);
sleep(1);
NSLog(@"线程2");
OSSpinLockUnlock(&theLock);
});
OSSpinLock 在iOS 10.0中被 <os/lock.h> 中的 os_unfair_lock 取代,同时OSSpinLock存在不安全性
6、os_unfair_lock
自旋锁已经不再安全,然后苹果又整出来个 os_unfair_lock,这个锁解决了优先级反转问题。
常用相关API:
// 初始化
os_unfair_lock_t unfairLock = &(OS_UNFAIR_LOCK_INIT);
// 加锁
os_unfair_lock_lock(unfairLock);
// 尝试加锁
BOOL b = os_unfair_lock_trylock(unfairLock);
// 解锁
os_unfair_lock_unlock(unfairLock);
os_unfair_lock 用法和 OSSpinLock 基本一直,就不一一列出了。
全文总结
应当针对不同的操作使用不同的锁,而不能一概而论哪种锁的加锁解锁速度快。
1. 其实每一种锁基本上都是加锁、等待、解锁的步骤,理解了这三个步骤就可以帮你快速的学会各种锁的用法。
2. @synchronized 的效率最低,不过它的确用起来最方便,所以如果没什么性能瓶颈的话,可以选择使用 @synchronized。
3. 当性能要求较高时候,可以使用 pthread_mutex 或者 dispath_semaphore,由于 OSSpinLock 不能很好的保证线程安全,而在只有在 iOS10 中才有 os_unfair_lock ,所以,前两个是比较好的选择。既可以保证速度,又可以保证线程安全。
文章开端已经放了一张各个锁的性能图,大家可以根据项目需要选择适合锁。