iOS底层-线程锁

1,446 阅读16分钟

前言

在开发中经常会碰到一边在写数据,一边的读数据的情况,往往会出现数据混乱甚至崩溃,这就是由线程不安全导致的资源抢夺问题,这种情况就需要对线程进行加锁就可以搞定,下面将对线程锁进行讲解

锁的分类

OC中有两种锁:自旋锁互斥锁

自旋锁

  • 自旋锁是一种用于保护多线程共享资源的锁,与一般互斥锁(mutex)不同之处在于当它尝试获取锁时以忙等待(busy waiting)的形式不断地循环检查锁是否可用。当上一个线程的任务没有执行完毕的时候(被锁住),那么下一个线程会一直等待(不会睡眠),当上一个线程的任务执行完毕,下一个线程会立即执行。在多CPU的环境中,对持有锁较短的程序来说,使用自旋锁代替一般的互斥锁往往能够提高程序的性能。

  • 优点:自旋锁不会引起调用者睡眠,所以不会进行线程调度CPU时间片轮转等耗时操作。所有如果能在很短的时间内获得锁,自旋锁的效率远高于互斥锁

  • 缺点:自旋锁一直占用CPU,他在未获得锁的情况下一直运行(自旋)占用着CPU,如果不能在很短的时间内获得锁,这无疑会使CPU效率降低

总结:效率高,但是一直占用CPU耗费资源,不能实现递归调用。 常见的自旋锁:atomicOSSpinLockdispatch_semaphore_t

互斥锁

  • 当上一个线程的任务没有执行完毕的时候(被锁住),那么下一个线程会进入睡眠状态等待任务执行完毕,此时CPU可以调度其他线程。当上一个线程的任务执行完毕,下一个线程会自动唤醒然后执行任务
  • 说到互斥后想到同步,同步是只有一个任务执行完了,下个任务才可以执行。
    • 同步:互斥+顺序 常见的互斥锁:@synchronizedNSLockpthread_mutexNSConditionLock(条件锁)NSCondition(条件锁)NSRecursiveLock(递归锁)

锁的性能对比

在网上经常能看到锁的性能图: lock_benchmark.png

  • 性能高低对比:OSSpinLock(自旋锁) > dispatch_semaphone(自旋锁) > pthread_mutex(互斥锁) > NSLock(互斥锁) > NSCondition(条件锁) > pthread_mutex(recursive) 互斥递归锁 > NSRecursiveLock(递归锁) > NSConditionLock(条件锁) > @synchronized(互斥锁)

  • @synchronized是我们比较常用的,以前他的新能很低,但是后面苹果对它做了 优化,收集一些锁分别在100000次循环中执行,然后用CFAbsoluteTimeGetCurrent分别计算执行完毕的时间得到:

  • iphone11模拟器:

    截屏2021-08-20 09.45.19.png

  • iphone11真机:

    截屏2021-08-20 09.47.46.png

通过对比发现,在真机中锁的性能都有了显著的提升,就拿@synchronized来说,由于它的“出勤率”还是挺高的,苹果终究还是对它进行了优化。

锁的作用

来模拟下卖票的场景:

- (void)testGlobalQueueSell {
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        for (int i = 0; i < 5; i++) {
            [self sellingTickets];
        }
    });
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        for (int i = 0; i < 8; i++) {
            [self sellingTickets];
        }
    });
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        for (int i = 0; i < 10; i++) {
            [self sellingTickets];
        }
    });
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        for (int i = 0; i < 12; i++) {
            [self sellingTickets];
        }
    });
}

- (void)sellingTickets {
    if (self.remainTickets > 0) {
        self.remainTickets--;
        sleep(1);
        NSLog(@"卖了一张,当前余票: %d", self.remainTickets);
    } else {
        NSLog(@" 车票已经卖完 ");
    }
}

// 调用
self.remainTickets = 30;
[self testGlobalQueueSell];

得到结果如下:

截屏2021-08-20 11.08.40.png

由于有多个线程在卖票,导致同一时间卖了多张票而不知道实际票数,进而就出现了数据混乱的问题,这个时候就需要加锁来保证线程安全

- (void)sellingTickets {
    @synchronized (self) {
        if (self.remainTickets > 0) {
            self.remainTickets--;
            sleep(1);
            NSLog(@"卖了一张,当前余票: %d", self.remainTickets);
        } else {
            NSLog(@" 车票已经卖完 ");
        }
    }
}

这里选择的是互斥锁@synchronized,保证同一时间只能有一个线程在处理票的数量改变,结果如下:

截屏2021-08-20 11.19.38.png

此时剩余票数就显示正常了,问题得以解决,那么@synchronized是怎么保证线程安全的呢,接下来我们将走进源码 objc4-818.2 进行分析

@synchronized原理

直接在源码中搜索synchronized发现搜不到,说明底层并不是直接用它分析,必定有其他的结构,我们可以使用xcrun查看源码:

{ 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);}
}
  • 在源码里@synchronized先会调用objc_sync_enter函数,然后调用结构体_SYNC_EXIT,它有构造方法析构方法,构造方法会调用sync_exit函数,析构方法会调用objc_sync_exit,于是研究的重心就放在objc_sync_enterobjc_sync_exit上面

结构分析

  • objc_sync_enterobjc_sync_exit源码如下:

    int objc_sync_enter(id obj)
    {
        int result = OBJC_SYNC_SUCCESS;
        if (obj) {
            SyncData* data = id2data(obj, ACQUIRE);
            ASSERT(data);
            data->mutex.lock();
        } else {
            // @synchronized(nil) does nothing
            if (DebugNilSync) {
                _objc_inform("NIL SYNC DEBUG: @synchronized(nil); set a breakpoint on objc_sync_nil to debug");
            }
            objc_sync_nil();
        }
        return result;
    }
    
    int objc_sync_exit(id obj)
    {
        int result = OBJC_SYNC_SUCCESS;
        if (obj) {
            SyncData* data = id2data(obj, RELEASE); 
            if (!data) {
                result = OBJC_SYNC_NOT_OWNING_THREAD_ERROR;
            } else {
                bool okay = data->mutex.tryUnlock();
                if (!okay) {
                    result = OBJC_SYNC_NOT_OWNING_THREAD_ERROR;
                }
            }
        } else {
            // @synchronized(nil) does nothing
        }
        return result;
    }
    

    通过观察发现二者的处理基本一致,数据类型都是SyncDataobjc_sync_enter时会调用data->mutex.lock()进行加锁,objc_sync_exit时会调用data->mutex.tryUnlock()进行解锁,两个data的区别只是类型不同。那么研究对象就来到了SyncData上面

  • 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是关联对象
    • threadCount记录线程的数量,也就是可以多线程访问
    • mutex:是是递归锁

结论@synchronized是一把可多线程可递归的互斥锁 再来分析SyncData的获取id2data函数:

  • id2data源码分析如下:

    截屏2021-08-20 14.27.32.png

      1. 首先创建了一个spinlock_t锁,再对SyncData进行内存创建与赋值时加锁,保证线程安全
      1. 然后通过LIST_FOR_OBJ获取链表,它的结构如下:
      #define LIST_FOR_OBJ(obj) sDataLists[obj].data
      static StripedMap<SyncList> sDataLists;
      
      // StripedMap结构
      class StripedMap {
      #if TARGET_OS_IPHONE && !TARGET_OS_SIMULATOR
          enum { StripeCount = 8 };
      #else
          enum { StripeCount = 64 };
      #endif
      }
      

      可以得出sDataLists是一个全局类型的哈希表,存储的是SyncList,它在真机环境容量为8,模拟器下容量为64SyncList的结构如下:

      struct SyncList {
          SyncData *data;
          spinlock_t lock;
          constexpr SyncList() : data(nil), lock(fork_unsafe_lock) { }
      };
      

      SyncData是一个单向链表,所以可以得到StripedMap的结构:

      截屏2021-08-20 15.53.23.png

      1. 获取SyncData根据why条件进行相关处理
      1. 如果没有获取到SyncData数据,就会先进行加锁然后创建并进行相关赋值
      1. 最后执行done进行相关的存储

但具体是怎么走的流程,得需要结合案例分析

结合案例分析

由于SyncData支持多线程且可递归,可以分为以下4类进行分析,在模拟器下将StripeCount调成1,增大哈希冲突的概率

单线程递归同一个object

void oneThreadOneObject() {
    LGPerson *p = [[LGPerson alloc] init];
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        @synchronized (p) {
            NSLog(@"0");
            @synchronized (p) {
                NSLog(@"1");
                @synchronized (p) {
                    NSLog(@"2");
                }
            }
        }
    });
}
  • 分别在@synchronized处打断点,然后在 objc4-818.2 中运行,来到断点后在id2data函数的data判断处,加锁处理处,解锁后的处理处都打上断点

    1. 第一进来时StripedMap64SyncList都是空的,所以第一次获取不到datacache数据 截屏2021-08-20 17.06.37.png
      然后就会来到加锁创建处,由于*listp为空,所以不会走进循环

    截屏2021-08-20 17.10.31.png
    于是就会走到posix_memalign创建处

    截屏2021-08-20 17.17.40.png
    这里主要是对result进行:

    • 分配内存空间,
    • 然后关联对象
    • 然后多线程数设置为1
    • 创建递归锁
    • 然后头插法设置nextData,第一个的nextData为空,并且*listp指向自己

    result的相关处理后,就会将result保存到当前的线程空间

    截屏2021-08-20 17.29.34.png

    至此第一次进入完成,继续断点后走到第二个@synchronized进入

    1. 第二次进入时StripedMap就有值了,由于是同一个对象,所以listp也是有值的

    截屏2021-08-21 16.11.59.png

    • 下面继续执行,会根据SYNC_DATA_DIRECT_KEY获取上一次进入时存储的数据,由于是同一个对象,所以会走进object辨别,根据why传入的参数为ACQUIRE进行lockCount++处理和lockCount存储

    截屏2021-08-20 17.51.36.png

    断点继续会进行第三次加锁

    1. 第三次进入情况和第二次进入一样,最终lockCount++ 截屏2021-08-20 17.56.26.png

    继续断点将会走到objc_sync_exit时的处理

    1. 解锁前id2data处理:由于此时data有值,最终会走到RELEASE进行lockCount--操作 截屏2021-08-20 18.10.41.png
    • 此时会对lockCount--并进行存储,当为0时就会清空存储并调用OSAtomicDecrement32Barrier进行原子操作-1

单线程递归不同object

案例如下

void oneThreadAnyObject() {
    LGPerson *p1 = [[LGPerson alloc] init];
    LGPerson *p2 = [[LGPerson alloc] init];
    LGPerson *p3 = [[LGPerson alloc] init];
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        @synchronized (p1) {
            NSLog(@"0");
            @synchronized (p2) {
                NSLog(@"1");
                @synchronized (p3) {
                    NSLog(@"2");
                }
            }
        }
    });
}
  • 第一次@synchronized由于StripedMap为空,所以取不到数据,因此会走创建流程,然后用头插法*list指向result

    截屏2021-08-21 08.35.25.png

    • 然后进行相关的resultlockCount存储

    截屏2021-08-21 08.38.58.png

  • 第二次@synchronized就有值了,但此时object变了,不会走进里面的if判断,且此时fastCacheOccupied = YES

    截屏2021-08-21 16.19.26.png

    • 此时会走进for循环判断

    截屏2021-08-21 16.26.18.png

    • 但此时threadCount = 1,不会在此处进行赋值,所以又走到了创建处

    截屏2021-08-21 16.28.46.png

    • 这里创建后进行赋值,再使用头插法进行插入,最后进行cache存储

    截屏2021-08-21 16.36.54.png

  • 第三次@synchronized,由于是不同object,所以fastCacheOccupied = YES后就走到判断cache处,由于第二次对cache进行了存储,所以这次可以进入cache判断

    截屏2021-08-21 16.45.04.png

    • 由于object不同,所以continue跳出循环走到for循环,但threadCount此时为1

    截屏2021-08-21 16.48.36.png

    • 后面走到创建处

    截屏2021-08-21 16.50.26.png

    • 这里创建后进行赋值,再使用头插法进行插入,最后进行cache存储

多线程递归同一个object

void anyThreadOneObject() {
    LGPerson *p = [[LGPerson alloc] init];
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        @synchronized (p) {
            NSLog(@"0");
            dispatch_async(dispatch_queue_create("wushuang.concurrent1", DISPATCH_QUEUE_CONCURRENT), ^{
                @synchronized (p) {
                    NSLog(@"1");
                    dispatch_async(dispatch_queue_create("wushuang.concurrent2", DISPATCH_QUEUE_CONCURRENT), ^{
                        @synchronized (p) {
                            NSLog(@"2");
                        }
                    });
                }
            });
        }
    });
}
  • 第一次进入和前面的两种情况一样,会走进创建流程,然后用头插法*list指向result,最后进行相关存储

  • 第二次@synchronized时,会走到for循环处:

    截屏2021-08-21 15.02.47.png

    • 然后将p的值赋给result,并调用OSAtomicIncrement32Barrier进行threadCount+1
    • 最后走到done进行相关存储
  • 第三次@synchronized时,和第二次进入时一样进行threadCount+1

多线程递归不同object

void anyThreadAnyObject() {
    LGPerson *p1 = [[LGPerson alloc] init];
    LGPerson *p2 = [[LGPerson alloc] init];
    LGPerson *p3 = [[LGPerson alloc] init];

    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        @synchronized (p1) {
            NSLog(@"0");
            dispatch_async(dispatch_queue_create("wushuang.concurrent1", DISPATCH_QUEUE_CONCURRENT), ^{
                @synchronized (p2) {
                    NSLog(@"1");
                    dispatch_async(dispatch_queue_create("wushuang.concurrent2", DISPATCH_QUEUE_CONCURRENT), ^{
                        @synchronized (p3) {
                            NSLog(@"2");
                        }
                    });
                }
            });
        }
    });
}
  • 第一次进入@synchronized没有数据,就会走创建并进行相关存储

  • 第二次@synchronized时进入for循环判断,由于此时object不同,会进行赋值

    截屏2021-08-21 15.25.18.png

    • 最后进行相关存储
  • 第三次@synchronized时和第二次一样

总结

整个加锁核心处理流程如下:

截屏2021-08-21 23.52.20.png

NSLockNSRecursiveLock

NSLock

NSLock我们比较常见,经常为了解决线程安全使用,先看以下案例:

- (void)threadDemo {
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        static void (^ testBlock)(int num);
        
        testBlock = ^(int num) {
            if (num > 0) {
                NSLog(@"current num value %d ", num);
                testBlock(num - 1);
            }
        };
        
        testBlock(10);
    });
}
  • 在全局队列里面是一个block,这个很明显按照顺序打印

    截屏2021-08-22 22.15.11.png

  • 但把代码放在for循环,由于打印就出现问题了

    截屏2021-08-22 22.17.42.png

    这个时候就需要加锁来解决顺序异常的问题,可以使用NSLock

    截屏2021-08-22 22.22.14.png

  • NSLock加载block块中的业务代码结果怎样呢,结果如下:

    截屏2021-08-22 23.16.21.png

    • 由于加锁后还没解锁,又调用block,所以导致了死锁只打印一次,说明NSLock可以多线程使用,但不能递归

NSRecursiveLock

  • 再来看看NSRecursiveLock,它的用法和NSLock类似

截屏2021-08-22 23.20.09.png

  • 在队列函数的块中加锁也是能保证都执行,下面在业务代码的block块中加锁试试:

    截屏2021-08-22 23.24.55.png

    • 结果完整的打印一次顺序后就崩溃了,说明NSRecursiveLock 不支持多线程调用
  • 在文章前面我们分析了@synchronized可多线程可递归使用,再用它试试:

截屏2021-08-22 23.30.45.png

  • 打印结果显示了10次顺序执行,@synchronized完美的解决了问题

NSCondition

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

    • [condition lock]:⼀般⽤于多线程同时访问修改同⼀个数据源,保证同⼀时间内数据源只被访问修改⼀次,其他线程的命令需要在lock外等待,只到unlock才可访问
    • [condition unlock]:与lock同时使用
    • [condition wait]:让线程处于等待状态
    • [condition signal]CPU发信号告诉线程不⽤在等待,可以继续执⾏
  • 案例分析(生产消费模型,例如卖奶茶):

    - (void)testCondition {
        for (int i = 0; i < 50; i++) {
            dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
                [self ws_product];
            });
            dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
                [self ws_consumer];
            });
            dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
                [self ws_product];
            });
            dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
                [self ws_consumer];
            });
        }
    }
    
    - (void)ws_product {
        self.milkTeaCount += 1;
        NSLog(@"生产一杯,现有 %zd 杯", self.milkTeaCount);
    }
    
    - (void)ws_consumer {
        if (self.milkTeaCount == 0) {
            NSLog(@"等待制作 count: %zd", self.milkTeaCount);
        }
        self.milkTeaCount -= 1;
        NSLog(@"卖了一杯,还剩 %zd", self.milkTeaCount);
    }
    

    输出结果如下:

    截屏2021-08-23 09.13.13.png
    此时数据明显出现了问题,多次生产数量还是1,多次消费数量还是0,要怎么解决呢?此时就需要用条件锁了,对生产和消费的过程加锁,当卖完后需要等待生产后再继续卖

    截屏2021-08-23 09.21.04.png

Fountion源码解读

  • 我们前面分析的NSLockNSRecursiveLockNSCondition这些锁,底层都是用pthread封装的,那么pthread底层原理是怎样的我们不得而知,于是查找lock,看它属于哪个框架

    截屏2021-08-23 09.39.31.png

    能够发现lock,unlock是属于NSLocking协议,原来是使用了协议才能调用,很不幸锁是在Foundation框架,它是不开源的,怎么搞?此时可以去看swiftfountion框架:swift-corelibs-foundation

    进入源码,找到NSLock.swift文件,然后对几种锁进行分析

  • NSLock分析:在源码中找到NSLock后,然后找到初始化和加锁解锁的地方

    • init:调用pthread_mutex_init(mutex, nil)进行初始化
    • 加锁:调用pthread_mutex_lock(mutex) 进行加锁
    • 解锁:调用pthread_mutex_unlock(mutex)解锁
  • NSRecursiveLock分析:

    • init
      withUnsafeMutablePointer(to: &attrib) { attrs in
          pthread_mutexattr_init(attrs)
          pthread_mutexattr_settype(attrs, Int32(PTHREAD_MUTEX_RECURSIVE))
          pthread_mutex_init(mutex, attrs)
      }
      
    • 加锁:pthread_mutex_lock(mutex)
    • 解锁:pthread_mutex_unlock(mutex)

通过对比发现NSLockNSRecursiveLock加锁解锁完全一样,只有初始化时不同,NSRecursiveLock初始化时,传入一个了PTHREAD_MUTEX_RECURSIVE类型的attrs,这也是NSRecursiveLock 可递归 的的原因

NSConditionLock

  • NSConditionLock也是一种条件锁,一旦一个线程获得锁,其他线程一定等待。主要有以下几个Api

      1. [conditionLock lock]:表示conditionLock期待获得锁,如果没有其他线程获得锁(不需要判断内部的condition)那它能执⾏此⾏以下代码,如果已经有其他线程获得锁(可能是条件锁,或者⽆条件锁),则等待其他线程解锁
      1. [conditionLock unlock]:解锁
      1. [conditionLock lockWhenCondition:A]:表示如果没有其他线程获得该锁,但是该锁内部的condition 不等于 A条件,它依然不能获得锁,仍然等待。如果内部的condition 等于 A条件,并且没有其他线程获得该锁,则进⼊代码区,同时设置它获得该锁,其他任何线程都将等待它代码的完成,直⾄它解锁
      1. [conditionLock unlockWithCondition:A]:表示释放锁,同时把内部的condition设置为A条件
      1. return = [xxx lockWhenCondition:A beforeDate:t]:表示如果被锁定(没获得锁),并超过该时间则不再阻塞线程。但是注意:返回的值是NO,它没有改变锁的状态,这个函数的⽬的在于可以实现两种状态下的处理
      1. condition:所谓的condition就是整数,内部通过整数⽐较条件
  • 先来看看案例:

    - (void)testConditionLock {
        NSConditionLock *lock = [[NSConditionLock alloc] initWithCondition:2];
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
            [lock lockWhenCondition:1];
            NSLog(@"1");
            [lock unlockWithCondition:0];
        });
    
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
            sleep(1);
            [lock lockWhenCondition:2];
            NSLog(@"2");
            [lock unlockWithCondition:1];
        });
    
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            [lock lock];
            NSLog(@"3");
            [lock unlock];
        }); 
    }
    
    • 打印顺序大概率为:3 -> 2 -> 1,由于3没有加条件只是普通的加锁,创建的条件是2,只有2时才能执行条件,然后再根据[lock unlockWithCondition:1]后执行1。此时有四个疑问:
        1. NSConditionLockNSCondition很像,他们有什么区别吗?
        1. 创建时的2是什么?
        1. lockWhenCondition做了什么?
        1. unlockWithCondition做了什么?
    1. 创建代码initWithCondition:如下:
    • swift中,其对应的代码如下: 截屏2021-08-23 13.41.58.png
      在创建对象时,后先创建NSCondition类型的成员变量_cond,然后将condition赋值给另一个成员变量_calue
    1. lockWhenCondition:代码如下:
    open func lock() {
        let _ = lock(before: Date.distantFuture)
    }
    
    open func lock(whenCondition condition: Int) {
        let _ = lock(whenCondition: condition, before: Date.distantFuture)
    }
    
    open func lock(whenCondition condition: Int, before limit: Date) -> Bool {
        _cond.lock() // _cond调用 lock 加锁
        while _thread != nil || _value != condition { // 如果没有_thread 有值或者condition改变了就进入判断
            if !_cond.wait(until: limit) { // 如果等待超时则调用解锁,返回false,说明加锁失败,如果没有超时则在while循环中不停判断,直到条件改变跳出循环
                _cond.unlock()
                return false
            }
        }
    #if os(Windows)
        _thread = GetCurrentThread()
    #else
        _thread = pthread_self() // thread对自己加锁
    #endif
        _cond.unlock()  // 方法执行完unlock解锁
        return true
    }
    
    • 加锁在swift中最终调用的是lock(whenCondition: before:方法,方法里先调用NSCondition的加锁lock,然后在while判断,如果没有_thread有值或者condition改变了就进入判断,如果等待超时则调用解锁,返回false,说明加锁失败;如果没有超时则在while循环中不停判断,直到条件改变跳出循环; 加锁成功后对_thread进行赋值,也就是对自己进行加锁pthread_self,方法执行完在返回前unlock解锁
    1. 解锁unlockWithCondition代码如下:
    open func unlock(withCondition condition: Int) {
        _cond.lock()
    #if os(Windows)
        _thread = INVALID_HANDLE_VALUE
    #else
        _thread = nil // 置空
    #endif
        _value = condition // 改变_value值
        _cond.broadcast()
        _cond.unlock()
    }
    
    • 解锁时先调用lock加锁,然后将_thread置为nil,再将value的值改变成新传入的条件condition,然后方法之前完时调用unlock,这样就解锁了,但过程中_cond调用了broadcast方法,这个是做什么的?
    • broadcast
      open func broadcast() {
      #if os(Windows)
          WakeAllConditionVariable(cond)
      #else
          pthread_cond_broadcast(cond) 
      #endif
      }
      
      方法里最终调用pthread_cond_broadcast,它的作用是唤醒全部阻塞在条件变量上的线程。这也印证了互斥锁在线程阻塞时休眠,解锁后唤醒执行任务的特点

读写锁(多读单写)

  • 读写锁实际是⼀种特殊的互斥锁,它把对共享资源的访问者划分成读者写者读者只对共享资源进⾏访问,写者则需要对共享资源进⾏操作。这种锁相对于⾃旋锁⽽⾔能提⾼并发性,因为在多处理器系统中,它允许同时多个读者来访问共享资源,最⼤可能的读者数为实际的逻辑CPU数。写者排他性的,⼀个读写锁同时只能有⼀个写者多个读者(与CPU数相关),但不能同时既有读者⼜有写者。在读写锁保持期间也是抢占失效的。如果读写锁当前没有读者也没有写者,那么写者可以⽴刻获得读写锁,否则它必须⾃旋在那⾥,直到没有任何写者或读者。如果读写锁没有写者,那么读者可以⽴即获得该读写锁,否则读者必须⾃旋在那⾥,直到写者释放该读写锁。
  • 读写锁适合于对数据结构的读次数⽐写次数多得多的情况. 因为读模式锁定时可以共享, 以写模式锁住时意味着独占, 所以读写锁⼜叫共享-独占锁

读写锁可以用以下pthread_rwlock_t和栅栏函数dispatch_barrier_async来实现:

    1. pthread_rwlock_tApi:
    • pthread_rwlock_init:初始化锁
    • pthread_rwlock_rdlock:读加锁
    • pthread_rwlock_tryrdlock:读尝试加锁
    • pthread_rwlock_wrlock:写加锁
    • pthread_rwlock_trywrlock:写尝试加锁
    • pthread_rwlock_unlock:解锁
    • pthread_rwlock_destroy:销毁锁

使用方法:

#import <pthread.h>

@property (nonatomic, assign) pthread_rwlock_t rwlock;
@property (nonatomic, strong) NSMutableDictionary *dic;

- (void)testRWLock {
    pthread_rwlock_init(&_rwlock, NULL);
    self.dic = [NSMutableDictionary dictionary];

    for (int i = 0; i < 10; i++) {
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            if (i % 2 == 0) {
                [self ws_writeName:[NSString stringWithFormat:@"name_%d", i]];
            } else {
                [self ws_readName];
            }
        });
    }
}

// 读
- (void)ws_readName {
    pthread_rwlock_rdlock(&_rwlock); //读加锁
    NSString *name = [self.dic valueForKey:@"name"];
    NSLog(@"read name ___ :%@ 🎈", name);

    pthread_rwlock_unlock(&_rwlock); //读解锁
}

// 写
- (void)ws_writeName: (NSString *)name {
    pthread_rwlock_wrlock(&_rwlock); // 写加锁
    [self.dic setValue:name forKey:@"name"];
    NSLog(@"write name ___ :%@ 🎉", name);

    pthread_rwlock_unlock(&_rwlock); // 写解锁
}

输出如下:

截屏2021-08-23 15.07.19.png

再来看看栅栏函数的方案

  • 2. dispatch_barrier_async使用如下
@property (nonatomic, strong) NSMutableDictionary *dic;
@property (nonatomic, strong) dispatch_queue_t myQueue;

- (void)testRWLock {
    self.dic = [NSMutableDictionary dictionary];
    self.myQueue = dispatch_queue_create("com.current.thread", DISPATCH_QUEUE_CONCURRENT);

    for (int i = 0; i < 10; i++) {
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            if (i % 2 == 0) {
                [self ws_barrier_writeName:[NSString stringWithFormat:@"name_%d", i]];
            } else {
                [self ws_barrier_readName];
            }
        });
    }
}

// 读
- (void)ws_barrier_readName {
    dispatch_sync(self.myQueue, ^{
        NSString *name = [self.dic valueForKey:@"name"];
        NSLog(@"read name ___ :%@ 🎈", name);
    });
}

// 写
- (void)ws_barrier_writeName: (NSString *)name {
    dispatch_barrier_async(self.myQueue, ^{
        [self.dic setValue:name forKey:@"name"];
        NSLog(@"write name ___ :%@ 🎉", name);
    });
}

输出结果如下:

截屏2021-08-23 15.15.30.png