锁的原理

3,416 阅读9分钟
  • 锁的种类

    • 互斥锁(Mutual exclusion,缩写 Mutex)
      防止两条线程同时对同一公共资源(比如全局变量)进行读写的机制。当获取锁操作失败时,线程会进入睡眠,等待锁释放时被唤醒。 互斥锁又分为递归锁和非递归锁。
      • 递归锁
        可重入锁,同一个线程在锁释放前可再次获取锁,即可以递归调用。
        例如:@synchronized
      • 非递归锁
        不可重入,必须等锁释放后才能再次获取锁。
        例如:NSLockpthread_mutex
    • 自旋锁
      线程反复检查锁变量是否可⽤。由于线程在这⼀过程中保持执⾏, 因此是⼀种忙等待。⼀旦获取了⾃旋锁,线程会⼀直保持该锁,直⾄显式释放⾃旋锁。⾃旋锁避免了进程上下⽂的调度开销,因此对于线程只会阻塞很 短时间的场合是有效的。与互斥锁最大的区别就在于互斥锁会进入休眠状态等待被唤醒,而自旋锁则不会休眠处于忙等待状态
    • 条件锁
      就是条件变量,当进程的某些资源要求不满⾜时就进⼊休眠,也就 是锁住了。当资源被分配到了,条件锁打开,进程继续运⾏
      • NSCondition
      • NSConditionLock
    • 递归锁
      就是同⼀个线程可以加锁N次⽽不会引发死锁
      • NSRecursiveLock
      • pthread_mutex(recursive)
    • 信号量(semaphore)
      是⼀种更⾼级的同步机制,互斥锁可以说是semaphore在仅取值0/1时的特例。信号量可以有更多的取值空间,⽤来实 现更加复杂的同步,⽽不单单是线程间互斥。
      • dispatch_semaphore
    • 读写锁
      读写锁实际是⼀种特殊的⾃旋锁,它把对共享资源的访问者划分成读者和写者,读者只对共享资源 进⾏读访问,写者则需要对共享资源进⾏写操作。这种锁相对于⾃旋锁⽽⾔,能提⾼并发性,因为 在多处理器系统中,它允许同时有多个读者来访问共享资源,最⼤可能的读者数为实际的逻辑CPU 数。写者是排他性的,⼀个读写锁同时只能有⼀个写者或多个读者(与CPU数相关),但不能同时 既有读者⼜有写者。在读写锁保持期间也是抢占失效的。 如果读写锁当前没有读者,也没有写者,那么写者可以⽴刻获得读写锁,否则它必须⾃旋在那⾥, 直到没有任何写者或读者。如果读写锁没有写者,那么读者可以⽴即获得该读写锁,否则读者必须 ⾃旋在那⾥,直到写者释放该读写锁。
      ⼀次只有⼀个线程可以占有写模式的读写锁, 但是可以有多个线程同时占有读模式的读写锁. 正 是因为这个特性, 当读写锁是写加锁状态时, 在这个锁被解锁之前, 所有试图对这个锁加锁的线程都会被阻塞. 当读写锁在读加锁状态时, 所有试图以读模式对它进⾏加锁的线程都可以得到访问权, 但是如果 线程希望以写模式对此锁进⾏加锁, 它必须直到所有的线程释放锁. 通常, 当读写锁处于读模式锁住状态时, 如果有另外线程试图以写模式加锁, 读写锁通常会阻塞 随后的读模式锁请求, 这样可以避免读模式锁⻓期占⽤, ⽽等待的写模式锁请求⻓期阻塞. 读写锁适合于对数据结构的读次数⽐写次数多得多的情况. 因为, 读模式锁定时可以共享, 以写 模式锁住时意味着独占, 所以读写锁⼜叫共享-独占锁.
    • 总结
      基本的锁其实就只有三类:自旋锁、互斥锁、读写锁。其他的比如条件锁、递归锁、信号量都是上层的封装实现
  • 锁的用法及源码探索

    在探索使用及源码之前先看一张图,上面展示的所有锁的性能对比aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy8yMzY3NTQ0NS1lYzhkMWY0ODJiZjFjMmMzLnBuZw.png
    • 准备工作

      在探索源码之前先写一个票的demo,先看没有加锁的情况下运行是怎样的

        - (void)viewDidLoad {
            [super viewDidLoad];
            // Do any additional setup after loading the view.
            self.ticketCount = 20;
            [self lg_testSaleTicket];
        }
      
      
        - (void)lg_testSaleTicket{
            dispatch_async(dispatch_get_global_queue(0, 0), ^{
                for (int i = 0; i < 5; i++) {
                    [self saleTicket];
                }
            });
      
            dispatch_async(dispatch_get_global_queue(0, 0), ^{
                for (int i = 0; i < 5; i++) {
                    [self saleTicket];
                }
            });
      
            dispatch_async(dispatch_get_global_queue(0, 0), ^{
                for (int i = 0; i < 3; i++) {
                    [self saleTicket];
                }
            });
      
            dispatch_async(dispatch_get_global_queue(0, 0), ^{
                for (int i = 0; i < 10; i++) {
                    [self saleTicket];
                }
            });
        }
      
        - (void)saleTicket{
            if (self.ticketCount > 0) {
                self.ticketCount--;
                sleep(0.1);
                NSLog(@"当前余票还剩:%ld张",self.ticketCount);
      
            }else{
                NSLog(@"当前车票已售罄");
            }
      
        }
      

      运行结果: image.png

    • @synchronized

      从上面的实例代码中可以看到多线程访问同一个数据的时候会出现问题,可能同时一个多个线程访问一个数据,此时为了避免这种问题可以加锁同时只让一个线程访问数据,具体用法如下:image.png再看运行结果: image.png发现测试就没有上述问题了。
      再看源码实现 首先开启汇编调试 image.pngimage.png 发现底层会调用两个方法:objc_sync_exitobjc_sync_enter,也可以通过clang查看编译后的文件验证image.png此时在下个符号断点objc_sync_exitobjc_sync_enterimage.png发现源码在libobjc.A.dylib库中,然后再去库里面找源码image.pngimage.pngimage.png
      先看SyncData结构

        typedef struct alignas(CacheLineSize) SyncData {
            struct SyncData* nextData;
            DisguisedPtr<objc_object> object;
            int32_t threadCount;  // number of THREADS using this block
            recursive_mutex_t mutex;
        } SyncData;
      

      发现是一个单链表结构

      • nextData指向下一个SyncData
      • object一个对象指针,对象是objc_object即OC对象,不难猜测,它保存了被锁定对象obj的指针
      • threadCount记录正在使用这个代码块的线程数
      • mutex递归锁、获取到该结构体对象后,就是调用它的lock()方法
        再看id2data源码 image.png具体源码注释图中都有解释。
        总结一下大致流程:
      1. 从线程缓存中查找如果能查找到说明当前线程有被使用锁所以此时只需要lockCount+1返回就好,objc_sync_exit方法对应的是减一
      2. 如果线程缓存中找不到则冲缓存中查找如果能找到同样的只需要lockCount+1返回,objc_sync_exit方法对应的是减一
      3. 如果在缓存中没有找到则说明当前线程一次锁都还没添加过,此时则去遍历缓存查看是否有其他线程使用过,如果有threadCount加1并且存到缓存中,如果也没有其他线程使用则threadCount置为1存到缓存中 缓存结构图: 未命名文件(32).jpg
    • OSSpinLock

      OSSpinLock被弃用,其替代方案是内部封装了os_unfair_lock,而os_unfair_lock在加锁时会处于休眠状态,而不是自旋锁的忙等状态

    • atomic(原子锁)

      atomic适用于OC中属性的修饰符,其自带一把自旋锁,属性在调用settergetter方法的时候会加一把锁

       static inline void reallySetProperty(id self, SEL _cmd, id newValue, ptrdiff_t offset, bool atomic, bool copy, bool mutableCopy)
        {
           ...
           id *slot = (id*) ((char*)self + offset);
           ...
      
            if (!atomic) {//未加锁
                oldValue = *slot;
                *slot = newValue;
            } else {//加锁
                spinlock_t& slotlock = PropertyLocks[slot];
                slotlock.lock();
                oldValue = *slot;
                *slot = newValue;        
                slotlock.unlock();
            }
            ...
        }
      
      id objc_getProperty(id self, SEL _cmd, ptrdiff_t offset, BOOL atomic) {
            if (offset == 0) {
                return object_getClass(self);
            }
      
            // Retain release world
            id *slot = (id*) ((char*)self + offset);
            if (!atomic) return *slot;
      
            // Atomic retain release world
            spinlock_t& slotlock = PropertyLocks[slot];
            slotlock.lock();//加锁
            id value = objc_retain(*slot);
            slotlock.unlock();//解锁
      
            // for performance, we (safely) issue the autorelease OUTSIDE of the spinlock.
            return objc_autoreleaseReturnValue(value);
        }
      

      从源码中可以看出,对于atomic修饰的属性,进行了spinlock_t加锁处理,但是在前文中提到OSSpinLock已经废弃了,这里的spinlock_t在底层是通过os_unfair_lock替代了OSSpinLock实现的加锁

    • 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);
      
    • NSLock

      首先通关断点调试查看NSLock源码的位置如下图: image.png 此时发现NSLock的源码在Foundation框架中,因为OCFoundation框架是闭源的所以看不了源码,但是swiftFoundation框架是开源的,所以我们也已查看swiftFoundation框架,因为也就是语法不一样大体实现逻辑都差不多 image.png 可以发现NSLock底层就是对pthread_mutex的封装,应为NSLock是一把互斥锁,会阻塞线程等待任务执行,所以使用NSLock需要注意不能重入NSLock锁,会造成线程相互等待的情况,造成死锁

    • NSRecursiveLock

      是互斥锁中的递归锁,可被同一线程多次获取的锁,而不会产生死锁。什么意思呢,一个线程已经获得了锁,开始执行受锁保护的代码(锁还未释放),如果这段代码调用了其他函数,而被调用的函数又要获取这个锁,此时已然可以获得锁并正常执行,而不会死锁。底层也是对pthread_mutex的封装底层实现代码也和NSLock很想lock方法和unLock方法都和NSLock是一样的无非就是init的时候NSRecursiveLock设置了该锁的类型是个递归锁iShot2021-04-20 17.03.59.png 使用示例: image.png

    • NSCondition

      NSCondition也是一把互斥锁他和NSLock的区别在于
      NSLock在获取不到锁的时候自动使线程进入休眠,锁被释放后线程又自动被唤醒
      NSCondition可以使我们更加灵活的控制线程状态,在任何需要的时候使线程进入休眠或唤醒它

      • 主要API image.png
      • 使用场景及示例 例如一个生产消费的例子,只有生产出来了商品才能被消费者售卖,消费者再买东西的时候商品没了就要等待生产者产出后在进行购买,示例代码如下:
      - (void)td_testConditon{
            _testCondition = [[NSCondition alloc] init];
            //创建生产-消费者
            for (int i = 0; i < 50; i++) {
                dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
                    [self td_producer];
                });
                dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
                    [self td_consumer];
                });
      
                dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
                    [self td_consumer];
                });
                dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
                    [self td_producer];
                });
            }
        }
      
        - (void)td_producer{
            [_testCondition lock]; // 操作的多线程影响
            self.ticketCount = self.ticketCount + 1;
            NSLog(@"生产一个 现有 count %zd",self.ticketCount);
            [_testCondition signal]; // 信号
            [_testCondition unlock];
        }
      
        - (void)td_consumer{
      
             [_testCondition lock];  // 操作的多线程影响
            if (self.ticketCount == 0) {
                NSLog(@"等待 count %zd",self.ticketCount);
                [_testCondition wait];
            }
            //注意消费行为,要在等待条件判断之后
            self.ticketCount -= 1;
            NSLog(@"消费一个 还剩 count %zd ",self.ticketCount);
             [_testCondition unlock];
        }
      
      • 源码探索 iShot2021-04-21 09.19.22.png 底层和NSLock很像都是对pthread_mutex_t的封装,无非就是使用了pthread_cond_t的条件
    • NSConditionLock

      条件锁,通俗的将就是有条件的互斥锁

      使用NSConditionLock对象,可以确保线程仅在满足特定条件时才能获取锁。 一旦获得了锁并执行了代码的关键部分,线程就可以放弃该锁并将关联条件设置为新的条件。 条件本身是任意的:您可以根据应用程序的需要定义它们。

      • 使用示例
        #pragma mark -- NSConditionLock
          - (void)td_testConditonLock{
              // 信号量
              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];
              });
          }
        
        image.png
      • 示例代码分析 image.png
      • 源码分析 image.png 从源码不难看出NSConditionLock的源码其实就是NSConditionNSLock结合封装的一把锁