锁的原理

  • 锁的种类

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

    在探索使用及源码以前先看一张图,上面展现的全部锁的性能对比aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy8yMzY3NTQ0NS1lYzhkMWY0ODJiZjFjMmMzLnBuZw.png
    • 准备工做

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

      - (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;
      复制代码

      发现是一个单链表结构markdown

      • 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实现的加锁async

    • 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结合封装的一把锁
相关文章
相关标签/搜索