美文网首页你好 苹果iOS
iOS 线程底层 - 锁

iOS 线程底层 - 锁

作者: 顶级蜗牛 | 来源:发表于2022-06-01 11:39 被阅读0次

    苹果官方资源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方法调用的底层atomicnonatomic有什么区别。

    • objc_setProperty的源码分析:
    objc_setProperty reallySetProperty

    setter
    非原子属性nonatomic直接是做了赋值操作
    原子属性atomic在赋值之前加锁,赋值之后解锁

    • objc_getProperty的源码分析:
    objc_getProperty

    getter:
    非原子属性nonatomic返回了值
    原子属性atomic在取值前加锁,取值后解锁,再返回值

    那么原子属性atomicgetter/setter底层有加锁解锁操作,为什么不能保证线程安全的呢?
    因为原子属性atomic锁住资源的范围不够大。在self.count --;的时候,既有getter也有setter,可能就出现当getter的时候还没有return出去就被其它线程setter

    二、锁的性能分析

    模拟器下测试同时加锁解锁10万次的性能:

    模拟器

    iPhone6s真机下测试同时加锁解锁10万次的性能:

    iPhone 6s真机

    iPhone13真机下测试同时加锁解锁10万次的性能:

    iPhone13真机

    可以看出@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源码

    源码里面并没有设置递归锁属性,也就是说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下的全局变量 errnowindows下的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_enterobjc_sync_exit

    • objc_sync_enter
    objc_sync_enter
    • objc_sync_exit
    objc_sync_exit

    enterexit逻辑都差不多,都是先判定参数objnil就啥也不做,obj不为nil通过id2data获得一个SyncData的数据结构,并且通过id2data的第二个参数来区分是enter调用的id2data还是exit调用的id2dataSyncData里拿出递归锁加锁解锁操作。

    所以重点研究的是id2data里面做了什么逻辑。

    先来看看SyncData数据结构:

    SyncData

    可以看出SyncData为每一个@synchronized 的参数object分配了一把递归锁和记录有几个线程使用了block。这两个分配记录就是多线程下递归调用的根本。(@synchronized(objc1) 相当于是SyncData->object=objc1

    我们现在要关注的是SyncData里的成员是如何在多线程下实现递归调用的。关键逻辑还得看id2data里面做了什么。

    • id2data第一部分
    TLS快速缓存查找SyncData环节

    1.TLS快速缓存中只存储了一个SyncData数据,从这里取出的SyncDataobject@synchronized 的参数object做对比(相同则说明是我们要找到的SyncData
    2.如果找到了SyncData,对lockCountthreadCount做记录更新,直接把SyncData返回出去;
    3.如果没有找到SyncData,则进入第二部分。

    • id2data第二部分
    线程缓存查找SyncData环节

    认识SyncCacheSyncCacheItem的数据结构

    线程缓存结构

    很明显线程缓存保存了好多的SyncData+lockCount

    1.遍历带锁的每个线程的缓存,取出每一个SyncCacheItem,取出SyncCacheItem里面的SyncDataSyncDataobject@synchronized 的参数object做对比(相同则说明是我们要找到的SyncData
    2.如果找到了SyncData,对lockCountthreadCount做记录更新,直接把SyncData返回出去;
    3.如果没有找到SyncData,则进入第三部分。

    • id2data第三部分

    全局Hash表里有什么?在id2data函数一开头就获取了全局Hash表的元素啦

    认识一下全局Hash表StripedMap

    StripedMap存储的是 真机下8张表/模拟器下64张表,每张表里存储的是很多的SyncList = SyncData单向链表 + lock

    TLS快速缓存线程缓存中没有找到SyncData,就会进入第三个步骤:
    1.遍历全局HashStripedMap,取出的SyncData单向链表object@synchronized 的参数object做对比(相同则说明是我们要找到的SyncData),如果对比不是同一个,会找链表下一个元素对比。
    2.当前没有与object关联的SyncData,则直接返回nil
    3.找到一个没用过的SyncData,就对其缓存到TLS快速缓存线程缓存,并返回这个SyncData

    • id2data第四部分
      如果是TLS快速缓存线程缓存全局Hash表StripedMap都没有找到,说明object被第一次加锁,去创建一个SyncData 返回它。
    image.png

    总结@synchronized工作原理:(为什么能多线程递归调用)
    @synchronized会为每一条线程都添加一个递归锁

    相关文章

      网友评论

        本文标题:iOS 线程底层 - 锁

        本文链接:https://www.haomeiwen.com/subject/uvmiprtx.html