文章目录
前言
iOS中的锁主要可以分为两大类,互斥锁
与自旋锁
,剩下的其他锁其实都是这两种锁的延伸与扩展
我们先用一张对比图来引出我们今天要讲的锁
自旋锁
自旋锁是一种在多处理器系统中被广泛使用的锁机制。它的原理是当一个线程试图获取一个被其他线程持有的锁时,它不是陷入休眠状态,而是一直循环等待(自旋),直到锁被释放为止。
OSSpinLock
这是一个不安全的锁自从OSSpinLock
出现安全问题,在iOS10
之后就被废弃了。
自旋锁之所以不安全是,因为获取锁后,线程会一直处于忙等待,造成了任务的优先级反转。
首先,我们需要理解自旋锁的工作原理。当一个线程尝试获取已被其他线程占有的自旋锁时,它不会被阻塞,而是在一个小的循环中一直检查锁的状态,直到获取到锁为止。这种一直运行的机制就被称为"忙等待"。
忙等待机制可能会导致一个问题,就是优先级反转。假设有一个低优先级任务持有一个锁,而一个高优先级任务正在等待获取这个锁。通常情况下,高优先级任务应该可以抢占低优先级任务的CPU时间片。但是由于低优先级任务一直在忙等待中运行,它会一直占用CPU时间片,而高优先级任务无法抢占,从而导致优先级反转。
也就是使用OSSpinLock会导致低优先级任务先执行
目前OSSpinLock
已经被os_unfair_lock
替代
os_unfair_lock
由于OSSpinLock
并不安全,因此苹果推出了os_unfair_lock
以解决优先级反转的问题
//创建一个锁
os_unfair_lock_t unfairLock;
//初始化
unfairLock = &(OS_UNFAIR_LOCK_INIT);
//加锁
os_unfair_lock_lock(unfairLock);
//解锁
os_unfair_lock_unlock(unfairLock);
Demo
#import "ViewController.h"
#import <libkern/OSAtomic.h>
#import <os/lock.h>
// 全局变量或实例变量
os_unfair_lock_t lock = &(OS_UNFAIR_LOCK_INIT);
@interface ViewController ()
@end
@implementation ViewController
-(void)lowPriorityTask {
os_unfair_lock_lock(lock);
NSLog(@"Low priority task started");
// 模拟一些耗时操作
// [NSThread sleepForTimeInterval:5];
NSLog(@"Low priority task completed");
os_unfair_lock_unlock(lock);}
// 高优先级任务
-(void)highPriorityTask {
os_unfair_lock_lock(lock);
NSLog(@"High priority task started");
// 模拟一些关键操作
NSLog(@"High priority task completed");
os_unfair_lock_unlock(lock);}
- (void)viewDidLoad {
[super viewDidLoad];
NSThread *lowPriorityThread = [[NSThread alloc] initWithTarget:self selector:@selector(lowPriorityTask) object:nil];
lowPriorityThread.name = @"Low Priority Thread";
[lowPriorityThread start];
// 创建高优先级线程
NSThread *highPriorityThread = [[NSThread alloc] initWithTarget:self selector:@selector(highPriorityTask) object:nil];
highPriorityThread.name = @"High Priority Thread";
highPriorityThread.threadPriority = 1.0; // 设置高优先级
[highPriorityThread start];
}
@end
由此便解决了任务优先级反转的问题
atomic
atomic
适用于OC中属性的修饰符,其自带一把自旋锁,但是这个一般基本不使用,都是使用的nonatomic
我们知道属性修饰符不同,setter
与getter
方法会分别调用不同的方法,但是最后会统一调用reallySetProperty
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
修饰的属性进行了spinlock加锁处理
nonatomic
修饰的属性除了没加锁,其他逻辑与atomic
一般无二
spinlock_t
的底层实现就是os_unfair_lock
当多个线程同时对同一个属性进行读取或写入操作时,使用atomic
修饰符可以确保属性的值不会出现异常或不一致的情况。
然而,尽管atomic
提供了一定的线程安全性,但它的性能开销较大。因为在多线程同时访问属性时,其他线程需要等待锁的释放,这可能导致性能下降。
尽管atomic
提供了一定的线程安全性,但由于性能开销较大,我们在实际开发中更加倾向于nonatomic
互斥锁
互斥锁(Mutex)是一种用于实现线程同步和互斥访问共享资源的基本同步原语。它可以**确保同一时间内,只有一个线程可以获取互斥锁**并访问被保护的临界区代码
我们来看一下自旋锁与互斥锁的区别
- 互斥锁:如果共享数据已经有其他线程加锁了,线程会进入休眠状态等待锁。一旦被访问的资源被解锁,则等待资源的线程会被唤醒。
- 自旋锁:如果共享数据已经有其他线程加锁了,线程会以死循环的方式等待(也就是忙等待),一旦被访问的资源被解锁,则等待资源的线程会立即执行
自旋锁的效率高于互斥锁,但是因为在自旋时不释放CPU,因此持有自旋锁的线程应该尽快释放自旋锁,防止CPU资源消耗,所以自旋锁适合耗时较短的操作
互斥锁又分为两种
- 递归锁
允许同一个线程多次获取同一个锁。 - 非递归锁
不允许同一个线程多次获取同一个锁
互斥锁的实现细节:
大多数操作系统都提供了互斥锁的底层实现,如POSIX线程库中的pthread_mutex。在iOS/macOS系统中,常用的互斥锁包括:
pthread_mutex
: POSIX线程库中的互斥锁,是底层实现。NSLock
: Objective-C中的互斥锁,内部使用pthread_mutex实现。- @
synchronized
: Objective-C中的另一种互斥锁实现,基于对象监视器(monitor)。
接下来我们具体讲讲这些互斥锁
pthread_mutex
pthread_mutex
就是互斥锁本身,当锁被占用,其他线程申请锁时,不会一直忙等待,而是阻塞线程并睡眠
// 导入头文件
#import <pthread.h>
// 全局声明互斥锁
pthread_mutex_t _lock;
// 初始化互斥锁
pthread_mutex_init(&_lock, NULL);
// 加锁
pthread_mutex_lock(&_lock);
// 这里做需要线程安全操作
// 解锁
pthread_mutex_unlock(&_lock);
// 释放锁
pthread_mutex_destroy(&_lock);
@synchronized
@synchronized是日常开发比较常用的一种锁,它的使用比较简单,但是性能较低
我们通过源码理解一下这个锁
在Objective-C Runtime源码中,@synchronized由objc-sync.mm文件中的几个函数实现。核心实现在objc_sync_enter
和objc_sync_exit
两个函数中。
// 进入临界区
id
objc_sync_enter(id obj)
{
int result = OBJC_SYNC_SUCCESS;
if (obj) {
SyncData_t *sd = sdallow(obj, false, false);
result = sdold_sync_enter(sd);
}
return (result == OBJC_SYNC_SUCCESS) ? obj : nil;
}
// 退出临界区
void
objc_sync_exit(id obj)
{
if (obj) {
SyncData_t *sd = sdallow(obj, true, true);
sdold_sync_exit(sd);
}
}
这两个函数的主要作用是进入和退出@synchronized临界区。它们的关键在于SyncData_t
结构体和sdallow
函数。
SyncData_t
结构体定义如下:
struct SyncData_t {
recursive_mutex_t mutex; // 互斥锁
uintptr_t value; // 关联对象存储的值
uintptr_t exception;
};
SyncData_t
结构体中包含了一个recursive_mutex_t
类型的互斥锁,这是@synchronized
底层实现同步的关键,这也说明@synchronized
其实是一个递归互斥锁
sdallow
函数的作用是获取与给定对象关联的SyncData_t
实例,如果不存在就创建一个新的。这个函数的实现利用了关联对象(AssociatedObject
)技术,将SyncData_t
实例与特定对象关联存储。
SyncData_t *
sdallow(id obj, bool create, bool warmup)
{
SyncData_t *sd = sdalloc(obj->isTaggedPointer(), create);
// ...
return sd;
}
SyncData_t *
sdalloc(bool tagged, bool create)
{
SyncData_t *sd;
if (tagged) {
sd = (SyncData_t *)&SyncDataObjc_Tag; // Tagged pointer objects
} else {
sd = (SyncData_t *)object_getAssociatedObject(obj, &SyncDataAQL); // 获取关联对象
if (sd == nil && create) {
sd = (SyncData_t *)calloc(1, sizeof(SyncData_t)); // 创建新实例
object_setAssociatedObject(obj, &SyncDataAQL, sd, OBJC_ASSOCIATION_ASSIGN); // 设置关联对象
}
}
return sd;
}
总结
- 因为@
synchronized
在底层封装的是一把递归锁,所以这个锁是递归互斥锁 @synchronized
的原因是为了方便下一个data的插入- 由于链表的查询与缓存的查找十分消耗性能,因此该锁的性能排名比较低下
- 但是因为使用起来方便简单,不用解锁,使用率还是比较高的
- 不能用非OC对象作为加锁对象,因为
object
的参数为id @synchronized (self)
这种适用于嵌套次数较少的场景。这里锁住的对象也并不永远是self
,这里需要读者注意
NSLock
我们来看一下NSLock
的底层实现
NSLock
的实现位于Foundation
框架的NSLock.m
文件中,它的核心数据结构是一个名为_NSLockingData
的结构体
typedef struct _NSLockingData {
pthread_mutex_t mutex; // POSIX线程互斥锁
volatile int32_t val; // 锁的状态值
volatile int32_t contenders; // 等待获取锁的线程数
uint16_t behavior; // 锁的行为选项
uint16_t reserved;
} _NSLockingData;
可以看到pthread_mutex_t
类型的互斥锁包含在我们的结构体中
所以我们可以知道他是一个非递归互斥锁
在使用非递归锁时,如果发生了递归调用,线程不会死锁,而是会被阻塞(堵塞)
- (void)test {
self.testArray = [NSMutableArray array];
NSLock *lock = [[NSLock alloc] init];
for (int i = 0; i < 200000; i++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[lock lock];
self.testArray = [NSMutableArray array];
[lock unlock];
});
}
}
从官方文档的解释里看的更清楚,在同一线程上调用NSLock
的两次lock
方法将永久锁定线程。同时官方文档重点提醒向NSLock
对象发送解锁消息时,必须确保该消息是从发送初始锁定消息的同一线程发送的。
NSRecursiveLock
NSRecursiveLock
是Objective-C
中的递归锁类,它是基于底层的pthread_mutex_t
实现的 ,同时是 NSLock 的子类,具有相同的基本功能,但允许同一个线程多次对锁进行加锁操作而不会造成死锁。
NSRecursiveLock
使用了一个互斥锁(mutex lock
)来实现递归锁的功能。互斥锁是一种同步机制,它提供了对临界区的独占访问
我们来看一下官方文档中的解释
他是一个同一线程可以多次获取而不会导致死锁的锁,重点是在同一线程。
我们来看一个可能会导致递归锁死锁的例子
这个示例中,我们将创建两个线程,每个线程都试图在获取了一个锁之后获取另一个锁,但是锁的获取顺序相反,这就可能导致死锁:
- (void)recursiveLockDeadlockExample {
NSRecursiveLock *lockA = [[NSRecursiveLock alloc] init];
NSRecursiveLock *lockB = [[NSRecursiveLock alloc] init];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
[lockA lock];
NSLog(@"Thread 1 acquired lock A");
sleep(1); // Wait to ensure Thread 2 locks lock B
[lockB lock];
NSLog(@"Thread 1 acquired lock B");
[lockB unlock];
[lockA unlock];
NSLog(@"Thread 1 released both locks");
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
[lockB lock];
NSLog(@"Thread 2 acquired lock B");
sleep(1); // Wait to ensure Thread 1 locks lock A
[lockA lock];
NSLog(@"Thread 2 acquired lock A");
[lockA unlock];
[lockB unlock];
NSLog(@"Thread 2 released both locks");
});
}
• 线程1 首先获取 lockA
,然后尝试获取 lockB
。
• 同时,线程2 首先获取 lockB
,然后尝试获取 lockA
。
如果两个线程几乎同时运行,线程1成功获取 lockA
后,线程2获取了 lockB
。此时,线程1在尝试获取 lockB
时会被阻塞,因为它已经被线程2持有;同样,线程2在尝试获取 lockA
时也会被阻塞,因为它已经被线程1持有。这样就形成了一个典型的死锁情况。
也就是多线程不能同时获取同一把互斥锁,不然可能导致死锁
条件锁
条件锁是一种特殊类型的同步机制,用于管理不同线程间的执行顺序,使得某些线程能在满足特定条件时才继续执行。
像NSCondition
封装了pthread_mutex
的以上几个函数,NSConditionLock
封装了NSCondition
。
NSCondition
NSCondition
是一个条件锁,它允许线程在满足某个条件之前挂起,直到其他线程改变了条件并通知 NSCondition
。
通俗的理解就是当当前线程不满足条件时阻塞该线程,直到其他线程通知可以继续被阻塞的线程可以继续往下走
NSCondition
的对象实际上作为一个锁 和 一个线程检查器
- 锁主要为了当检测条件时保护数据源,执行条件引发的任务
- 线程检查器主要是根据条件决定是否继续运行线程,即线程是否被阻塞
看一个经典的生产-消费例子
- (void)producer {
while (true) {
[_condition lock];
// 生产一个产品
self.ticketconuts++;
// NSLog(@"Produced a product");
NSLog(@"生产一个 现有 count %d",self.ticketconuts);
[_condition signal]; // 通知消费者
[_condition unlock];
// sleep(1); // 模拟生产时间
}
}
- (void)consumer {
while (true) {
[_condition lock];
while (self.ticketconuts == 0) {
[_condition wait]; // 等待产品可用
NSLog(@"等待");
}
self.ticketconuts--;
// NSLog(@"Consumed a product");
NSLog(@"消费一个 现有 count %d",self.ticketconuts);
[_condition unlock];
// sleep();
}
}
- (void)viewDidLoad {
[super viewDidLoad];
self.ticketconuts = 50;
_condition = [[NSCondition alloc] init];
for (int i = 0; i < 50; i++) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
[self producer];
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
[self consumer];
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
[self consumer];
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
[self producer];
});
}
}
如果产品不足就用wait
告诉当前线程需要等待,也就是阻塞线程,直到signal
通知线程可以继续往下走
(void)wait
阻塞当前线程,使线程进入休眠,等待唤醒信号。调用前必须已加锁。(void)waitUntilDate
阻塞当前线程,使线程进入休眠,等待唤醒信号或者超时。调用前必须已加锁。(void)signal
唤醒一个正在休眠的线程,如果要唤醒多个,需要调用多次。如果没有线程在等待,则什么也不做。调用前必须已加锁。(void)broadcast
唤醒所有在等待的线程。如果没有线程在等待,则什么也不做。调用前必须已加锁。
NSLock NSRecursiveLock NSCondition总结
NSLock
不支持递归加锁NSRecursiveLock
虽然有递归性,但没有多线程特性NSCondition
的对象实际上作为⼀个锁和⼀个线程检查器
NSConditionLock
NSConditionLock也是条件锁,一旦一个线程获取该锁,其他想获取该锁的线程一定等待
NSConditionLock
是对NSCondition
的封装
NSConditionLock 提供了一系列方法来支持基于条件的锁操作:
initWithCondition
::初始化一个带有特定初始状态的 NSConditionLock
。
lockWhenCondition
::只有当锁的状态与指定的条件匹配时,调用该方法的线程才能获取锁。如果状态不匹配,线程将阻塞,直到状态变为所需的条件。
tryLockWhenCondition
::尝试获取锁,只有当锁的状态与指定条件匹配时才会成功。如果无法立即获取锁,方法将返回 NO 而不会阻塞线程。
unlockWithCondition
::释放锁并将其状态设置为一个新的值。这对于在多个线程间同步状态转换非常有用。
用一个Demo来示例NSConditionLock的使用
- (void)cjl_testConditonLock{
// 信号量
NSConditionLock *conditionLock = [[NSConditionLock alloc] initWithCondition:2];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
[conditionLock lockWhenCondition:1]; // conditoion = 1 内部 Condition 匹配
// -[NSConditionLock lockWhenCondition: beforeDate:]
NSLog(@"线程 1");
[conditionLock unlockWithCondition:0];
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
[conditionLock lockWhenCondition:2];
sleep(0.1);
NSLog(@"线程 2");
// self.myLock.value = 1;
[conditionLock unlockWithCondition:1]; // _value = 2 -> 1
});
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[conditionLock lock];
NSLog(@"线程 3");
[conditionLock unlock];
});
}
由于队列优先级的原因,线程的调用顺序就是:线程1->线程3->线程2,由于线程1由于条件不符合不会先执行,即使他的优先级是最高的,然后线程3执行任务,执行线程3由于条件符合执行线程2,线程2改变条件后线程1开始执行
demo分析汇总
线程 1 调用[NSConditionLock lockWhenCondition:],此时此刻因为不满足当前条件,所以会进入
waiting 状态,当前进入到 waiting 时,会释放当前的互斥锁。此时当前的线程 3 调用[NSConditionLock lock:],本质上是调用 [NSConditionLock
lockBeforeDate:],这里不需要比对条件值,所以线程 3 会打印接下来线程 2 执行[NSConditionLock lockWhenCondition:],因为满足条件值,所以线程2
会打印,打印完成后会调用[NSConditionLock unlockWithCondition:],这个时候将value 设置为
1,并发送 boradcast, 此时线程 1 接收到当前的信号,唤醒执行并打印。自此当前打印为 线程 3->线程 2 -> 线程 1
[NSConditionLock lockWhenCondition:];这里会根据传入的 condition 值和 Value
值进行对比,如果不相等,这里就会阻塞,进入线程池,否则的话就继续代码执行[NSConditionLock
unlockWithCondition:]: 这里会先更改当前的 value 值,然后进行广播,唤醒当前的线程
总结
OSSpinLocK
自旋锁由于性能问题,底层已经用os_unfair_lock
替代,这是一种非常高效的锁,用于替代不再推荐使用的OSSpinLock
。它避免了忙等,而是使线程休眠,但在锁竞争激烈的情况下可以保持高性能。但推荐执行时间较短的任务使用,否则会造成CPU资源消耗atomic
原子锁自带一般自旋锁,在使用getter与setter方法时底层实现会进行加锁保证安全,但是比较消耗性能,日常开发还是更推荐nonatomic
@synchronized
在底层使用哈希表来维护每个锁对象相关的线程数据,同时通过链表来记录锁的获取情况,虽然性能较低,但是使用方便,使用率高。但是同时由于它是递归锁,需要进行额外的计数管理,增加了运行时的开销NSLock
与NSRecursiveLock
是对pthread_mutex
的封装,NSLock
是互斥锁,NSRecursive
是递归互斥锁NSCondition
与NSConditionLock
是条件锁,底层都是对pthread_mutex
的封装