iOS 锁分析下:各种锁的使用&源码分析&读写锁

484 阅读4分钟

准备工作

一、锁的分类

互斥锁

互斥锁有互斥同步两条特性,互斥性:当多条线程同时处理一个任务时,一条线程在处理任务,其他线程都不能再进行处理,只有该任务结束运行后才可以运行。同步性:首先要满足互斥性,还要有执行的顺序。常用的有:

  • @synchronized
  • NSLock
  • pthread_mutex

自旋锁

自旋锁相当于互斥锁+忙等,当其检测到资源不可用时,会保持一种忙等的状态,直到获取该资源。它的优势在于避免了上下文的切换,非常适合于堵塞时间很短的场合。缺点则是在忙等的状态下会不停的检测状态,会占用cpu资源。常用的有:

  • OSSpinLock
  • atomic

条件锁

通过一些条件来控制资源的访问,当然条件是会发生变化的。常用的有:

  • NSCondition
  • NSConditionLock

信号量

是一种高级的同步机制。互斥锁可以认为是信号量取值0/1时的特例,可以实现更加复杂的同步。常用的有:

  • dispatch_semaphore

递归锁

它允许同一线程多次加锁,而不会造成死锁。递归锁是特殊的互斥锁,主要是用在循环或递归操作中。常用的有:

  • pthread_mutex(recursive)
  • NSRecursiveLock

读写锁

是并发控制的一种同步机制,也称“共享-互斥锁”,也是一种特殊的自旋锁。它把对资源的访问者分为读者和写者,它允许同时有多个读者访问资源,但是只允许有一个写者来访问资源。常用的有:

  • pthread(rwlock)
  • dispatch_barrier_async / dispatch_barrier_sync

二、NSLock 和 NSRecursiveLock 的分析

先看下面的示例代码,外层是for循环,内层是block的递归调用:

- (void)ssl_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);
        });
    }
}

查看执行结果:

image.png

  • 可以看到value值是乱序打印的,如果我们想要顺序打印,就要给代码加锁,那么该怎么加锁呢。

NSLock 示例

testMethod(10);的执行前后加上NSLock锁:

image.png

  • 没有问题,可以顺序打印。

有的时候,我们喜欢把加锁和业务代码写在一起,也就是在testMethodblock中加锁:

image.png

  • 只打印了一个10,为什么呢,因为这是个递归函数,会递归加锁,而NSLock是非递归锁,所以无法正常执行。

NSRecursiveLock 示例

NSRecursiveLock是递归锁,我们把NSLock换成NSRecursiveLock,运行程序:

image.png

  • 程序从101只顺序打印了1次,这又是为什么呢,因为NSRecursiveLock只是递归锁,可以解决递归性但是解决不了多线程性,那么既要满足递归性又要满足多线程性就需要用到我们 上一篇 分析的@synchronized

@synchronized 示例

使用@synchronized,程序正常打印:

image.png

三、NSCondition 的分析

简介

SCondition 是一个条件锁,在日常开发中使用较少,与信号量有点相似:线程需要满足条件才会往下走,否则会堵塞等待,直到条件满足,是经典的生产消费者模型

NSCondition的对象实际上作为一个和一个线程检查器

  • 主要为了当检测条件时保护数据源,执行条件引发的任务
  • 线程检查器主要是根据条件决定是否继续运行线程,即线程是否被阻塞

相关函数

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

//与lock 同时使用
[condition unlock];

//让当前线程处于等待状态
[condition wait];

//CPU发信号告诉线程不用在等待,可以继续执行
[condition signal];

生产消费者模型 示例

- (void)viewDidLoad {
    [super viewDidLoad];
    [self ssl_testConditon];
}

- (void)ssl_testConditon {
    _testCondition = [[NSCondition alloc] init];
    //创建生产-消费者
    for (int i = 0; i < 50; i++) {
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
            [self ssl_producer];
        });
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
            [self ssl_consumer];
        });

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

- (void)ssl_producer {
    [_testCondition lock]; // 操作的多线程影响
    self.ticketCount = self.ticketCount + 1;
    NSLog(@"生产一个 现有 count %zd",self.ticketCount);
    [_testCondition signal]; // 信号
    [_testCondition unlock];
}

- (void)ssl_consumer {
     [_testCondition lock];  // 操作的多线程影响
    if (self.ticketCount == 0) {
        NSLog(@"等待 count %zd",self.ticketCount);
        [_testCondition wait];
    }
    //注意消费行为,要在等待条件判断之后
    self.ticketCount -= 1;
    NSLog(@"消费一个 还剩 count %zd ",self.ticketCount);
     [_testCondition unlock];
}

执行结果:
生产一个 现有 count 1
消费一个 还剩 count 0 
等待 count 0
等待 count 0
生产一个 现有 count 1
消费一个 还剩 count 0 
生产一个 现有 count 1
消费一个 还剩 count 0 
等待 count 0
生产一个 现有 count 1
...
  • 生产者生产,消费者消费,消费者消费生产者生产的,如果不能生产就不能消费。
  • 生产者和消费者,本身要是安全的要进行加锁,它们之间又有联系,需要信号的控制,消费为0时要进行等待,生产有了以后不能让消费一直等待,要发送信号消费者就可以结束等待继续执行。

四、foundation 源码关于锁的封装

我们看下NSLock的定义:

image.png

  • lockunlockNSLocking的协议方法,是一个锁的协议,很多锁都有这两个方法,接下来结合swift-corelibs-foundation源码分析NSLock到底是怎么实现的。

NSLock 源码分析

查看NSLock源码:

image.png

  • 可以看到NSLock源码中确实是实现了NSLocking协议,初始化时,本质上是对pthread_mutex进行的封装,调用的都是pthread_mutex的函数。

查看lockunlock的实现:

image.png

  • 可以看到lockunlock也是对pthread_mutex进行的封装。

NSRecursiveLock 源码分析

查看NSRecursiveLock源码:

image.png

  • 可以看到NSRecursiveLock也实现了NSLocking协议,初始化也是用pthread_mutex进行的封装,不同之处在于NSRecursiveLock中有设置PTHREAD_MUTEX_RECURSIVE这个type,这里就决定了它是个递归锁

NSRecursiveLocklockunlock的实现和NSLock基本是一样的,都是对pthread_mutex的封装:

image.png

NSCondition 源码分析

查看NSCondition源码:

image.png

  • NSCondition也实现了NSLocking协议,初始化也是用pthread_mutex进行的封装,区别就在于封装了cond的相关操作,使它拥有了waitsignal等功能。

lockunlock也是对pthread_mutex的封装:

image.png

五、NSConditionLock 分析

NSConditionLock 示例

看下面的示例代码及执行结果:

- (void)ssl_testConditonLock{

    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];
    });
    //
}

执行结果:
线程 3
线程 2
线程 1
  • 线程 2一定在线程 1之前先执行,因为我们初始化conditionLock时指定的condition等于2,所以lockWhenCondition:2可以正常执行,unlockWithCondition:1以后lockWhenCondition:1才能执行。
  • 线程 3中只是普通的lock所以是可以正常执行,线程 2线程 3本来是乱序的,但我们在线程 2加了sleep(0.1)延迟操作,所以线程 3先执行。

NSConditionLock 源码分析

初始化源码分析

打开源码,查看NSConditionLock的初始化:

image.png

  1. 通过NSCondition()初始化了成员变量_cond
  2. 初始化_value成员变量为0

lockWhenCondition: 源码分析

查看lockWhenCondition:源码:

image.png

继续进入whenCondition:before:

image.png

  • _value != condition时进行while死循环,循环中判断等待是否超时,如果超时了返回false,如果等到_value == condition向下执行返回true,任务正常执行。

unlockWithCondition: 源码分析

unlockWithCondition:

image.png

  • 更新_value值为传进来的condition
  • _cond.broadcast()进行广播通知,通知等待中的任务_value值的改变。

六、读写锁

读写锁 概念

  • 读写锁实际是一种特殊的互斥锁,它把对共享资源的访问者划分成读者和写者,读者只对共享资源进行读访问,写者则需要对共享资源进行写操作。这种锁相对于自旋锁而言,能提高并发性,因为在多处理器系统中,它允许同时有多个读者来访问共享资源,最大可能的读者数为实际的逻辑CPU数。写者是排他性的,一个读写锁同时只能有一个写者或多个读者(与CPU数相关),但不能同时既有读者又有写者。在读写锁保持期间也是抢占失效的。
  • 如果读写锁当前没有读者,也没有写者,那么写者可以立刻获得读写锁,否则它必须自旋在那里,直到没有任何写者或读者。如果读写锁没有写者,那么读者可以立即获得该读写锁,否则读者必须自旋在那里,直到写者释放该读写锁。

  • 一次只有一个线程可以占有写模式的读写锁, 但是可以有多个线程同时占有读模式的读写锁,正是因为这个特性,当读写锁是写加锁状态时, 在这个锁被解锁之前, 所有试图对这个锁加锁的线程都会被阻塞
  • 当读写锁在读加锁状态时, 所有试图以读模式对它进行加锁的线程都可以得到访问权, 但是如果线程希望以写模式对此锁进行加锁, 它必须直到所有的线程释放锁。
  • 通常,当读写锁处于读模式锁住状态时, 如果有另外线程试图以写模式加锁, 读写锁通常会阻塞随后的读模式锁请求, 这样可以避免读模式锁⻓期占用, 而等待的写模式锁请求⻓期阻塞。
  • 读写锁适合于对数据结构的读次数比写次数多得多的情况. 因为, 读模式锁定时可以共享, 以写模式锁住时意味着独占, 所以读写锁又叫共享-独占锁

总结一下就是:

  • 多读单写
  • 写 和 写互斥
  • 写 和 读互斥
  • 读、写 不能阻塞任务执行

栅栏函数 实现读写锁

@interface ViewController ()
@property (nonatomic, strong) dispatch_queue_t ssl_safeQueue;
@property (nonatomic, strong) NSMutableDictionary *mDict;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    [self ssl_safeSetter:@"111" time:4];
    [self ssl_safeSetter:@"222" time:3];
    [self ssl_safeSetter:@"333" time:2];
    [self ssl_safeSetter:@"444" time:1];
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    for (int i = 0; i < 10000; i++) {
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            NSLog(@"读取情况:%@ -- %d -- %@",[self ssl_safeGetter],i,[NSThread currentThread]);
        });
    }
}

- (void)ssl_safeSetter:(NSString *)name time:(int)time {
    dispatch_barrier_async(self.ssl_safeQueue, ^{
        sleep(time);
        [self.mDict setValue:name forKey:@"ssl"];
        NSLog(@"写情况:%@ - %@",self.mDict[@"ssl"],[NSThread currentThread]);
    });
}

- (NSString *)ssl_safeGetter {
    __block NSString *result;
    dispatch_sync(self.ssl_safeQueue, ^{
        result = self.mDict[@"ssl"];
    });
    return result;
}

- (dispatch_queue_t)ssl_safeQueue
{
    if (!_ssl_safeQueue) {
        _ssl_safeQueue = dispatch_queue_create("ssl", DISPATCH_QUEUE_CONCURRENT);
    }
    return _ssl_safeQueue;
}

- (NSMutableDictionary *)mDict
{
    if (!_mDict) {
        _mDict = [NSMutableDictionary new];
    }
    return _mDict;
}
@end
  • 写操作中我们使用dispatch_barrier_async函数,使用这个函数前面的任务执行完才会执行这里,这里执行完才会执行后面的任务,这样可以达到写和写互斥的目的。
  • 读操作也是放在了self.ssl_safeQueue中,这样读和写互斥就完成了,也就真正意义的达成单写的目的,注意这里的dispatch_sync只是阻塞当前任务的线程,否则 result 就获取不到值了。
  • 因为self.ssl_safeQueue并发队列,所以多读也是没问题的。