OC底层原理21-锁的原理
本文探索常用锁以及@synchronized
底层的原理。
锁的分类
在开发中,使用最常见的恐怕就是@synchronized
(互斥锁)、NSLock
(互斥锁)、以及dispatch_semaphore
(信号量)。其实还有许多种,总分类有:互斥锁、自旋锁,细分之下多出了: 读写锁、递归锁、条件锁、信号量,后三者是对基本锁的上层封装。先介绍几个概念。
【自旋锁】是用于多线程同步的一种锁,线程反复检查锁变量是否可用。由于线程在这一过程中保持执行,因此是一种忙等待。一旦获取了自旋锁,线程会一直保持该锁,直至显式释放自旋锁。 自旋锁避免了进程上下文的调度开销,因此对于线程只会阻塞很短时间的场合是有效的。
【互斥锁】是一种用于多线程编程中,防止两条线程同时对同一公共资源(比如全局变量)进行读写的机制。该目的通过将代码切片成一个一个的临界区而达成。
【读写锁】是计算机程序的并发控制的一种同步机制(也称“共享-互斥锁”、多读-单写锁) 用于解决多线程对公共资源读写问题。读的操作可并发重入,写操作是互斥的。 读写锁通常用互斥锁、条件变量、信号量实现。
【信号量】是一种更高级的同步机制,互斥锁可以说是semaphore
在仅取值0/1时的特例。信号量可以有更多的取值空间,用来实现更加复杂的同步,而不单单是线程间互斥。
【条件锁】:条件锁就是条件变量,当进程的某些资源要求不满足时就进入休眠,即锁住了,当资源被分配到了,条件锁打开了,进程继续运行。
对应有以下锁:
- OSSpinLock(自旋锁)
- dispatch_semaphone(信号量)
- pthread_mutex(互斥锁)
- NSLock(互斥锁)
- NSCondition(条件锁)
- os_unfair_lock (互斥锁)
- pthread_mutex(recursive 互斥递归锁)
- NSRecursiveLock(递归锁)
- NSConditionLock(条件锁)
- synchronized(互斥锁)
OSSpinLock(自旋锁)
- 与互斥锁(阻塞-睡眠)不同,自旋锁加锁后是进入忙等状态。
- 如果共享数据已经有其他线程加锁了,线程会以忙等的方式等待锁,一旦被访问的资源被解锁,则等待资源的线程会立即执行。
OSSpinLock效率很高,但是已不再安全。如果一个低优先级的线程获得锁并访问共享资源,这时一个高优先级的线程也尝试获得这个锁,它会处于 spin lock 的忙等状态从而占用大量 CPU。此时低优先级线程无法与高优先级线程争夺 CPU 时间,从而导致任务迟迟完不成、无法释放 lock。
ibireme 大神--<不再安全的 OSSpinLock>
各类型锁的性能
所以苹果已经推荐使用os_unfair_lock
。
- os_unfair_lock基本使用
关于os_unfair_lock
是苹果在iOS10之后推出,它属于互斥锁,os_unfair_lock
加锁会让等待的线程进入休眠状态,而不是忙等。这样就提高了安全也降低了性能损耗。
#import <os/lock.h>
// 创建一个 os_unfair_lock_t 锁
os_unfair_lock_t unfairLock;
// 先分配此类型的变量并将其初始化为OS_UNFAIR_LOCK_INIT
unfairLock = &(OS_UNFAIR_LOCK_INIT);
// 尝试加锁,返回YES or NO
os_unfair_lock_trylock(unfairLock)
// 加锁
os_unfair_lock_lock(unfairLock);
// 解锁
os_unfair_lock_unlock(unfairLock);
dispatch_semaphore(信号量)
信号量适用于异步线程同步操作的场景。
// 创建使用
dispatch_semaphore_create(long value); // 创建信号量
dispatch_semaphore_signal(dispatch_semaphore_t deem); // 发送信号量
dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout); // 等待信号量
// 👉注意: 发送信号量和信号等待是成对出现
// 常见使用场景
dispatch_semaphore_t sem = dispatch_semaphore_create(0);
dispatch_async(dispatch_get_global_queue(0, 0), ^{ // ①
NSLog(@"任务1:%@",[NSThread currentThread]);
dispatch_semaphore_signal(sem); // ③
});
dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER); // ②
dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSLog(@"任务2:%@",[NSThread currentThread]);
dispatch_semaphore_signal(sem); // ⑤
});
dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER); // ④
dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSLog(@"任务3:%@",[NSThread currentThread]); // ⑥
});
// 执行顺序:① - ② - ③ - ④ - ⑤ - ⑥
}
通过控制信号量通过数,就可实现锁的功能。
pthread_mutex(互斥锁)
- 阻塞线程并
sleep
(加锁),加锁过程中切换上下(主动出让时间片,线程休眠,等待下一次唤醒)、cpu的抢占、信号的发送等开销。 - 如果共享数据已经有其他线程加锁了,线程会进入休眠状态等待锁。一旦被访问的资源被解锁,则等待资源的线程会被唤醒。
- 互斥锁范围,应该尽量小;锁定范围越大,效率越差。
- 能够给任意NSObject对象加锁。
加解锁流程:sleep(加锁) -> 出让时间片 -> 线程休眠 -> 等待唤醒 -> running(解锁)
时间⽚(quantum):系统给每个正在运行的进程或线程微观上的一段CPU时间。
// 导入互斥锁头文件--C语言
#import <pthread.h>
// 可添加成员变量
pthread_mutex_t mutex;
- (void)myfun
{
pthread_mutex_init(&mutex, NULL);
}
- (void)MyLockingFunction
{
pthread_mutex_lock(&mutex);
// Do something.
pthread_mutex_unlock(&mutex);
}
- (void)dealloc
{
// 不用要释放掉
pthread_mutex_destroy(&mutex);
}
// 这只是简单使用,具体还需针对进行错误代码处理
互斥锁 vs 自旋锁
相同:都能保证同一时间只有一个线程访问共享资源。都能保证线程安全。
不同:
- 互斥锁:如果共享数据已经有其他线程加锁了,线程会进入休眠状态等待锁。一旦被访问的资源被解锁,则等待资源的线程会被唤醒。
- 自旋锁:如果共享数据已经有其他线程加锁了,线程会以忙等的方式等待锁,一旦被访问的资源被解锁,则等待资源的线程会立即执行。
NSLock(互斥锁)
NSLock是对底层pthread_mutex
的封装。一般使用有:
self.lock = [[NSLock alloc] init];
[self.lock tryLock]; // 尝试加锁;返回YES or NO
[self.lock lock]; // 加锁
[self.lock unlock]; // 解锁
在Apple官方文档中指出
Warning
The NSLock class uses POSIX threads to implement its locking behavior. When sending an
unlock message to an NSLock object, you must be sure that message is sent from the
same thread that sent the initial lock message. Unlocking a lock from a different thread can result in undefined behavior.
Tra:本NSLock类使用POSIX线程执行其锁定行为。向NSLock对象发送解锁消息时,必须确保该消
息是从发送初始锁定消息的同一线程发送的。从其他线程解锁锁可能导致未定义的行为。
所有它仅限用于同一线程中,且也不应使用此类来实现递归锁。lock
在同一线程上两次调用该方法将永久锁定您的线程。原因是加锁还未解锁又再一次加锁,一直在加锁就会陷入死锁状态。如下:
NSLock *lock = [[NSLock alloc] init];
for (int i= 0; i<50; i++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
static void (^testMethod)(int);
testMethod = ^(int value){
[lock lock];
if (value > 0) {
NSLog(@"current value = %d",value);
testMethod(value - 1);
}
};
testMethod(10);
[lock unlock];
});
}
可以使用NSRecursiveLock
来实现递归锁。
atomic & nonatomic
atomic
- atomic 原⼦属性(线程安全),针对多线程设计的,需要消耗⼤量的资源
- atomic 本身就有⼀把锁(⾃旋锁)
- 保证同⼀时间只有⼀个线程能够写⼊,但是同⼀个时间多个线程都可以取值。(单写多读:单个线程写⼊,多个线程可以读取)
nonatomic
- nonatomic ⾮原⼦属性
- nonatomic:⾮线程安全,适合内存⼩的移动设备。
属性应都声明为 nonatomic
;
尽量避免多线程抢夺同⼀块资源;
尽量将加锁、资源抢夺的业务逻辑交给服务器端处理,减⼩移动客户端的压⼒。