多线程Lock收藏

iOS 锁的原理分析(二)

2021-08-27  本文已影响0人  晨曦的简书

锁的分类

自旋锁

线程反复检查锁变量是否可用。由于线程在这一过程中保持执行, 因此是一种忙等待。一旦获取了自旋锁,线程会一直保持该锁,直至显式释放自旋锁。 自旋锁避免了进程上下文的调度开销,因此对于线程只会阻塞很短时间的场合是有效的。

互斥锁

是一种用于多线程编程中,防止两条线程同时对同一公共资源(比如全局变量)进行读写的机制。该目的通过将代码切片成一个一个的临界区,而达成。这里有两个要注意的点互斥跟同步,互斥就是当多个线程进行同一操作的时候,同一时间只有一个线程可以进行操作。同步是多个线程进行同一操作的时候,按照相应的顺序执行。互斥锁又分为两种情况,可递归和不可递归。

这里属于互斥锁的有:

条件锁

就是条件变量,当进程的某些资源要求不满足时就进入休眠,也就
是锁住了。当资源被分配到了,条件锁打开,进程继续运行。

递归锁

就是同一个线程可以加锁N次而不会引发死锁。

信号量

信号量(semaphore):是一种更高级的同步机制,互斥锁可以说是 semaphore 在仅取值 0/1 时的特例。信号量可以有更多的取值空间,用来实现更加复杂的同步,而不单单是线程间互斥。

读写锁

读写锁实际是一种特殊的互斥锁,它把对共享资源的访问者划分成读者和写者,读者只对共享资源 进行读访问,写者则需要对共享资源进行写操作。这种锁相对于自旋锁而言,能提高并发性,因为在多处理器系统中,它允许同时有多个读者来访问共享资源,最大可能的读者数为实际的逻辑CPU 数。写者是排他性的,一个读写锁同时只能有一个写者或多个读者(与CPU数相关),但不能同时既有读者又有写者。在读写锁保持期间也是抢占失效的。

如果读写锁当前没有读者,也没有写者,那么写者可以立刻获得读写锁,否则它必须自旋在那里, 直到没有任何写者或读者。如果读写锁没有写者,那么读者可以立即获得该读写锁,否则读者必须自旋在那里,直到写者释放该读写锁。

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

总结

其实基本的锁就包括了三类,自旋锁, 互斥锁 读写锁,其他的比如条件锁,递归锁,信号量都是上层的封装和实现!

pthread

Posix Thread 中定义有一套专⻔用于线程同步的函数 mutex,用于保证在任何时刻,都只能有一个线程访问该对象。 当获取锁操作失败时,线程会进入睡眠,等待锁释放时被唤醒。

  1. 创建和销毁
    A: POSIX 定义了一个宏 PTHREAD_MUTEX_INITIALIZER 来静态初始化互斥锁
    B: int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *mutexattr)
    C: pthread_mutex_destroy () 用于注销一个互斥锁

  2. 锁操作

NSLock 和 NSReLock 的分析

这里我们通过几个使用案例来介绍一下 NSLockNSReLock 这两种锁。

类似这样一段代码,当我们不加锁的情况下打印就会乱序,当我们在 testMethod(10) 执行前后分别加锁解锁就会循环按顺序打印。

类似这种,我们把 lockunlock 调整了下位置,就会出现类似死锁的现象,testMethod 递归执行。导致这个的原因是因为 NSLock 不具有可递归性。针对这种情况我们可以用 @synchronized 来解决,也可以用 NSRecursiveLock 来解决。因为在前面已经分析了 @synchronized,这里我们来试一下用 NSRecursiveLock 来解决,NSRecursiveLock 的使用频率也很高,我们在很多三方库里面在一些递归加锁的场景也可以看到 NSRecursiveLock 的应用。

当我们使用 NSRecursiveLock 的时候发现第一次可以打印,但是第二次就报错了,这是因为 NSRecursiveLock 具有可递归性,但是不支持多线程执行。

我们使用 @synchronized 既解决了递归调用,也解决了多线程的问题。

NSCondtion的分析

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

NSConditionapi介绍:

案例

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

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

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

类似这样一段代码,我们定义了生产方法 cx_producer 跟消费方法 cx_consumer,在 ticketCount 值修改的时候都会加锁,但是在消费方法里面会判断 ticketCount 小于零的时候就会进入等待,禁止消费,在生产方法 cx_producer 中判断 ticketCount 大于零的时候就会发送信号,继续执行。保证了事务的安全。

foundation 源码关于锁的封装

我们前面也讲了,例如 NSLock, NSRecursiveLock, NSCondition 等这些锁的底层都是基于 pthread 的封装,但是这些锁的底层都是在 NSFoundation 框架下实现的,但是 NSFoundation 框架并不开源,我们怎么来探究它们的底层实现呢?这里我们取了个巧,用 swiftfoundation 框架源码来进行探究。源码已上传到 github,感兴趣的小伙伴可以下载。

NSLock

在我们的代码下我们我们按住 control + command 键点击进入 NSLock 的头文件实现可以看到 NSLock 是继承于 NSObject 的一个类,只是遵循了 NSLocking 协议。因为这里只能看到协议的声明,具体实现我们打开源码来看一下。


NSRecursiveLock

上面案例分析的时候我们知道 NSRecursiveLock 相对于 NSLock 具有可递归性,对比他们的源码我们可以看到,只是因为 NSRecursiveLock 的底层 pthread_mutex_init 的时候多了一个 attrs 参数。它们的 lockunlock 方法的底层实现都是一样。

NSCondition

查看 NSCondition 的源码实现,我们发现 NSCondition 只是在初始化的时候多了一句 pthread_cond_init(cond, nil),它的 wait 方法底层调用的是 pthread_cond_wait(cond, mutex)。通过对这几种锁的分析我们可以看到它们的底层都是基于 pthread 的封装,当我们不知道使用哪种锁的时候,用 pthread 来实现是最完美的。

NSConditionLock

NSConditionLock 介绍

案例

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

NSConditionLock 执行流程分析

通过上面的案例我们会有几个疑问:

前面几种锁我们都是通过源码看到了底层的实现,但是当我们没有源码的时候我们又应该用哪种思路去分析呢?这里我们尝试一下通过反汇编跟来探索一下。这里环境用的是真机。

首先对 initWithCondition 方法下符号断点 -[NSConditionLock initWithCondition:],这里需要注意因为是对象方法,所以符号断点有点特殊。

断点之后我们可以看到汇编代码,这里 x0, x1, x2 分别代表方法的调用者, 调用方法, 参数。这里我们输出之后跟我们 OC 代码的调用都能一一对应上。这里我们重点追踪 bl 的执行,因为 bl 代表跳转。下面我们就一步一步的断点 bl 的执行。

这里 x0 输出暂时看不到,但是可以看到调用了 init 方法,并且参数是 2。

这里追踪到 NSConditionLock 调用了 init 方法,并且参数是 2。

这里 NSConditionLock 调用了 zone 方法,也就是内存开辟。

这里 NSCondition 调用了 allocWithZone 方法。

这里 NSCondition 调用了 init 方法。

这里就是 returnx0 就是返回对象,打印 x0 的内存结构,可以看到它有 NSCondition 跟 2 两个成员变量。


这里 NSDate 调用了 distantFuture 方法且参数为 1。

这里执行了 waitUntilDate 方法,进行了等待。

这里 NSConditionLock 调用了 lockWhenCondition:beforeDate:,第一个参数为 1,第二个参数为 [NSDate distantFuture] 的返回值。并且在这里新增符号断点 -[NSConditionLock lockWhenCondition:beforeDate:]

这里会断到 lockWhenCondition:beforeDate: 方法。


lockWhenCondition:beforeDate: 之后会再次来到 lockWhenCondition 方法,只是到了线程 4,参数变为了 2。

线程 4 中 lockWhenCondition 之后还会来到 lockWhenCondition:beforeDate: 方法。在 bl 这里 NSCondition 调用了 lock 方法。


这里会来到 unlockWithCondition 方法,并且也进行了加锁操作。

这里 NSCondition 调用了 broadcast 方法。

方法结束后 NSCondition 调用了 unlock 方法。

紧接着这里会来到我们 OC 代码中的线程一中的 lockWhenCondition:beforeDate: 方法,在这里又进行了一次解锁操作,跟上面我们两次加锁一一对应上了。

执行结束也返回了 0x0000000000000001,也就是 1。

最后执行 OC 代码中的线程一中的 unlockWithCondition 方法。然后又会执行上面 unlockWithCondition 方法的汇编流程。这里 1 代表不在等待。

反汇编分析与源码对比


通过对比我们可以看到我们反汇编分析的执行流程与源码逻辑一致。

GCD􏳖􏳗􏴯􏵮􏳖􏳗􏴯􏵮􏵯􏲫􏳖􏳗􏴯􏵮􏵯􏲫 实现多读单写

􏵰􏰚􏱼􏵱􏳜􏴄􏵲􏵳􏰘􏵴􏵵􏵶􏰋􏱀􏴯􏳓􏱋􏲘􏱮􏴒􏲷􏱝􏱿􏵥􏴺􏱟􏵷􏵵􏵶􏰋􏵸􏳆􏴒􏴼􏵹􏵵􏵶􏵺􏵻􏵼􏵰􏰚􏱼􏵱􏳜􏴄􏵲􏵳􏰘􏵴􏵵􏵶􏰋􏱀􏴯􏳓􏱋􏲘􏱮􏴒􏲷􏱝􏱿􏵥􏴺􏱟􏵷􏵵􏵶􏰋􏵸􏳆􏴒􏴼􏵹􏵵􏵶􏵺􏵻􏵼比如在内存中维护一份数据,有多处地方可能同时操作这块数据,怎么能保证数据库的安全呢?这里需要满足以下三点:

- (instancetype)init {
    self = [super init];
    if (self) {
        _currentQueue = dispatch_queue_create("chenxi", DISPATCH_QUEUE_CONCURRENT);
        _dic = [NSMutableDictionary dictionary];
    }
    return self;
}

- (void)cx_setSafeObject:(id)object forKey:(NSString *)key {
    key = [key copy];
    __weak __typeof(self)weakSelf = self;
    dispatch_barrier_async(_currentQueue, ^{
        [weakSelf.dic setObject:object forKey:key];
    });
}

- (id)cx_safeObjectForKey:(NSString *)key {
    __block id temp;
    __weak __typeof(self)weakSelf = self;
    dispatch_sync(_currentQueue, ^{
        temp = [weakSelf.dic objectForKey:key];
    });
    return temp;
}

函数调用者可以自由传递一个 NSMutableStringkey,并且能够在函数返回后修改它。因此我们必须对传入的字符串使用 copy 操作以确保函数能够正确地工作。如果传入的字符串不是可变的(也就是正常的 NSString 类型),调用 copy 基本上是个空操作。

上一篇下一篇

猜你喜欢

热点阅读