美文网首页
iOS 锁的底层分析(2)--各种的锁使用分析

iOS 锁的底层分析(2)--各种的锁使用分析

作者: 冼同学 | 来源:发表于2021-10-09 17:31 被阅读0次

前言

上一篇文章重点讲解了@synchronized的使用以及其底层原理,其实iOS开发中还提供了其他锁让我们使用,那么现在就开始来分析探索各种所的使用。

准备工作

1. NSLock和NSRecursiveLock的使用

首先我们引入下面的案例:

- (void)lg_testRecursive{
    for (int i= 0; i<10; i++) {
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            static void (^testMethod)(int);
            testMethod = ^(int value){
                    if (value > 0) {
                        NSLog(@"current value = %d",value);
                        testMethod(value - 1);
                    }
            };
            testMethod(10);
        });
    }
}

运行结果如下:

运行结果
其中testMethod静态block,因为多线程的影响,所以运行结果错乱无序的,但是这个案例还是出现了混乱的过程,这是什么回事呢?又有什么解决的办法呢?请继续往下。
案例分析
加载递归里面:
案例分析
在分析@synchronized时知道,数据结构SyncData中分装了recursive_mutex_t递归互斥锁@synchronized是一个支持多线程递归的锁。

1.1 NSLock

使用NSLock进行加锁,将锁加在业务代码外层,修改后代码如下:

案例分析
好明显 在业务代码外层使用NSLock是可以的,那么在业务代码中使用NSLock加锁呢?请往下看:
案例分析
此时的输出被阻塞了,这是为什么呢?因为在调用textMethod方法之后,lock加锁,内部又继续调用testMethod,导致重复加锁

注意:这里说明NSLock是不支持递归加锁

1.2 NSRecursiveLock

同样的流程,在业务代码外部使用NSRecursiveLock进行加锁,如下:

案例分析
由运行结果可以看出,使用NSRecursiveLock在业务代码外层加锁是可以的,那么放在业务代码内,请往下看:
案例分析
通过上面的运行结果发现,NSRecursiveLock在完成一次业务操作后就崩溃了。

注意:以上说明NSRecursiveLock支持单线程内的递归加锁,但是并不支持多线程递归

2. NSCondition的使用

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

2.1 NSCondition提供的API

  • [condition lock]; ⼀般⽤于多线程同时访问、修改同⼀个数据源,保证在同⼀时间内数据源只被访问、修改⼀次,其他线程的命令需要在lock外等待,只到unlock,才可访问。
  • [condition unlock];lock同时使⽤。
  • [condition wait];让当前线程处于等待状态
  • [condition signal]; CPU发信号告诉线程不⽤在等待,可以继续执⾏

2.2 案例分析

模拟生产和消费的需求,开启多个线程进行产品生产,同时开启多个线程进行销售产品。见下面案例:

#pragma mark **-- NSCondition**
- (void)lg_testConditon{
    NSCondition *testCondition = [[NSCondition alloc] init];

    // 创建-生产者
    for (int i = 0; i < 50; i++) {
       dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
            [self lg_producer];
        });
       dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
            [self lg_producer];
        });
    }
    
    // 创建-消费者
    for (int i = 0; i < 50; i++) {
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
            [self lg_consumer];
        });

        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
            [self lg_consumer];
        });
    }
}

- (void)lg_producer{
    [_testCondition lock]; // 操作的多线程影响

    self.ticketCount = self.ticketCount + 1;
    NSLog(@"生产一个 现有 count %zd",self.ticketCount);

    [_testCondition signal]; // 信号

    [_testCondition unlock];
}

- (void) lg_consumer {
    [_testCondition lock];  // 操作的多线程影响

    if (self.ticketCount == 0) {
        NSLog(@"等待 count %zd",self.ticketCount);
        [_testCondition wait];
    }

    // 注意消费行为,要在等待条件判断之后
    self.ticketCount -= 1;
    NSLog(@"消费一个 还剩 count %zd ",self.ticketCount);

    [_testCondition unlock];
}

注意:
生产线、消费线需要进行加锁处理,以保证多线程安全,于此同时,生产和消费之前也存在关系,比如库存数的安全!通过[_testCondition wait];模拟库存不足,让消费窗口停止消费;用[_testCondition signal];模拟已有库存可以消费,向等待的线程发送信号,开始执行。

运行结果如下:


运行结果

3. Foundation源码了解锁的封装

NSLockNSCondtionNSRecursiveLock底层都是对pthread的封装。它们的底层实现都封装在了OC环境下的Foundation框架中,由于Foundation框架不开源,那么我们只能从它的声明部分进行简单的分析探索。

锁的声明1
锁的声明2
那么看了上面的生面,可以知道定义了一个NSLocking协议,并且提供了lockunlock方法。所以我们使用的⽐如条件锁递归锁都会有对应的lock方法和unlock方法。
  • NSLock的底层分析
    源码中全局搜索NSLock:,查到NSLock锁定义的地方,见下图:

    NSLock定义
    然后进入定义中的pthread_mutex_init函数,如下:
    pthread_mutex_init
    得出:
    lock方法中,调用了pthread_mutex_lock函数;unlock方法中,调用了pthread_mutex_unlock函数。底层就是对pthread的封装。
  • NSRecursiveLock的底层分析
    采用相同的方式,搜索递归锁NSRecursiveLock,如下:

    NSRecursiveLock定义
    然后进入pthread_mutex_init方法,都是一样的。如下:
    pthread_mutex_init
    得出:
    发现其也是对pthread的封装,并且通过在init方法中,通过pthread_mutexattr_settype(attrs, Int32(PTHREAD_MUTEX_RECURSIVE))进行递归设置。
  • NSCondition的底层分析
    NSCondition底层也是对pthread进行了封装,见下图:

    NSCondition定义
    NSCondition其他操作
    得出:
    除了进行pthread_mutex互斥处理外,还对pthread_cond进行了处理,同时提供了waitsignalbroadcase方法。底层同样对phread的封装。
  • NSConditionLock的底层分析
    NSConditionLock底层没有直接操作pthread_mutex,如下:

    NSConditionLock定义
    但是NSConditionLock实现中提供了一个NSCondition属性和一个pthread_t属性,通过这两个属性,实现加锁线程方面的相关处理。所以也可以推断出NSConditionLock也是由phread封装得来的。

4. NSConditionLock的使用

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

4.1 NSConditionLock相关的API

  • [xxxx lock];表示xxx期待获得锁,如果没有其他线程获得锁(不需要判断内部的condition) 那它能执⾏此⾏以下代码,如果已经有其他线程获得锁(可能是条件锁,或者⽆条件锁),则等待,直⾄其他线程解锁
  • [xxx lockWhenCondition:A条件]; 表示如果没有其他线程获得该锁,但是该锁内部的condition不等于A条件,它依然不能获得锁,仍然等待。如果内部的condition等于A条件,并且没有其他线程获得该锁,则进⼊代码区,同时设置它获得该锁,其他任何线程都将等待它代码的完成,直⾄它解锁
  • [xxx unlockWithCondition:A条件]; 表示释放锁,同时把内部的condition设置为A条件
  • return = [xxx lockWhenCondition:A条件 beforeDate:A时间];表示如果被锁定(没获得锁),并超过该时间不再阻塞线程。但是注意:返回的值是NO,它没有改变锁的状态,这个函数的⽬的在于可以实现两种状态下的处理。

4.2 案例分析

案例如下:

- (void)lg_testConditonLock{

    // 创建条件锁 - 需要满足条件2,否则不执行
    NSConditionLock *conditionLock = [[NSConditionLock alloc] initWithCondition:2];

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
        [conditionLock lockWhenCondition:1];

        NSLog(@"线程 1");

        [conditionLock unlockWithCondition:0];
    });

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
        [conditionLock lockWhenCondition:2];

        sleep(0.1);

        NSLog(@"线程 2");

        [conditionLock unlockWithCondition:1];
    });

    dispatch_async(dispatch_get_global_queue(0, 0), ^{
       [conditionLock lock];

       NSLog(@"线程 3");

       [conditionLock unlock];
    });
}

运行结果见下图:


运行结果

分析结果:

  • NSConditionLock创建时,设置的条件时2,也就是说需要满足条件2,否则不执行
  • 线程 1调⽤[NSConditionLock lockWhenCondition:1],此时因为不满⾜当前条件,所以会进⼊waiting状态,当前进⼊到waiting时,会释放当前的互斥锁
  • 此时当前的线程 3调⽤[NSConditionLock lock:],本质上是调⽤[NSConditionLock lockBeforeDate:],这⾥不需要⽐对条件值,所以线程 3会打印。
  • 接下来线程 2执⾏[NSConditionLock lockWhenCondition:2],因为满⾜条件值,所以线程 2会打印,打印完成后会调⽤[NSConditionLock unlockWithCondition:1],这个时候将value设置为1,并发送boradcast, 此时线程 1接收到当前的信号,唤醒执⾏并打印。
  • ⾃此当前打印为 线程 3->线程 2->线程 1
  • [NSConditionLock lockWhenCondition:]:这⾥会根据传⼊的condition值和Value值进⾏对⽐,如果不相等,这⾥就会阻塞,进⼊线程池,否则的话就继续代码执⾏
  • [NSConditionLock unlockWithCondition:]: 这⾥会先更改当前的value值,然后进⾏⼴播唤醒当前的线程

5. 读写锁的实现

5.1 读写锁概念的分析理解

读写锁实际是⼀种特殊的⾃旋锁,它把对共享资源的访问者划分成读者和写者,读者只对共享资源进⾏读访问,写者则需要对共享资源进⾏写操作。

这种锁相对于⾃旋锁⽽⾔,能提⾼并发性,因为在多处理器系统中,它允许同时有多个读者来访问共享资源,最⼤可能的读者数为实际的逻辑CPU数。写者是排他性的,⼀个读写锁同时只能有⼀个写者或多个读者(与CPU数相关),但不能同时既有读者⼜有写者。在读写锁保持期间也是抢占失效的

⼀次只有⼀个线程可以占有写模式的读写锁, 但是可以有多个线程同时占有读模式的读写锁. 正是因为这个特性,当读写锁是写加锁状态时, 在这个锁被解锁之前, 所有试图对这个锁加锁的线程都会被阻塞.当读写锁在读加锁状态时, 所有试图以读模式对它进⾏加锁的线程都可以得到访问权, 但是如果线程希望以写模式对此锁进⾏加锁, 它必须直到所有的线程释放锁.

通常, 当读写锁处于读模式锁住状态时, 如果有另外线程试图以写模式加锁, 读写锁通常会阻塞随后的读模式锁请求, 这样可以避免读模式锁⻓期占⽤, ⽽等待的写模式锁请求⻓期阻塞.读写锁适合于对数据结构的读次数⽐写次数多得多的情况. 因为, 读模式锁定时可以共享, 以写模式锁住时意味着独占, 所以读写锁⼜叫共享-独占锁.

5.2 用到的API

  • pthread_rwlock_t lock; // 结构
  • pthread_rwlock_init(&lock, null); // 初始化
  • pthread_rwlock_rdlock(&lock); // 读加锁
  • pthread_rwlock_tryrdlock(&lock); // 读尝试加锁
  • pthread_rwlock_wdlock(&lock); // 写加锁
  • pthread_rwlock_trywdlock(&lock); // 写尝试加锁
  • pthread_rwlock_unlock(&lock); // 解锁
  • pthread_rwlock_destory(&lock); // 销毁

5.3 pthread_rwlock_t的使用

引入下面的案例,开启十个线程,同时进行读写操作, 要求:

  • 可以实现多读,多读不互斥
  • 单写,读写互斥
  • 写写互斥

实现代码如下:

#import <Pthread.h>

@interface ViewController ()

@property (nonatomic, assign) NSUInteger ticketCount;
@property (nonatomic,assign) pthread_rwlock_t lock;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.ticketCount = 0;
    [self rwTest];
}

- (void)rwTest {
    // 初始化
    pthread_rwlock_init(&_lock, NULL);
    // 全局队列
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    // 开启读写
    for (int i = 0; i<10; i++) {

        dispatch_async(queue, ^{
            [self read];
        });

        dispatch_async(queue, ^{
            [self read];
        });
        
        // 写
        dispatch_async(queue, ^{
            [self write];
        });
    }
}

// 读流程
-(void)read{
    // 读加锁
    pthread_rwlock_rdlock(&_lock);

    sleep(1);
    NSLog(@"读……%zd", self.ticketCount);
    
    // 解锁
    pthread_rwlock_unlock(&_lock);
}

// 写
-(void)write{
    // 写加锁
    pthread_rwlock_wrlock(&_lock);

    sleep(1);
    NSLog(@"写……%zd", ++self.ticketCount);

    // 解锁
    pthread_rwlock_unlock(&_lock);
}

@end

运行结果如下:

运行结果
通过上面的案例可以反映出读写锁同时只能有⼀个写者,并且可以保证多读同时进行

5.4 GCD栅栏函数的使用来实现读写锁

实现代码如下:

#import <pthread.h>
@interface ViewController ()
@property (nonatomic, assign) NSUInteger ticketCount;
// 并发队列-多读
@property (nonatomic, strong) dispatch_queue_t qCONCURRENT;
// 串行队列-限制读取顺序
@property (nonatomic, strong) dispatch_queue_t qSERIAL;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    self.ticketCount = 0;

    // 队列初始化
    self.qCONCURRENT = dispatch_queue_create("selfCONCURRENT", DISPATCH_QUEUE_CONCURRENT);

    self.qSERIAL = dispatch_queue_create("selfSERIAL", DISPATCH_QUEUE_SERIAL);

    [self go_testReadWrite];
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    // 写  栅栏函数,保证读写的互斥
    dispatch_barrier_async(self.qCONCURRENT, ^{
        [self writeAction];
    });
}

#pragma read wirte
- (void)go_testReadWrite{
    // 多线程读
    for (int i = 0; i < 2000; i++) {
        dispatch_async(self.qCONCURRENT, ^{
            [self readAction];
        });

        dispatch_async(self.qCONCURRENT, ^{
            [self readAction];
        });

        dispatch_async(self.qCONCURRENT, ^{
            [self readAction];
        });
    }
}

- (void)readAction {

    // 保证读取顺序
    dispatch_async(self.qSERIAL, ^{
        sleep(1);
        NSLog(@"读 ..... %ld ------ %@", self.ticketCount, [NSThread currentThread]);
    });
}

- (void)writeAction {
    sleep(1);
    NSLog(@"写 ..... %ld ------ %@", ++self.ticketCount, [NSThread currentThread]);
}

运行结果如下:


运行结果

通过案例发现读写锁的实现还是有多种的方式的。

总结

OC中锁的探索就到此结束了,在学习过程中收获满满。这对我们在开发过程中保证线程的安全有很大的帮助哦。

相关文章

  • iOS 锁的底层分析(2)--各种的锁使用分析

    前言 上一篇文章重点讲解了@synchronized的使用以及其底层原理,其实iOS开发中还提供了其他锁让我们使用...

  • iOS 锁

    iOS Lock(锁) 主要介绍常见的锁,以及synchronized、NSLock、递归锁、条件锁的底层分析 借...

  • iOS底层探索之多线程(十三)—锁的种类你知多少?

    你用过哪些锁?对于锁的种类你了解多少?锁的原理知道否?从本篇博客开始将对锁的相关内容进行分析! iOS底层探索之多...

  • OC-底层原理 26:锁的原理

    本文主要介绍常见的锁,以及synchronized、NSLock、递归锁、条件锁的底层分析 锁 借鉴一张锁的性能数...

  • iOS 锁的原理

    本文主要介绍常见的锁,以及synchronized、NSLock、递归锁、条件锁的底层分析 锁 借鉴一张锁的性能数...

  • iOS-底层原理27:锁的原理

    本文主要介绍常见的锁,以及synchronized、NSLock、递归锁、条件锁的底层分析 锁 借鉴一张锁的性能数...

  • iOS 底层原理 - 锁分析

    了解锁的机制会有助于项目开发,从而避免项目中多个线程访问同一块资源引发数据混乱的问题。 一 概念 锁的归类 基本...

  • iOS-底层原理 29:锁的原理

    本文主要介绍常见的锁,以及synchronized、NSLock、递归锁、条件锁的底层分析 线程安全:线程安全:当...

  • 锁的分析

    本文主要介绍常见的锁,以及synchronized、NSLock、递归锁、条件锁的底层分析 锁 先看一张大家都非常...

  • iOS中锁的分析

    iOS中锁的分析 ** @synchronized ** 递归互斥锁 // objc_sync_enter loc...

网友评论

      本文标题:iOS 锁的底层分析(2)--各种的锁使用分析

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