iOS底层-锁的原理

锁在我们开发中用的相对比较少,但是作为一个开发者,还是需要了解锁的原理;

下图是锁的性能数据图:

iShot2020-11-14 11.42.00.png

锁的归类

  • 自旋锁:线程反复检查锁变量是否可用。由于线程在这一过程中保持执行, 因此是一种忙等待。一旦获取了自旋锁,线程会一直保持该锁,直至显式释 放自旋锁。 自旋锁避免了进程上下文的调度开销,因此对于线程只会阻塞很 短时间的场合是有效的。
  • 互斥锁:是一种用于多线程编程中,防止两条线程同时对同一公共资源(比 如全局变量)进行读写的机制。该目的通过将代码切片成一个一个的临界区 而达成
  • 条件锁:就是条件变量,当进程的某些资源要求不满足时就进入休眠,也就是锁住了。当资源被分配到了,条件锁打开,进程继续运行
  • 递归锁:就是同一个线程可以加锁N次而不会引发死锁
  • 信号量:是一种更高级的同步机制,互斥锁可以说是semaphore在仅取值0/1时的特例。信号量可以有更多的取值空间,用来实现更加复杂的同步,而不单单是线程间互斥
  • 读写锁:读写锁实际是一种特殊的自旋锁,它把对共享资源的访问者划分成读者和写者,读者只对共享资源 进行读访问,写者则需要对共享资源进行写操作,这种锁相对于自旋锁而言,能提高并发性,它允许同时有多个读者来访问共享资源。

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

  • 互斥锁在上图包括:NSLock,pthread_mutex,@synchronized
  • 条件锁有:NSCondition,NSConditionLock
  • 递归锁:NSRecursiveLock,pthread_mutex(recursive)
  • 信号量:dispatch_semaphore

互斥锁与递归锁

在开发中,我们常用的大概是@synchronized,就从这个开始讲解;
下面使用@synchronized来举一个例子:
锁的应用是为了线程的安全执行,例如购票,不同线程购票不加锁的话,会出现同一张票被卖多次。
下面看一段代码:

- (void)saleTicket{
    @synchronized (self) {
        if (self.ticketCount > 0) {
            self.ticketCount--;
            sleep(0.1);
            NSLog(@"当前余票还剩:%ld张",self.ticketCount)
        }else{
            NSLog(@"当前车票已售罄");
        }
    }
}

通过@synchronized对票数的减少进行加锁之后,我们执行程序后,就不会出现问题。
下图是执行后的打印结果:

iShot2020-11-14 17.37.40.png

那既然@synchronized能够保证线程的安全,那就先去看一下它的底层原理;

首先创建一个iOS工程,在main.m函数中加上@synchronized (appDelegateClassName) { }这句代码,对main函数进行xcrun -sdk iphonesimulator clang -arch x86_64 -rewrite-objc main.m转换成cpp文件。

接下来就看一下main函数中的@synchronized转化之后变成的代码块:

  {
            id _rethrow = 0;
            id _sync_obj = (id)appDelegateClassName;
            objc_sync_enter(_sync_obj);

            try {
                struct _SYNC_EXIT { _SYNC_EXIT(id arg) : sync_exit(arg) {} ~_SYNC_EXIT() {objc_sync_exit(sync_exit);}
                    id sync_exit;
                } 
                _sync_exit(_sync_obj);

            } catch (id e) {_
            rethrow = e;
            }
            
            {
                struct _FIN {
                _FIN(id reth) : rethrow(reth) {}
    ~_FIN() { if (rethrow) objc_exception_throw(rethrow); }
    id rethrow;
    }
                _fin_force_rethow(_rethrow);}
}

可以看到它有一个objc_sync_enter_sync_exit
通过对objc_sync_enter的断点查看,得知@synchronized的底层在libobjc.A.dylib中。

下图是objc_sync_enter的源码实现,可以看到它传入了obj之后,就会进行处理,而没有传入时,就会调用objc_sync_nil()函数,这个函数没有任何实现。

iShot2020-11-14 18.06.23.png

也就是说,当@synchronized(nil)括号中传入的是nil,它什么都不做,无法起作用。
在有值的情况下,会对mutex进行lock()加锁函数的调用,那么看一下objc_sync_exit的函数实现:

iShot2020-11-14 18.13.09.png

可以看到在objc_sync_exit中会对mutex调用tryUnlock()解锁函数。

那么重点就是SyncData了,它是一个结构体:

iShot2020-11-14 18.19.45.png

而结构体中最重要的就是recursive_mutex_t

iShot2020-11-14 18.22.11.png

它是一个recursive_mutex_tt类,在这个类中有lockunlock两个方法,是一个递归锁。

那么回到objc_sync_enter,锁的创建是由id2data创建的,这个函数代码很多,下图是一部分代码,主要功能是通过kvc的方式拿到data值;

iShot2020-11-14 18.34.24.png

if(data)判断中,有对ACQUIRERELEASE的方法实现:

iShot2020-11-14 18.35.19.png

其中lockCount表示被锁了多少次,可重入,比递归锁功能更强大,因为在查看SyncData时,它是一个链表结构。
上面的代码是在SUPPORT_DIRECT_THREAD_KEYS的情况下的,而不在这个情况下,也跟上面的代码差不多;
SUPPORT_DIRECT_THREAD_KEYS是从线程栈缓存的形式,而endif是从cache的形式获取去缓存。

而如果第一次加载时,就会从下面的代码执行:


iShot2020-11-14 18.48.14.png
iShot2020-11-14 18.49.42.png

也就是说,第一次进来,会通过kvc对tls进行设值和标记,设置threadCount = 1lockCount = 1,在线程栈存空间和缓存空间中都会进行处理。

那么一个完整的流程也就很清晰了。

总结:@synchronized整个流程其实就是一张哈希表,因为底层封装的是recursive_mutex_t,所以是一把递归锁,扩展了递归锁里面增加了lockCount,防止多线程的重入,增加了threadCount进行处理。

@synchronized这把锁的性能是比较低的,因为里面有很多链表的查询,缓存,下层代码的查找,导致了性能是比较差;但是为什么用的多呢,因为方便简单,好用。

下面来看一段代码:

for (int i = 0; i < 200; i++) {
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            _testArray = [NSMutableArray array];
        });
    }

这一段代码是使用异步函数创建数组对象,这一段代码是有问题的,因为不断的初始化导致了问题,没销毁就创建,多个线程创建同一个对象,释放一次还好,释放多次,僵尸对象,就造成野指针。

那么防止问题的产生可以进行加锁;例如使用@synchronized(),那么括号内的参数可以填什么呢?
如果是填_testArray,那么还是会存在问题,因为存在某一个临界点,_testArray会变成nil,那么锁nil就会出问题;可以进行锁self,因为self是持有者,它是有一个生命周期的。

那么除了用@synchronized,在性能和对objc的生命周期不明确时,还可以使用NSLock
在创建数组前执行[lock lock],在创建之后执行[lock unlock];

下面来研究一下NSRecursiveLockNSLock这两把锁的使用,看下面一段代码:

NSRecursiveLock *recursiveLock = [[NSRecursiveLock alloc] init];
 NSLock *lock = [[NSLock alloc] init];
for (int i= 0; i<100; 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函数前调用[lock lock]testMethod(10)之后执行[lock unlock];那这样做产生的后果就是它会循环10次从10到1的结果,这是一种解决方法;

那如果在testMethod方法里的if判断外加锁在if判断外解锁,这样会产生循环递归问题,因为Lock是一把简单的互斥锁,当执行testMethod进行加锁,之后又调用testMethod,又一次加锁,也就是不同的线程进行加锁,当想要解锁时,因为其他线程已经加锁了,需要等待其他线程进行解锁。

那么NSLock是无法解决这种递归的特性,使用@synchronized也是可以的,但是效果也是执行10次从10到1。

那么使用NSRecursiveLock这把递归锁,是可以很好的解决这个问题的。

testMethod方法前调用[recursiveLock lock];if判断结束后调用[recursiveLock unlock];是可以解决问题的。

NSRecursiveLockLock的底层都是在pthread的基础上进行封装的;而他们的代码实现大部分是一样的,但是NSRecursiveLock的初始化的地方与NSLock是有区别的,看下面两张图片:

底层源码是来自swift的Foundation框架。


NSLock.png
NSRecursiveLock.png

那么关于NSLockNSRecursiveLock的使用就介绍完了,总的来说,使用是比较麻烦的,在使用便捷方面,@synchronized更简单、实用,而这两把锁就比较复杂,但是性能会比较高一些。

条件锁

下面来研究一下NSCondition这把锁;

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

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

它适用于一个生产消费者模型;
看代码:

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

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

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

上面的代码执行流程需要先生产出来,才能消费,当消费多了,还没生产完,就需要等待。
那么为了避免多线程的影响,需要对生产的时候进行加锁,防止它还为生产完,就进行消费了,当生产完了,发送一个信号,再进行解锁。

那么新增需求,当我们需要对事务进行顺序处理时,如何使用锁来处理;那就要介绍一下NSConditionLock这把条件锁了,
看代码:

// 信号量
    NSConditionLock *conditionLock = [[NSConditionLock alloc] initWithCondition:2];
    
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
         [conditionLock lockWhenCondition:1]; // conditoion = 1 内部 Condition 匹配
        // -[NSConditionLock lockWhenCondition: beforeDate:]
        NSLog(@"线程 1");
         [conditionLock unlockWithCondition:0];
    });
    
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
       
        [conditionLock lockWhenCondition:2];
        sleep(0.1);
        NSLog(@"线程 2");
        // self.myLock.value = 1;
        [conditionLock unlockWithCondition:1]; // _value = 2 -> 1
    });
    
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
       
       [conditionLock lock];
       NSLog(@"线程 3");
       [conditionLock unlock];
    });

这把锁的特殊性在于这把锁是有一个条件,通过设置信号量lockWhenCondition,在执行时会对信号量进行匹配,首先执行线程3,因为它没有设置信号量,然后信号量进行匹配,由于初始化时设置了信号量为2,因此先执行线程2,在里面又设置了解锁信号量为1,就匹配线程1,如果解锁信号量与线程1的信号量不匹配,那么线程1不会执行。

注意,线程3与线程2的顺序有可能是不确定的,如果线程3比较耗时,那么有可能会先执行线程2,再执行线程1,最后执行线程3.

那么关于其他的一些锁这边就不再介绍了。

你可能感兴趣的:(iOS底层-锁的原理)