底层原理-25-锁(下)

这是我参与8月更文挑战的第10天,活动详情查看:8月更文挑战
上一篇我们介绍了@synchoroized的原理,以及使用。接下来继续介绍其它锁的情况。

1.NSLock

NSLock是OC层的封装,底层是对pthread_mutex的封装.
看一个日常多线程对数据操作导致不安全崩溃,异步函数在多个线程下对数组进行旧值release,新值retain。由于是异步某一时刻,多个线程同时对数组进行release操作,没来及retain,导致释放了,造成野指针崩溃。
image.png

加锁后,保证了安全不崩溃。

image.png
查看NSLock头文件

image.png
是在Foundation框架中,NSObject遵循了NSLocking协议。由于OC的Foundation框架不开源,因此借助Swift的开源框架Foundation来 分析NSLock的底层实现,其原理与OC是类似的。

image.png
pthread_mutex的操作,初始化后做了些关于pthread_mutex的属性的初始化。加锁和解锁就是对pthread_mutex互斥锁的操作。swift支持windows,看来以后是想成为一个跨平台的语言。

  • 递归操作分析

image.png
正常没问题,我们多线程下就会乱序

image.png
我们对多线程下递归加锁报错

image.png

我们改成NSRecursiveLock递归锁锁

image.png

虽然我们可以把整个递归区域加锁也能达到统一效果,但是意思不一样了,只是对多线程下某一代码块进行互斥。

image.png
我们在使用我们日常常用的@Synchorized,就是支持多线程和递归。

image.png

2. NSRecursiveLock

上面我们知道NSRecursiveLock是用来解决异步递归情况下,我们看下源码

image.png
基本上和NSLock一样都是对pthread_mutex的封装和操作,区别在于
NSLock
image.png
NSRecursiveLock
image.png
设置了类型为PTHREAD_MUTEX_RECURSIVE,因而支持了递归性。

3. NSCondition

NSCondition 的对象实际上作为一个锁和一个线程检查器,锁主要 为了当检测条件时保护数据源,执行条件引发的任务;线程检查器 主要是根据条件决定是否继续运行线程,即线程是否被阻塞

1:[condition lock];//一般用于多线程同时访问、修改同一个数据源,保证在同一 时间内数据源只被访问、修改一次,其他线程的命令需要在lock 外等待,只到unlock ,才可访问

2:[condition unlock];//与lock 同时使用\
3:[condition wait];//让当前线程处于等待状态\
4:[condition signal];//CPU发信号告诉线程不用在等待,可以继续执行
复制代码

经典模型是生产消费者模型比我我们买票和卖票,同一时间只有一个人能操作,其他人买票的话就要等待有了才可以买。

image.png
看下源码:
image.png
源码也是基于pthread_mutex的封装和操作,类似NSLock,多了一个条件和等待。 wait操作会阻塞线程,使其进入休眠状态signal操作是唤醒一个正在休眠等待的线程; broadcast会唤醒所有正在等待的线程。

4. NSConditionLock

NSConditionLock是条件锁,一旦一个线程获得锁,其他线程一定等待

//初始化
NSConditionLock *conditionLock = [[NSConditionLock alloc] initWithCondition:2];

//表示 conditionLock 期待获得锁,如果没有其他线程获得锁(不需要判断内部的 condition) 那它能执行此行以下代码,如果已经有其他线程获得锁(可能是条件锁,或者无条件 锁),则等待,直至其他线程解锁
[conditionLock lock]; 

//表示如果没有其他线程获得该锁,但是该锁内部的 condition不等于A条件,它依然不能获得锁,仍然等待。如果内部的condition等于A条件,并且 没有其他线程获得该锁,则进入代码区,同时设置它获得该锁,其他任何线程都将等待它代码的 完成,直至它解锁。
[conditionLock lockWhenCondition:A条件]; 

//表示释放锁,同时把内部的condition设置为A条件
[conditionLock unlockWithCondition:A条件]; 

// 表示如果被锁定(没获得 锁),并超过该时间则不再阻塞线程。但是注意:返回的值是NO,它没有改变锁的状态,这个函 数的目的在于可以实现两种状态下的处理
return = [conditionLock lockWhenCondition:A条件 beforeDate:A时间];

//其中所谓的condition就是整数,内部通过整数比较条件


复制代码

image.png
线程1的优先级最高,其次是线程3,最后是线程2。因此执行顺序是1-3-2,但是线程1设置了条件只有Condition1的时候才会执行,我们默认是3,所以不走,线程3没有条件限制,正常加锁解锁,之后线程2符号条件为3执行,最后解锁改变条件为1,这个时候线程1执行任务。

image.png
我们换一下条件线程2符号条件执行。

image.png
源码和NSCondition类似,在NSCondition的基础上进行设置了条件condition,可以自由搭配更加灵活。

5. atomic

原子锁我们经常遇到的,自旋锁添加后线程一直持有,处于一种忙等的状态,会堵塞线程。不过我们属性修饰的时候使用的是nonatomic不涉及安全的话,提高效率,atomic 是系统自带的自旋锁。
setter方法会根据修饰符调用不同方法,其中最后会统一调用reallySetProperty方法,其中就有atomic非atomic的操作

image.png
reallySetProperty
image.png
os_unfair_lock
image.png
使用os_unfair_lock进行加锁和解锁的处理
getter方法只对atomic的处理类似:

image.png

6. 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);

复制代码

7. 读写锁

顾名思义就是我们对数据操作的时候读取的时候可以多个线程进行读取,但是写入的时候只有一个操作。读写锁适合于对数据结构的读次数比写次数多得多的情况. 因为, 读模式锁定时可以共享, 以写
模式锁住时意味着独占, 所以读写锁又叫共享-独占锁.
我们主要满足以下几点:

  1. 可以供多线程读取
  2. 写入的时候只有单个线程写入,其它写入和读取操作等待。
  3. 读取的时候,有在进行的写入则等待写入完成后读取。
  4. 多个读取过程中依次读取,防止顺序不同

使用dispatch_barrier_async进行模拟:

#import "TestViewController.h"



@interface TestViewController ()

///<#name#>

@property (nonatomic, strong) dispatch_queue_t  queue;

@property (nonatomic, strong) dispatch_queue_t  queue2;

///

@property (nonatomic, strong) NSArray *data;

@end

@implementation TestViewController


- (void)viewDidLoad {

    [super viewDidLoad];

    // Do any additional setup after loading the view.

    

    

    self.queue = dispatch_queue_create("customQueue", DISPATCH_QUEUE_CONCURRENT);

    self.queue2 = dispatch_queue_create("serialQueue", DISPATCH_QUEUE_SERIAL);

    //赋值-写入

    for (int i =0; i<100; i++) {

        

        dispatch_async(self.queue, ^{

           

            [self writeAction:i];

            

            

        });

        

    }

    

    //取值-读取

    for (int i =0; i<100; i++) {

        

        dispatch_async(self.queue, ^{

           

                [self readAction];

            

        });

        

    }

    

}

-(void)readAction

{

    dispatch_async(self.queue2, ^{

        

        NSLog(@"读取中组数个数%ld",self.data.count);

    });

}

-(void)writeAction:(NSInteger)i

{

        dispatch_barrier_async(self.queue, ^{

            

            if (i%9 == 0) {

                self.data = @[@"2",@"wq",@"wq"];

            }else{

                

                self.data = nil;

            }

            NSLog(@"写入中******组数个数%ld",self.data.count);

        });

    

}

复制代码

image.png
dispatch_barrier_async之前探讨过必然要等线程中前面的任务完成才会执行栅栏函数的任务,读取中在串行队列进行读取,防止数据读取先后顺序不一样。

8. 总结

锁主要为了解决线程安全设计的,不同的锁性能有一定的差别,对应的场景也不同。以上我们了解了几种锁的大概原理和使用,开发中根据场景选择合适的锁,在一些简单的在递归多线程情况下使用@synchorized比较好,在真机上也进行了优化。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享