苹果官方资源opensource
objc4-838可编译联调源码
多线程相关文献:
iOS 多线程原理 - 线程与队列底层
iOS 多线程原理 - GCD函数底层
iOS 线程底层 - 锁
本章节探究:
1.了解线程安全与锁
2.原子属性atomic能保证线程安全吗
3.锁的性能分析
4.锁的实践
5.读写锁
6.@synchronized源码分析
前言
线程安全
:在拥有共享数据的多条线程并行执行的程序中,线程安全的代码会通过同步机制保证各个线程都可以正常且正确的执行,不会出现数据污染等意外情况。多个线程访问同一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他操作,调用这个对象的行为都可以获得正确的结果,那么这个对象就是线程安全的。
#import "ViewController.h"
@interface ViewController ()
@property (nonatomic ,assign) int count;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.count = 10;
[self test];
}
- (void)test {
// self.count初始值是10
for (int i = 0; i < 10; i ++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
self.count --;
NSLog(@"%d",self.count);
});
}
}
@end
此时很明显是线程不安全的,那就得运用到锁,把资源锁住不被其它线程访问,待得当前线程把任务处理完后才可以被其它线程访问。
锁主要分为两大类自旋锁
和互斥锁
。
自旋锁
:线程反复检查锁变量是否可用。由于线程在这一过程中保持执行,因此是一种忙等待。一旦获取自旋锁,线程会一直保持该锁,直至显式释放自旋锁。自旋锁避免了线程上下文切换的调度开销,因此对于线程只会阻塞很短的时间是很高效的,但是对于比较长时间的阻塞也是比较消耗CPU的。(线程忙等)互斥锁
:上一个线程的任务没有执行完毕的时候(被锁住),那么下一个线程会进入睡眠状态等待任务执行完毕,直到上一个执行完成,一个线程会自动唤醒,然后开始执行任务。有上下文的切换(主动出让时间片, 线程休眠, 等待下一次唤醒)、CPU的抢占、信号的发送等开销。(线程闲等)
一、原子属性atomic
如果我们类的property
使用原子属性atomic
,它能保证property
是线程安全的吗?
#import "ViewController.h"
@interface ViewController ()
@property (atomic ,assign) int count;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.count = 10;
[self test_atomic];
}
- (void)test_atomic {
// self.count初始值是10
for (int i = 0; i < 10; i ++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
self.count --;
NSLog(@"%d",self.count);
});
}
}
@end
atomic总结:原子属性atomic不能保证数据的线程安全。
下面打开objc4源码进行分析:在属性的getter/setter
方法调用的底层atomic
和nonatomic
有什么区别。
-
objc_setProperty
的源码分析:
setter
:
非原子属性nonatomic
直接是做了赋值操作
原子属性atomic
在赋值之前加锁,赋值之后解锁
-
objc_getProperty
的源码分析:
getter
:
非原子属性nonatomic
返回了值
原子属性atomic
在取值前加锁,取值后解锁,再返回值
那么原子属性atomic
在getter/setter
底层有加锁解锁操作,为什么不能保证线程安全的呢?
因为原子属性atomic
锁住资源的范围不够大。在self.count --;
的时候,既有getter
也有setter
,可能就出现当getter
的时候还没有return
出去就被其它线程setter
。
二、锁的性能分析
在模拟器
下测试同时加锁解锁10万次的性能:
在iPhone6s真机
下测试同时加锁解锁10万次的性能:
在iPhone13真机
下测试同时加锁解锁10万次的性能:
可以看出@synchronized
的性能是不一定是差的。
附上关于锁的性能测试下载demo。自己可以试试。
三、锁的实践
1.OSSpinLock(废弃) - 自旋锁
OSSpinLock
在iOS10之后被移除了。 被移除的原因是它有一个bug
:优先级反转
。
优先级反转:当多个线程有优先级的时候,有一个优先级较低的线程先去访问了资源,并是有了OSSpinLock
对资源加锁,又来一个优先级较高的线程去访问了这个资源,这个时候优先级较高的线程就会一直占用cpu的资源,导致优先级较低的线程没办法与较高的线程争夺cpu的时间,最后导致最先被优先级较低的线程锁住的资源迟迟不能被释放,从而造成优先级反转的bug。
OSSpinLock使用限制
:必须保证所有访问同一资源的线程处于优先级平等的时候,才可以使用。
OSSpinLock
被苹果放弃了,大家也可以放弃它,苹果设计了os_unfair_lock
来代替OSSpinLock
。
2.os_unfair_lock - 互斥锁
iOS10之后开始支持,用于取代OSSpinLock。ps:os_unfair_lock是互斥锁
API | 说明 | 参数 |
---|---|---|
OS_UNFAIR_LOCK_INIT |
初始化锁 | |
os_unfair_lock_lock |
加锁 |
os_unfair_lock 地址 |
os_unfair_lock_unlock |
解锁 |
os_unfair_lock 地址 |
os_unfair_lock_trylock |
尝试加锁。如果成功返回true。如果锁已经被锁定则返回false |
os_unfair_lock 地址 |
os_unfair_lock_assert_owner |
如果当前线程未持有指定的锁或者锁已经被解锁,则触发崩溃 |
os_unfair_lock 地址 |
os_unfair_lock_assert_not_owner |
如果当前线程持有指定的锁,则触发崩溃 |
os_unfair_lock 地址 |
#import "ViewController.h"
#import <os/lock.h>
@interface ViewController ()
@property (nonatomic ,assign) int count;
@property (nonatomic ,assign) os_unfair_lock unfairLock;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.count = 10;
self.unfairLock = OS_UNFAIR_LOCK_INIT; // 初始化锁
}
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
for (int i = 0; i<10; i++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
os_unfair_lock_lock(&_unfairLock); // 加锁
self.count --;
NSLog(@"%d",self.count);
os_unfair_lock_unlock(&_unfairLock); // 解锁
});
}
}
@end
3.NSLock - 互斥锁
NSLock
是基于pthread
的封装
源码里面并没有设置递归锁属性,也就是说NSLock
不是一把递归锁!
API | 说明 |
---|---|
- (void)lock | 加锁 |
- (void)unlock | 解锁 |
- (BOOL)tryLock | 尝试加锁。成功返回YES,失败返回NO。 |
- (BOOL)lockBeforeDete:(NSDate *)limit | 在指定时间点之前获取锁,能够获取返回YES,获取不到返回NO。 |
@property (nullable ,copy) NSString *name | 锁名称 |
#import "ViewController.h"
@interface ViewController ()
@property (nonatomic ,assign) int count;
@property (nonatomic ,strong) NSLock *iLock;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.count = 10;
self.iLock = [[NSLock alloc] init]; // 初始化锁
}
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
for (int i = 0; i<10; i++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[self.iLock lock];
self.count --;
NSLog(@"%d",self.count);
[self.iLock unlock];
});
}
}
@end
NSLock
使用限制:不可以重复加锁和解锁(不适用于递归函数)。
// 错误案例
[self.iLock lock];
[self.iLock lock];
self.count --;
[self.iLock unlock];
[self.iLock unlock];
4.NSCondition - 互斥锁
API | 说明 |
---|---|
- (void)lock | 加锁 |
- (void)unlock | 解锁 |
- (void)wait | 阻塞当前线程,使线程进入休眠,等待唤醒信号。调用前必须已加锁。 |
- (void)waitUntilDate | 阻塞当前线程,使线程进入休眠,等待唤醒信号或者超时。调用前必须已加锁。 |
- (void)signal | 唤醒一个正在休眠的线程,如果要唤醒多个,需要调用多次。如果没有线程在等待,则什么也不做。调用前必须已加锁。 |
- (void)broadcast | 唤醒所有在等待的线程。如果没有线程在等待,则什么也不做。调用前必须已加锁。 |
@property (nullable ,copy) NSString *name | 锁名称 |
供不应求场景(消费>生产):
#import "ViewController.h"
@interface ViewController ()
@property (nonatomic ,assign) int count;
@property (nonatomic ,strong) NSCondition *iCondition;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.count = 10;
self.iCondition = [[NSCondition alloc] init]; // 初始化锁
}
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
[self nscondition_test];
}
#pragma mark -- NSCondition
- (void)nscondition_test {
// 生产
for (int i = 0; i < 50; i ++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[self my_production];
});
}
// 消费
for (int i = 0; i < 100; i ++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[self my_consumption];
});
}
}
- (void)my_production {
[self.iCondition lock];
self.count ++;
NSLog(@"生产了一个产品,现有产品 : %d个",self.count);
[self.iCondition signal]; // 唤醒一个wait正在休眠的线程
[self.iCondition unlock];
}
- (void)my_consumption {
[self.iCondition lock];
while (self.count == 0) { // 这里使用 if 会出现现有产品是负数的情况
[self.iCondition wait]; // 阻塞当前线程,使线程进入休眠,等待唤醒信号signal
}
self.count --;
NSLog(@"消费了一个产品,现有产品: %d个",self.count);
[self.iCondition unlock];
}
@end
分析 供不应求场景:
生产和消费都在不同的子线程下进行,所以执行的时机是不一定的,所以容易出现消费过快,导致当前现有产品个数出现负数的情况。
于是当产品个数为0的时候,就需要阻断消费的能力,就是在消费之前判断count == 0
去阻塞线程(调用wait
),在生产完成后告诉消费子线程可以去消费了(调用signal
)。
但是这里不能使用if (self.count == 0)
而使用while (self.count == 0)
,if
也会出现产品出现负数的情况。是因为signal
也有一个bug
:系统的虚假唤醒
。
在调用signal
的时候,就可能达到了broadcast
的效果,不是预期的signal : wait = 1:1
效果。解决这个虚假唤醒的情况就是使用while
代替if
。
NSCondition使用限制
:调用signal
去唤醒wait
的时候,不能达到1:1唤醒的效果。
5.NSConditionLock - 互斥锁
NSConditionLock
是基于NSCondition
的封装。目的是让NSConditionLock
自带条件探测
API | 说明 |
---|---|
- (void)lock | 加锁 |
- (void)unlock | 解锁 |
- (instancetype)initWithCondition:(NSinteger) | 初始化一个NSConditionLock对象 |
@property(readonly) NSInteger condition | 锁的条件 |
- (void)lockWhenCondition:(NSInteger)condition | 满足条件时加锁 |
- (BOOL)tryLock | 尝试加锁 |
- (BOOL)tryLockWhenCondition | 如果接受对象的condition与给定的condition相等,则尝试获取锁,不阻塞线程 |
- (void)unlockWithCondition:(NSInteger)condition | 解锁,重置锁的条件 |
- (BOOL)lockBeforDate:(NSDate *)limit | 在指定时间点之前获取锁 |
- (BOOL)lockWhenCondition:(NSInteger)condition beforeDate:(NSDate *)limit | 在指定的时间前获取锁 |
@property (nullable ,copy) NSString *name | 锁名称 |
#import "ViewController.h"
@interface ViewController ()
@property (nonatomic ,assign) int count;
@property (nonatomic ,strong) NSConditionLock *iConditionLock;
@end
@implementation ViewController
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
[self conditonLock_test];
}
#pragma mark -- NSConditionLock
- (void)conditonLock_test {
self.iConditionLock = [[NSConditionLock alloc] initWithCondition:3];
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[self.iConditionLock lockWhenCondition:3];
NSLog(@"1");
[self.iConditionLock unlockWithCondition:2];
});
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[self.iConditionLock lockWhenCondition:2];
NSLog(@"2");
[self.iConditionLock unlockWithCondition:1];
});
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[self.iConditionLock lockWhenCondition:1];
NSLog(@"3");
[self.iConditionLock unlockWithCondition:0];
});
}
@end
// 线程任务的执行顺序:1 2 3
NSConditionLock
能够达到控制线程执行任务顺序的目的。
6.NSRecursiveLock - 互斥锁(递归锁)
递归锁
:同一时刻只能被一条线程所拥有。
NSRecursiveLock
是基于pthread
的封装,并设置了递归属性。
NSConditionLock
是一把递归锁,可递归加锁解锁(可适用于递归函数)。
API | 说明 |
---|---|
- (void)lock | 加锁 |
- (void)unlock | 解锁 |
- (BOOL)tryLock | 尝试加锁。成功返回YES,失败返回NO。 |
- (BOOL)lockBeforeDete:(NSDate *)limit | 在指定时间点之前获取锁,能够获取返回YES,获取不到返回NO。 |
@property (nullable ,copy) NSString *name | 锁名称 |
#import "ViewController.h"
@interface ViewController ()
@property (nonatomic ,assign) int count;
@property (nonatomic ,strong) NSRecursiveLock *iRecursiveLock;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.count = 10;
self.iRecursiveLock = [[NSRecursiveLock alloc] init]; // 初始化锁
[self recursiveTest]; // 递归锁案例
}
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
for (int i = 0; i<10; i++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[self recursiveLock_test];
});
}
}
#pragma mark -- NSRecursiveLock
-(void)recursiveLock_test {
[self.iRecursiveLock lock];
self.count --;
NSLog(@"%d",self.count);
[self.iRecursiveLock unlock];
}
- (void)recursiveTest {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
static void (^recursiveMethod)(int);
recursiveMethod = ^(int value){
if (value > 0) {
[self.iRecursiveLock lock];
NSLog(@"%d",value);
recursiveMethod(value - 1);
[self.iRecursiveLock unlock];
}
};
recursiveMethod(10);
});
}
@end
注意:如果在不同线程进行递归调用的话,会出现问题,把recursiveTest
方法放到for
循环里
- (void)recursiveTest {
for (int i = 0; i < 5; i++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
static void (^recursiveMethod)(int);
recursiveMethod = ^(int value){
if (value > 0) {
[self.iRecursiveLock lock];
NSLog(@"%d",value);
recursiveMethod(value - 1);
[self.iRecursiveLock unlock];
}
};
recursiveMethod(10);
});
}
}
此时代码会因为子线程相互等待资源而造成线程死锁。
NSRecursiveLock 使用限制
:不能在多线程访问同一资源的情况下使用。
那我们希望达到多线程递归的效果的话,就可以使用@synchronized
。
7.@synchronized - 互斥锁(递归锁)
@synchronized
不管你几条线程,不管你是否递归调用,它都支持。
#import "ViewController.h"
@interface ViewController ()
@property (nonatomic ,assign) int count;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.count = 10;
[self synchronized_test]; // 递归锁案例
}
- (void)synchronized_test {
for (int i=0; i<5; i++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
static void (^recursiveMethod)(int);
recursiveMethod = ^(int value){
if (value > 0) {
@synchronized(self) {
NSLog(@"%d",value);
recursiveMethod(value - 1);
}
}
};
recursiveMethod(10);
});
}
}
@end
- 使用
@synchronized
同步代码示例:
NSObject *obj = [[NSObject alloc] init];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0), ^{
@synchronized(obj){
NSLog(@"线程1 start");
sleep(2);
NSLog(@"线程1 end");
}
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0), ^{
sleep(1);
@synchronized(obj){
NSLog(@"线程2");
}
});
// 线程1 start --> 线程1 end --> 线程2
@synchronized(obj)指令使用的obj为该锁的唯一标识,只有当标识相同时,才为满足互斥。
- 如果线程2中的
@synchronized(obj)
改为@synchronized(self)
,刚线程2就不会被阻塞,代码示例如下:
NSObject *obj = [[NSObject alloc] init];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0), ^{
@synchronized(obj){
NSLog(@"线程1 start");
sleep(2);
NSLog(@"线程1 end");
}
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0), ^{
sleep(1);
@synchronized(self){
NSLog(@"线程2");
}
});
-
@synchronized
还是个递归可重入锁,如下代码所示:
NSObject *obj = [[NSObject alloc] init];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0), ^{
@synchronized(obj){
NSLog(@"1开始");
@synchronized (obj) {
NSLog(@"2开始");
@synchronized (obj) {
NSLog(@"3");
}
NSLog(@"2完成");
}
NSLog(@"1结束");
}
});
@synchronized是个递归互斥锁
,同一个线程可以重复获得这个锁并进入执行执行块里面的代码而不会导致死锁。
@synchronized
的优点:
不需要在代码中显式的创建锁对象,便可以实现锁的机制;
递归互斥,同一个线程可以重复进入而不导致死锁。
@synchronized
的缺点:
效率低(在真机上不见得效率那么低)。@synchronized
块会隐式的添加一个异常处理例程来保护代码,该处理例程会在异常抛出的时候自动的释放互斥锁,这会增加额外的开销。同时为了实现递归互斥可重入,底层使用的是递归锁加上复杂的业务逻辑,也增加了不少的消耗。
@synchronized
加锁需要一个对象参数
,在选着对象参数
的时候要特别注意不能让对象参数
为nil
,否则加锁无效。
7.pthread_mutex
API | 说明 |
---|---|
pthread_mutex_init(pthread_mutex_t mutex, const pthread_mutexattr_t attr) | 初始化锁,pthread_mutexattr_t可用来设置锁的类型。 |
pthread_mutex_lock(pthread_mutex_t mutex); | 加锁 |
pthread_mutex_trylock(*pthread_mutex_t *mutex); | 加锁,但是上面方法不一样的是当锁已经在使用的时候,返回为EBUSY,而不是挂起等待,成功返回0.失败返回错误信息 |
pthread_mutex_unlock(pthread_mutex_t *mutex); | 解锁 |
pthread_mutex_destroy(pthread_mutex_t* mutex); | 使用完锁之后释放锁 |
pthread_mutexattr_setpshared(); | 设置互斥锁的范围 |
pthread_mutexattr_getpshared() | 获取互斥锁的范围 |
#pragma mark -- pthread_mutex
- (void)pthread_mutex_test {
//非递归加锁
pthread_mutex_t lock0;
pthread_mutex_init(&lock0, NULL);
pthread_mutex_lock(&lock0);
// 锁住的资源...
pthread_mutex_unlock(&lock0);
pthread_mutex_destroy(&lock0); // c对象,需要自己释放资源
//递归加锁
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);
pthread_mutex_destroy(&lock); // c对象,需要自己释放资源
}
8.信号量 dispatch_semaphore_t
API | 说明 |
---|---|
dispatch_semaphore_create(intptr_t value) | 创建信号量,并且创建的时候需要指定信号量的大小 |
dispatch_semaphore_wait(dispatch_semaphore_t dsema, diapatch_time_t timeout) | 等待信号量,如果信号量值为0,那么该函数就会一直等待(相当于阻塞当前线程),直到该函数等待的信号量的值大于等于1,该函数会对信号量的值进行减1操作,然后返回。 |
dispatch_semaphore_signal(dispatch_semaphore_t dsema) | 发送信号量,该函数会对信号量的值进行加1操作 |
#pragma mark -- dispatch_semaphore_t
- (void)dispatch_semaphore_t_test {
dispatch_semaphore_t sem = dispatch_semaphore_create(0);
dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSLog(@"任务1");
dispatch_semaphore_signal(sem);
});
dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSLog(@"任务2");
dispatch_semaphore_signal(sem);
});
dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSLog(@"任务3");
});
}
// 123
四、读写锁
读写锁实际是一种特殊的自旋锁
,它把对共享资源的访问者划分成读者和写者,读者只对共享资源进行读访问,写者则需要对共享资源进行写操作。这种锁相对于自旋锁而言,能提高并发性,因为在多处理器系统中,它允许同时有多个读者来访问共享资源,最大可能的读者数为实际的逻辑CPU数。写者是排他性的,一个读写锁同时只能有一个写者或多个读者(与CPU数相关),但不能同时既有读者又有写者。在读写锁保持期间也是抢占失效的。
如果读写锁当前没有读者,也没有写者,那么写者可以立刻获得读写锁,否则它必须自旋在那里,直到没有任何写者或读者。如果读写锁没有写者,那么读者可以立即获得该读写锁,否则读者必须自旋在那里,直到写者释放该读写锁。
读写锁可以实现多读单写功能(读读并发、读写互斥、写写互斥)
自己实现的一个简单读写锁案例:
#import "ViewController.h"
@interface ViewController ()
@property (nonatomic ,strong) dispatch_queue_t iQueue;
@property (nonatomic ,strong) NSMutableDictionary *dataDic;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.iQueue = dispatch_queue_create("AnAn", DISPATCH_QUEUE_CONCURRENT);
self.dataDic = [NSMutableDictionary new];
[self my_write: @"安安"];
}
- (void)test {
for (int i = 0; i < 10; i ++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[self my_read];
});
}
}
#pragma mark -- 读写锁
- (NSString *)my_read {
// 异步读取
__block NSString *ret;
dispatch_sync(self.iQueue, ^{
// 读取的代码
ret = self.dataDic[@"name"];
});
NSLog(@"%@",ret);
return ret;
}
- (void)my_write: (NSString *)name {
// 写操作
dispatch_barrier_async(self.iQueue, ^{
[self.dataDic setObject:name forKey:@"name"];
});
}
五、@synchronized源码分析
TLS(线程本地存储)
是一种在多线程时使用的技术,它可以使你的全局变量、静态变量以及局部静态、静态成员变量成为线程独立的变量,即每个线程的TLS变量之间互不影响
。
例如linux
下的全局变量errno
,windows
下的GetLastError
,线程A
在设置了一个错误信息后,线程B
又设置了一个错误信息,前一个线程设置的信息就被覆盖了。
解决方法就是将这个全局变量设置为TLS
变量,这样在用户看来errno
是一个全局变量,实际上它是每个线程独立的。
@synchronized
是如何实现递归互斥的?是如何实现可重入的呢?
带着这两个问题去分析源码。
#import <Cocoa/Cocoa.h>
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSObject *obj = [NSObject alloc];
@synchronized (obj) {
}
}
return NSApplicationMain(argc, argv);
}
将上面这段代码编译成.cpp
$ clang -rewrite-objc main.m
找到main函数
编译后的代码分析出,先去调用了objc_sync_enter
然后是一个_SYNC_EXIT
的构造函数和析构函数,构造函数什么都没做,析构函数出了作用域就会调用的objc_sync_exit
。
@synchronized
会被编译成objc_sync_enter
和objc_sync_exit
。
objc_sync_enter
objc_sync_exit
enter
和exit
逻辑都差不多,都是先判定参数obj
为nil
就啥也不做,obj
不为nil
通过id2data
获得一个SyncData
的数据结构,并且通过id2data
的第二个参数来区分是enter
调用的id2data
还是exit
调用的id2data
。SyncData
里拿出递归锁加锁解锁操作。
所以重点研究的是id2data
里面做了什么逻辑。
先来看看SyncData
数据结构:
可以看出SyncData
为每一个@synchronized 的参数object
分配了一把递归锁和记录有几个线程使用了block
。这两个分配记录就是多线程下递归调用的根本。(@synchronized(objc1)
相当于是SyncData->object=objc1
)
我们现在要关注的是SyncData
里的成员是如何在多线程下实现递归调用的。关键逻辑还得看id2data
里面做了什么。
-
id2data
第一部分
1.TLS快速缓存
中只存储了一个SyncData
数据,从这里取出的SyncData
的object
和@synchronized 的参数object
做对比(相同则说明是我们要找到的SyncData
)
2.如果找到了SyncData
,对lockCount
和threadCount
做记录更新,直接把SyncData
返回出去;
3.如果没有找到SyncData
,则进入第二部分。
-
id2data
第二部分
认识SyncCache
和SyncCacheItem
的数据结构
很明显线程缓存保存了好多的SyncData+lockCount
。
1.遍历带锁的每个线程的缓存,取出每一个SyncCacheItem
,取出SyncCacheItem
里面的SyncData
,SyncData
的object
和@synchronized 的参数object
做对比(相同则说明是我们要找到的SyncData
)
2.如果找到了SyncData
,对lockCount
和threadCount
做记录更新,直接把SyncData
返回出去;
3.如果没有找到SyncData
,则进入第三部分。
-
id2data
第三部分
全局Hash表
里有什么?在id2data
函数一开头就获取了全局Hash表
的元素啦
认识一下全局Hash表StripedMap
:
StripedMap
存储的是 真机下8张表/模拟器下64张表,每张表里存储的是很多的SyncList = SyncData单向链表 + lock
。
在TLS快速缓存
和线程缓存
中没有找到SyncData
,就会进入第三个步骤:
1.遍历全局Hash
表StripedMap
,取出的SyncData单向链表
的object
和@synchronized 的参数object
做对比(相同则说明是我们要找到的SyncData
),如果对比不是同一个,会找链表下一个元素对比。
2.当前没有与object
关联的SyncData
,则直接返回nil
3.找到一个没用过的SyncData
,就对其缓存到TLS快速缓存
和线程缓存
,并返回这个SyncData
-
id2data
第四部分
如果是TLS快速缓存
和线程缓存
和全局Hash表StripedMap
都没有找到,说明object
被第一次加锁,去创建一个SyncData
返回它。
总结
@synchronized
工作原理:(为什么能多线程递归调用)
@synchronized根据我们传入的对象,为每个线程构建一把递归锁,同时记录每个线程加锁的次数,通过两点,对每条线程用不同的递归锁进行加锁和解锁的操作,从而达到多线程递归调用的目的。