一: 锁的种类
读写锁: atomic (iOS10之后采用os_unfair_lock,之前采用spinlock_t自旋锁)
自旋锁: OSSpinLock (已废弃,不安全,会出现优先级反转问题)
互斥锁: pthread_mutex、@synchronized、NSLock
条件锁: NSConditionLock 、NSCondition
递归锁: NSRecursiveLock
信号量: dispatch_semaphore_t
二: 概念
1. 临界区: 指的是一块进行访问的代码
2. 自旋锁: 特点是在线程等待时会一直轮询,处于忙等状态,直到被锁资源释放锁。
3\. 互斥锁: 如果共享数据已经有其他线程加锁了,线程会进入休眠状态等待锁。一旦被访问的资源被解锁,则等待资源的线程会被唤醒。
三: 细说
3.1 OSSpinLock
3.1.1 OSSpinLock 是一种自旋锁
由于它一直处于running的状态,给人感觉很耗CPU资源,但是它在互斥临界区计算量较小
的场景下,它的效率远高于其它的锁。这里为什么后面会讲解
3.1.2 OSSpinLock 不再安全?
主要原因发生在低优先级线程拿到锁时,高优先级线程进入忙等(busy-wait)状态,
消耗大量 CPU 时间,从而导致低优先级线程拿不到 CPU 时间,也就无法完成任务并释放锁。
这种问题被称为优先级反转。
举个例子说明:
有高优先级任务A / 次高优先级任务B / 低优先级任务C / 资源Z 。A 等待 C
执行后的 Z,而 B 并不需要 Z,抢先获得时间片执行。C 由于没有时间片,无法执行(优先
级相对没有B高)。
这种情况造成 A 在C 之后执行,C在B之后,间接的高优先级A在次高优先级任务B 之
后执行, 使得优先级被倒置了。(假设: A 等待资源时不是阻塞等待,而是忙循环,则可能
永远无法获得资源。此时 C 无法与 A 争夺 CPU 时间,从而 C 无法执行,进而无法释放资
源。造成的后果,就是 A 无法获得 Z 而继续推进。)
为什么忙等会导致低优先级线程拿不到时间片?
操作系统在管理普通线程时,通常采用时间片轮转算法(Round Robin,简称 RR)。
每个线程会被分配一段时间片(quantum),通常在 10-100 毫秒左右。当线程用完属于自己
的时间片以后,就会被操作系统挂起,放入等待队列中,直到下一次被分配时间片。
如何解决优先级反转?
1.优先级继承: 将占有锁的线程优先级,继承等待该锁的线程高优先级,如果存在多
个线程等待,就取其中之一最高的优先级继承。
2.优先级天花板: 则是直接设置优先级上限,给临界区一个最高优先级,进入临界区
的进程都将获得这个高优先级。
3.禁止中断: 禁止中断的特点,在于任务只存在两种优先级:可被抢占的 / 禁止中
断的 。前者为一般任务运行时的优先级,后者为进入临界区的优先级。通过禁止中断来保护临
界区,没有其它第三种的优先级,也就不可能发生反转了。
3.1.3 实现原理
自旋锁的目的是为了确保临界区只有一个线程可以访问,看一段伪代码
do {
Acquire Lock
Critical section // 临界区
Release Lock
Reminder section // 不需要锁保护的代码
}
在 Acquire Lock 这一步,我们申请加锁,目的是为了保护临界区(Critical Section) 中的代码不会被多个线程执行。
bool lock = false;
do {
while(lock); //lock为ture一直循环
lock = ture; //锁住,其它线程无法执行临界区
Critical section // 临界区
lock = false; //释放锁
Reminder section // 不需要锁保护的代码
}
3.2 os_unfair_lock (互斥锁)
os_unfair_lock 是苹果官方推荐的替换OSSpinLock的方案,但是它在iOS10.0以上的
系统才可以调用。os_unfair_lock是一种互斥锁,它不会向自旋锁那样忙等,
而是等待线程会休眠。
os_unfair_lock_t unfairLock;
unfairLock = &(OS_UNFAIR_LOCK_INIT);
os_unfair_lock_lock(unfairLock);
os_unfair_lock_unlock(unfairLock);
3.3 pthread_mutex_t(互斥锁)
pthread定义了一组跨平台的线程相关的 API,其中可以使用 pthread_mutex作为互斥锁。
pthread_mutex 不是使用忙等,而是同信号量一样,会阻塞线程并进行等待,调用时进行
线程上下文切换。
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);//释放锁
3.4 NSLock(pthread_mutex_t的封装)
对pthread_mutex_t封装
@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
tryLock 和 lock 方法都会请求加锁,唯一不同的是trylock在没有获得锁的时候可以继
续做一些任务和处理。lockBeforeDate方法也比较简单,就是在limit时间点之前获得锁,
没有拿到返回NO。
3.5 NSCondition(条件锁,对pthread_mutex_t的封装)
同样是对pthread_mutex_t封装
@interface NSCondition : NSObject <NSLocking> {
@private
void *_priv;
}
- (void)wait;
- (BOOL)waitUntilDate:(NSDate *)limit;
- (void)signal;
- (void)broadcast;
遵循NSLocking协议,使用的时候同样是lock,unlock加解锁,wait是傻等,
waitUntilDate:方法是等一会,都会阻塞掉线程,signal是唤起一个在等待的线程,
broadcast是广播全部唤起。
看个例子
NSCondition *condition = [[NSCondition alloc] init];
NSMutableArray *products = [NSMutableArray array];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[condition lock];
if ([products count] == 0) {
NSLog(@"wait for product");
[condition wait];
}
[products removeObjectAtIndex:0];
NSLog(@"custome a product");
[condition unlock];
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[condition lock];
[products addObject:[[NSObject alloc] init]];
NSLog(@"produce a product,总量:%zi",products.count);
[condition signal];
[condition unlock];
sleep(1);
});
输出
2021-08-01 19:29:22.456624+0800 UsuallyLockDemo[15160:419950] wait for product
2021-08-01 19:29:22.456905+0800 UsuallyLockDemo[15160:419948] produce a product,总量:1
2021-08-01 19:29:22.457068+0800 UsuallyLockDemo[15160:419950] custome a product
3.6 NSConditionLock(基于NSCondition)
@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;
例子
//发送信号条件锁 加载4张图片绘制
NSConditionLock *conditionLock = [[NSConditionLock alloc] init];
NSMutableArray *products = [NSMutableArray array];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[conditionLock lock];
for(int i=0;i<6;i++) {
NSLog(@"load image count: %d",i);
[products addObject:@(i)];
if(products.count==3) {
[conditionLock unlockWithCondition:3];
}
}
});
dispatch_async(dispatch_get_main_queue(), ^{
[conditionLock lockWhenCondition:3];
NSLog(@"已经获取到4张图片->主线程渲染");
[conditionLock unlock];
});
输出:
2021-08-01 22:38:17.948660+0800 UsuallyLockDemo[19286:545978] load image count: 0
2021-08-01 22:38:17.948779+0800 UsuallyLockDemo[19286:545978] load image count: 1
2021-08-01 22:38:17.948873+0800 UsuallyLockDemo[19286:545978] load image count: 2
2021-08-01 22:38:17.948980+0800 UsuallyLockDemo[19286:545978] load image count: 3
2021-08-01 22:38:17.949078+0800 UsuallyLockDemo[19286:545978] load image count: 4
2021-08-01 22:38:17.949184+0800 UsuallyLockDemo[19286:545978] load image count: 5
2021-08-01 22:38:17.960780+0800 UsuallyLockDemo[19286:545426] 已经获取到4张图片->主线程渲染
3.7 NSRecursiveLock(递归锁,基于pthread_mutex_t的封装)
特点: 同一个线程可以加锁N次而不会引发死锁。递归锁会跟踪它被lock的次数。每次成功
的lock都必须平衡调用unlock操作。只有所有达到这种平衡,锁最后才能被释放,
以供其它线程使用
不会造成死锁例子:
NSRecursiveLock *relock = [[NSRecursiveLock alloc]init];
dispatch_async(dispatch_get_global_queue(0, 0), ^{
static void (^RecursiveLock)(int);
RecursiveLock = ^(int value){
[relock lock];
if(value>0) {
NSLog(@"value = %d", value);
sleep(1);
RecursiveLock(value - 1);
}
NSLog(@"解锁value = %d", value);
[relock unlock];
};
RecursiveLock(3);
});
输出:
2021-08-01 22:40:54.120858+0800 UsuallyLockDemo[19383:549318] value = 3
2021-08-01 22:40:54.120986+0800 UsuallyLockDemo[19383:549318] value = 2
2021-08-01 22:40:54.121076+0800 UsuallyLockDemo[19383:549318] value = 1
2021-08-01 22:40:54.121202+0800 UsuallyLockDemo[19383:549318] 解锁value = 0
2021-08-01 22:40:54.121305+0800 UsuallyLockDemo[19383:549318] 解锁value = 1
2021-08-01 22:40:54.121417+0800 UsuallyLockDemo[19383:549318] 解锁value = 2
2021-08-01 22:40:54.121515+0800 UsuallyLockDemo[19383:549318] 解锁value = 3
3.8 dispatch_semaphore(信号量)
GCD 中的信号量是指 Dispatch Semaphore,是持有计数的信号。在 Dispatch
Semaphore 中,使用计数来完成这个功能,计数小于 0 时等待,不可通过。计数为 0 或
大于 0 时,计数减 1 且不等待,可通过。
Dispatch Semaphore 提供了三个方法:
* dispatch_semaphore_create:创建一个 Semaphore 并初始化信号的总量
* dispatch_semaphore_signal:发送一个信号,让信号总量加 1
* dispatch_semaphore_wait:可以使总信号量减 1,信号总量小于 0 时就会一直等待
(阻塞所在线程),否则就可以正常执行。
注意:信号量的使用前提是:想清楚你需要处理哪个线程等待(阻塞),又要哪个线程继续
执行,然后使用信号量。
Dispatch Semaphore 在实际开发中主要用于:
* 并发控制
* 保持线程同步,将异步执行任务转换为同步执行任务
* 保证线程安全,为线程加锁
3.9 @synchronized (互斥锁,具体为递归锁)
@synchronized (self) {
}
转换成:
@try {
objc_sync_enter(obj);
// do work
} @finally {
objc_sync_exit(obj)
};
swift使用锁就是直接调用两个函数
int objc_sync_enter(id obj){
int result = OBJC_SYNC_SUCCESS;
if (obj) {
// 查找这个obj是否已经生成SyncData,如果没有生成一个
SyncData* data = id2data(obj, ACQUIRE);
assert(data);
data->mutex.lock(); // 调用SyncData的递归锁加锁
} else {
// @synchronized(nil) does nothing
// 如果传入nil, 打印了一个log,然后什么都不做
if (DebugNilSync) {
_objc_inform("NIL SYNC DEBUG: @synchronized(nil); set a breakpoint on objc_sync_nil to debug");
}
objc_sync_nil();
return result;
}
通过代码可知: objc_sync_enter内部是一个递归锁实现
原理: @synchronized使用传入的object的内存地址作key,通过hash map对应
的一个系统维护的递归锁。所以不管是传入什么类型的object,只要是有内存地址,
就能启动同步代码块的效果。如果传入nil, 那就相当于没有加锁.
3.10 atomic(读写锁)
看一段源码:
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);
}
// atomic中用到的锁
using spinlock_t = mutex_tt<LOCKDEBUG>;
// mutex_tt 的结构
class mutex_tt : nocopy_t {
os_unfair_lock mLock;
iOS10之前代码:
typedef uintptr_t spin_lock_t;
OBJC_EXTERN void _spin_lock(spin_lock_t *lockp);
OBJC_EXTERN int _spin_lock_try(spin_lock_t *lockp);
OBJC_EXTERN void _spin_unlock(spin_lock_t *lockp);
可以看出:atomic 原子操作只是对setter 和 getter 方法进行加锁
iOS之前:自旋锁spin_lock_t,之后互斥锁os_unfair_lock
atomic 并不是绝对线程安全,它能保证代码进入 getter 和 setter 方法的时候是
安全的,但是并不能保证多线程的访问情况下是安全的,一旦出了 getter 和 setter
方法,其线程安全就要由程序员自己来把握,所以 atomic 属性和线程安全并没有必然联系。
例子:
典型的 i = i+1;
四: 性能对比
1.自旋锁的实现
bool lock = false; // 一开始没有锁上,任何线程都可以申请锁
do {
while(test_and_set(&lock); // test_and_set 是一个原子操作
Critical section // 临界区
lock = false; // 相当于释放锁,这样别的线程可以进入临界区
Reminder section // 不需要锁保护的代码
}
一直在循环查询锁的状态,效率非常高,但是如果临界区处理时间长,非常消耗CPU资源
2.信号量的底层实现
int sem_wait (sem_t *sem) {
int *futex = (int *) sem;
if (atomic_decrement_if_positive (futex) > 0)
return 0;
int err = lll_futex_wait (futex, 0);
return -1;
)
把信号量的值减一,并判断是否大于零。如果大于零,说明不用等待,所以立刻返回。
具体的等待操作在 lll_futex_wait 函数中实现,lll 是 low level lock 的简称。
这个函数通过汇编代码实现,调用到 SYS_futex 这个系统调用,使线程进入睡眠状态,
主动让出时间片,这个函数在互斥锁的实现中,也有可能被用到。
重点: 主动让出时间片并不总是代表效率高。让出时间片会导致操作系统切换到另一个线程,
这种上下文切换通常需要 10 微秒左右,而且至少需要两次切换。如果等待时间很短,
比如只有几个微秒,忙等就比线程睡眠更高效。
3.pthread_mutex
pthread 表示 POSIX thread,定义了一组跨平台的线程相关的 API,pthread_mutex
表示互斥锁。互斥锁的实现原理与信号量非常相似,不是使用忙等,而是阻塞线程并睡眠,
需要进行上下文切换。
对于 pthread_mutex 来说,它的用法和之前没有太大的改变,比较重要的是锁的类型,
可以有 PTHREAD_MUTEX_NORMAL、PTHREAD_MUTEX_ERRORCHECK、PTHREAD_MUTEX_RECURSIVE 等等
所以结论: pthread_mutex的性能肯定没有信号量好
4.NSLock
#define MLOCK \n- (void) lock\n{\n int err = pthread_mutex_lock(&_mutex);\n // 错误处理 ……
}
NSLock 只是在内部封装了一个 pthread_mutex,属性为 PTHREAD_MUTEX_ERRORCHECK,
它会损失一定性能换来错误提示。
这里使用宏定义的原因是,OC 内部还有其他几种锁,他们的 lock 方法都是一模一样,
仅仅是内部 pthread_mutex 互斥锁的类型不同。通过宏定义,可以简化方法的定义。
NSLock 比 pthread_mutex 略慢的原因在于它需要经过方法调用和错误提示,
同时由于缓存的存在,多次方法调用不会对性能产生太大的影响。
5.NSCondition
NSCondition 其实是封装了一个互斥锁和条件变量, 它把前者的 lock 方法
和后者的 wait/signal 统一在 NSCondition 对象中,暴露给使用者:
- (void) signal {
pthread_cond_signal(&_condition);
}
// 其实这个函数是通过宏来定义的,展开后就是这样
- (void) lock {
int err = pthread_mutex_lock(&_mutex);
}
open func wait() {
pthread_cond_wait(cond, mutex)
}
它的加解锁过程与 NSLock 几乎一致,理论上来说耗时也应该一样(实际测试也是如此)
6.NSConditionLock
NSConditionLock 借助 NSCondition 来实现,它的本质就是一个生产者-消费者模型。
“条件被满足”可以理解为生产者提供了新的内容。NSConditionLock 的内部持有一个
NSCondition 对象,以及 _condition_value 属性,在初始化时就会对这个属性
进行赋值:
// 简化版代码
- (id) initWithCondition: (NSInteger)value {
if (nil != (self = [super init])) {
_condition = [NSCondition new]
_condition_value = value;
}
return self;
}
它的 lockWhenCondition 方法其实就是消费者方法:
- (void) lockWhenCondition: (NSInteger)value {
[_condition lock];
while (value != _condition_value) {
[_condition wait];
}
}
对应的 unlockWhenCondition 方法则是生产者,使用了 broadcast 方法通知了
所有的消费者:
- (void) unlockWithCondition: (NSInteger)value {
_condition_value = value;
[_condition broadcast];
[_condition unlock];
}
7.NSRecursiveLock
递归锁也是通过 pthread_mutex_lock 函数来实现,在函数内部会判断锁的类型,
如果显示是递归锁,就允许递归调用,仅仅将一个计数器加一,锁的释放过程也是同理。
NSRecursiveLock 与 NSLock 的区别在于内部封装的 pthread_mutex_t 对象的类型
不同,前者的类型为 PTHREAD_MUTEX_RECURSIVE。
8.@synchronized (互斥锁->递归锁)
这其实是一个 OC 层面的锁, 主要是通过牺牲性能换来语法上的简洁与可读。
我们知道 @synchronized 后面需要紧跟一个 OC 对象,它实际上是把这个对象当做
锁来使用。这是通过一个哈希表来实现的,OC 在底层使用了一个互斥锁的数组(你可以理解
为锁池),通过对对象去哈希值来得到对应的互斥锁(递归锁)。
结合 dispatch_barrier_async 可以实现 频繁读,少量写操作
@interface Atme_Lock ()
// 定义一个并发队列:
@property (nonatomic, strong) dispatch_queue_t concurrent_queue;
// 数据中心, 可能多个线程需要数据访问:
@property (nonatomic, strong) NSMutableDictionary *dataDic;
@end
- (id)init {
self = [super init];
if (self){
// 创建一个并发队列:
self.concurrent_queue = dispatch_queue_create("read_write_queue", DISPATCH_QUEUE_CONCURRENT);
// 创建数据字典:
self.dataDic = [NSMutableDictionary dictionary];
}
return self;
}
#pragma mark - 读数据
- (id)get_objectForKey:(NSString *)key{
__block id obj;
// 同步读取指定数据:
dispatch_sync(self.concurrent_queue, ^{
obj = [self.dataDic objectForKey:key];
});
return obj;
}
#pragma mark - 写数据
- (void)set_setObject:(id)obj forKey:(NSString *)key{
// 异步栅栏调用设置数据:
dispatch_barrier_async(self.concurrent_queue, ^{
[self.dataDic setObject:obj forKey:key];
});
}
性能总结:
1.自旋锁->信号量->互斥锁->读写锁
2.NSLock->NSCondition->NSRecursiveLock->NSConditionLock