iOS底层学习——锁

4,196 阅读17分钟

1.锁的归类

OC中锁分为互斥锁自旋锁两种。

1.自旋锁

是一种用于保护多线程共享资源的锁,与一般互斥锁(mutex)不同之处在于当自旋锁尝试获取锁时以忙等待busy waiting)的形式不断地循环检查锁是否可用。当上一个线程的任务没有执行完毕的时候(被锁住),那么下一个线程会一直等待(不会睡眠),当上一个线程的任务执行完毕,下一个线程会立即执行。

在多CPU的环境中,对持有锁较短的程序来说,使用自旋锁代替一般的互斥锁往往能够提高程序的性能。

自旋锁:OSSpinLock(自旋锁)、读写锁

2.互斥锁

当上一个线程的任务没有执行完毕的时候(被锁住),那么下一个线程会进入睡眠状态等待任务执行完毕,当上一个线程的任务执行完毕,下一个线程会自动唤醒然后执行任务,该任务也不会立刻执行,而是成为可执行状态(就绪)。互斥锁(mutex),⽤于保证在任何时刻,都只能有⼀个线程访问该对象。

  • mutex函数

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

    NSLockNSCondtionNSRecursiveLock底层都是对pthread的封装。

  • 互斥和同步的理解

    • 互斥:两条线程处理,同一时间只有一个线程可以运行;
    • 同步:除了有互斥的意思外,同时还有一定的顺序要求,即按照一定的顺序执行。
  • 递归锁

    就是同⼀个线程可以加锁N次⽽不会引发死锁NSRecursiveLock@synchronizedpthread_mutex(recursive)

互斥锁:pthread_mutex(互斥锁)、@synchronized(互斥锁)、NSLock(互斥锁)、NSConditionLock(条件锁)、NSCondition(条件锁)、NSRecursiveLock(递归锁)、dispatch_semaphore_t(信号量)

3.自旋锁和互斥锁的特点

  • 自旋锁会忙等,所谓忙等,即在访问被锁资源时,调用者线程不会休眠,而是不停循环在那里,直到被锁资源释放锁。

  • 互斥锁会休眠,所谓休眠,即在访问被锁资源时,调用者线程会休眠,此时cpu可以调度其他线程工作,直到被锁资源释放锁。此时会唤醒休眠线程。

  • 自旋锁优缺点

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

    缺点在于,自旋锁一直占用CPU,他在未获得锁的情况下,一直运行自旋,所以占用着CPU,如果不能在很短的时间内获得锁,这无疑会使CPU效率降低。自旋锁不能实现递归调用。

4.锁的性能

image.png

图中锁的性能从高到底依次是:OSSpinLock(自旋锁) -> dispatch_semaphone(信号量) -> pthread_mutex(互斥锁) -> NSLock(互斥锁) -> NSCondition(条件锁) -> pthread_mutex(recursive 互斥递归锁) -> NSRecursiveLock(递归锁) -> NSConditionLock(条件锁) -> synchronized(互斥锁)

2.锁的作用

我们通过一个案例进行分析。模拟一个售票流程,总票数为20张,有4个窗口在同时进行售票,实时跟踪剩余票数。见下面代码:

@interface ViewController ()
@property (nonatomic, assign) NSUInteger ticketCount;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.ticketCount = 20;
    [self testSaleTicket];
}

- (void)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(@"当前余票还剩:%lu张",(unsigned long)self.ticketCount);
    } else {
        NSLog(@"当前车票已售罄");
    }
}
@end

运行结果如下图:

image.png

通过运行结果,发现因为异步操作的原因,出现了数据不安全问题,数据出现了混乱。通常我们会通过加锁的方式来保证数据的安全,用来保证在任一时刻,只能有一个线程访问该对象。

对象上面的案例进行修改,见下图:

image.png

添加一个@synchronized互斥锁,重新运行程序,发现其能够正常运行,并能够保证数据的安全性。@synchronized用着更方便,可读性更高,也是我们最常用的。

3.@synchronized实现原理

通过上面的案例我们了解到了锁的作用,那么@synchronized到底做了什么工作呢?这是我们所需要研究分析的。

1.底层探索

  • clang分析实现原理

    提供下面一段代码,通过clang来查看其底层实现原理,加下图:

    image.png

    clang之后生成.cpp文件,打卡.cpp文件,定位到main函数对应的位置。见下图:

    image.png

    可以看到,调用了objc_sync_enter方法,并且使用了try-catch,在正常处理流程中,提供了_SYNC_EXIT结构体,最后也会调用对应的析构函数objc_sync_exit

  • 查看汇编流程

    首先我们可以通过汇编来分析,底层到底做了哪些操作。通过设置断点,并打开汇编调试,获取以下信息:

    image.png image.png

    通过汇编我们可以发现底层调用了两个方法分别是objc_sync_enterobjc_sync_exit,通过字面可以理解,分别是进入和退出。这与clang中看到的结果是一样的。

2.实现原理

libObjc.dylib源码中分析其实现原理。搜索objc_sync_enterobjc_sync_exit两个方法的源码实现:

// enter
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;
}

// exit
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;
}

解读源码发现,enter方法exit方法的实现流程是一一对应的。

首先加锁和解锁都会对obj进行判断,如果obj为空,则锁了个寂寞,什么也没有做,在libObjc.dylib源码中,没有查到objc_sync_nil()的相关实现。

如果obj不为空,在enter方法中,会封装一个SyncData对象,并对调用mutex属性进行上锁lock();exit方法时,同样获取对应的SyncData对象,然后调用data->mutex.tryUnlock();进行解锁。

  • 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;
    
    
    • struct SyncData* nextData;包含了一个相同的数据结构,说明它是一个单项链表结构
    • object使用DisguisedPtr进行了包装
    • threadCount线程的数量,有多少个线程对该对象进行加锁
    • recursive_mutex_t mutex;递归锁

    SyncData的属性可以判断,@synchronized支持递归锁,并且支持多线程访问。

  • StripedMap数据结构

    首先要分析底层的数据存储结构。SyncData存储在一个hash表中,并且是静态的。见下面代码:

    static StripedMap<SyncList> sDataLists;
    
    class StripedMap {
    
        #if TARGET_OS_IPHONE && !TARGET_OS_SIMULATOR
            enum { StripeCount = 8 };
    
        #else
            enum { StripeCount = 64 };
    
        #endif
    }
    

    给表为不同的架构环境提供了不同的容量,真机环境的容量为8,模拟环境的容量为64。而其元素为SyncListSyncList的数据结构为:

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

    SyncData是一个链表结构,至此形成了一个拉链结构。见下图:

    image.png

  • id2data方法

    id2data实现源码见下图:

    image.png

    这里包含3个大步骤,首先通过tls,从线程缓存中获取当前线程的SyncData进行相关处理;如果缓存中存在对应的SyncData则从缓存中获取并处理;最后包括一些内部的初始化插入缓存等操作。

流程分支比较多,具体会调用哪些流程呢?下面通过案例结合lldb调试进行分析。

3.案例跟踪

单线程递归加锁object不变

引入下面的案例,在一个子线程中递归添加同一个锁。见下图:

image.png

  • 断点1:案例的104

    运行程序,在案例的104行设置断点,跟踪进入id2data方法。此时StripedMap表中64个数据全是空。见下图:

    image.png

    继续跟踪调试,会调用tls_get_direct方法,获取当前线程绑定的SyscData,因为是第一次进行加锁,所以这里的data是空。见下图:

    image.png

    紧接着会从当前线程的缓存列表中获取对应的SyncData,很显然此时缓存中也没有存储该对象,所以此时也是空。见下图:

    image.png

    当前线程绑定的SyncData和线程对应的缓存列表中的SyncData都为空,则会从哈希表中获取,当前的表中也没有对应的数据,见下图:

    image.png

    上面三个地方都没有找到对应的SyncData,最终会创建一个SyncData,并采用头插法将数据插入到对应listp头部。见下图:

    image.png

    完成SyncData创建后,会绑定到当前线程上(一个线程只会绑定一个,并且绑定后不再改变),注意此时并没有保存到线程对应的缓存列表中。见下图:

    image.png

    最后返回result,完成加锁功能。

  • 断点2:案例的107

    从此断点开始,进行该对象的第二次加锁。进入id2data方法,此时哈希表中已经有一个数据,也就是此时对象对应的listp此时也不再为空(同一个对象)。见下图:

    image.png

    继续运行程序,再次获取当前线程绑定的SyncData,此时不再为空,并且object相同。见下图:

    image.png

    线程绑定的SyncData对应的object,与此时的object相同,再次创建锁,并且锁次数++,见下图:

    image.png

  • 断点3:案例的110

    进行第三次加锁时,因为此时object没有发生改变,线程也没有改变,此时哈希表依然是一个元素,同时对应的listp也只有一个元素,此时上锁此时会变为3。见下图:

    image.png

单线程递归加锁object变化

引入下面这个案例,我们直接从第二个断点开始分析,见下图:

image.png

第一个断点的处理流程我们已经分析了,此时会创建一个新的SyncData,并且会绑定到当前线程中。

  • 断点2:案例的108

    objectperson2,此时线程已经绑定了person1对应的SyncData,所以线程绑定关系已经被占用,但是object不相同。见下图:

    image.png

    因为person2对象是第一次加锁,所以线程对应缓存列表和listp中都没有对应的SyncData。见下图:

    image.png

    person2初次进入,会进行对象的创建,并将SyncData放入缓存列表中。见下图:

    image.png

    如果下次person2再次加锁时,会从缓存列表中获取。而如果person1再次加锁,会从当前线程中获取,因为当前线程已经绑定了person1对应的SyncData

多线程递归加锁object变化

引入下面的案例,见下图:

image.png

上面案例中,前两个加锁过程这里不再分析,和上面单线程是一样的,我们从多线程时开始分析,也就是第113行开始。

  • 断点1:案例的113

    断点1处进行跟踪,进入id2data方法,此时哈希表中的数据个数为2,也就是外层线程添加的两个SyncData。见下图:

    image.png

    继续跟踪代码,从线程中获取其绑定的SyncData,此时为NULL,因为是新的线程,还没有加过锁,所以绑定数据为空,fastCacheOccupied=NO。见下图:

    image.png

    接着,从缓存列表中获取对应的SyncData,也是NULL,所以这里的缓存列表也是和线程一一对应的。见下图:

    image.png

    紧接着,会从listp中获取对应的数据,在外层线程中,已经添加了person1person2对应的SyncData,所以这里是可以获取的。并且会针多线程操作,从而是threadCount1,此时对应的线程数会变成2**见下图:

    image.png

    获取数据后,因为前面fastCacheOccupied=NO,则会将该SyncData绑定到当前这个线程,也就是每个线程都会默认绑定第一个object,见下图:

    image.png

  • 断点2:案例的116

    进行person2的加锁操作,此时首先会获取当前线程绑定的SyncData,因为此时已经绑定了person1tls对应的Object不相同。

    然后会从线程对应的缓存列表中获取,因为当前线程没有添加过,所以这里查询不到,最终会在listp中获取对应的SyncData。与此同时会进行threadCount1操作。完成以上操作后,会将该SyncData添加到线程对应的缓存列表中。见下图:

    image.png

在新线程中的流程与外层线程的逻辑是一样的,只是线程绑定的数据和缓存列表数据不一样。

4.@synchronized原理总结

通过上面的分析,objc_sync_enter可以得出以下流程图,在获取SyncData之后,会调用属性mutex.lock();进行加锁。见下图:

image.png

注意事项:上图中多线程的情况需要注意,只要遇到新开线程,开始加锁,tlscache一定是空,肯定是listp中查找,或者是create。一个线程中第一个添加的Object一定会绑定到tls中,并且在当前线程中不会改变。如果tls已经完成设置,之后添加的SyncData都会添加到缓存列表中。

objc_sync_exit流程和这个相反,同样会调用id2data方法,获取SyncData,对lockCountthreadCount进行减操作。如果count等于0,则会从相应的绑定关系和缓存列表中移除。

综上:@synchronized是一个支持多线程的递归锁。

4.NSLock和NSRecursiveLock的使用

引入一个案例,见下面代码:

- (void)lg_testRecursive{
    for (int i= 0; i<10; 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);
        });
    }
}

案例中开启十个线程,进行101的输出打印,运行上面代码,运行结果见下图:

image.png

其中testMethod为静态block,因为多线程的影响,所以运行结果错乱无序的,这点很好理解。如何解决混乱的问题呢?加锁!但是锁加在哪里呢?很显然testMethod(10);方法调用的地方是核心步骤。见下图:

image.png image.png

验证结果和我们的分析是一致的,我们在分析@synchronized时知道,数据结构SyncData中分装了recursive_mutex_t递归互斥锁,@synchronized是一个支持多线程递归的锁。

1.NSLock

使用NSLock进行加锁,将锁加在业务代码外层,修改后代码如下:

image.png

NSLock启到了作用,那么通常我们在开发过程中会将锁业务代码中,即将锁放在testMethod方法内部,见下图:

image.png

此时程序没有正常输出,为什么呢?因为在调用textMethod方法之后,lock加锁,内部又继续调用testMethod,导致重复加锁。

这里说明NSLock是不支持递归加锁的!

2.NSRecursiveLock

使用NSRecursiveLock进行加锁,将锁加在业务代码外层,修改后代码如下:

image.png

正常运行,并得到了预期的效果,如果将NSRecursiveLock加在业务代码内部呢?

image.png

通过上面的运行结果发现,NSRecursiveLock在完成一次业务操作后就崩溃了。

说明NSRecursiveLock支持单线程内的递归加锁,但是并不支持多线程递归。

5.NSCondition的使用

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

  • NSCondition提供的API

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

    模拟生产和消费的需求,开启多个线程进行产品生产,同时开启多个线程进行销售产品。见下面案例:

    #pragma mark **-- NSCondition**
    - (void)lg_testConditon{
        NSCondition *testCondition = [[NSCondition alloc] init];
    
        // 创建-生产者
        for (int i = 0; i < 50; i++) {
           dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
                [self lg_producer];
            });
           dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
                [self lg_producer];
            });
        }
        
        // 创建-消费者
        for (int i = 0; i < 50; i++) {
            dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
                [self lg_consumer];
            });
    
            dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
                [self lg_consumer];
            });
        }
    }
    
    - (void)lg_producer{
        [_testCondition lock]; // 操作的多线程影响
    
        self.ticketCount = self.ticketCount + 1;
        NSLog(@"生产一个 现有 count %zd",self.ticketCount);
    
        [_testCondition signal]; // 信号
    
        [_testCondition unlock];
    }
    
    - (void) lg_consumer {
        [_testCondition lock];  // 操作的多线程影响
    
        if (self.ticketCount == 0) {
            NSLog(@"等待 count %zd",self.ticketCount);
            [_testCondition wait];
        }
    
        // 注意消费行为,要在等待条件判断之后
        self.ticketCount -= 1;
        NSLog(@"消费一个 还剩 count %zd ",self.ticketCount);
    
        [_testCondition unlock];
    }
    

    这里需要注意的是,生产线、消费线需要进行加锁处理,以保证多线程安全,于此同时,生产和消费之前也存在关系,比如库存数的安全!通过[_testCondition wait];模拟库存不足,让消费窗口停止消费;用[_testCondition signal];模拟已有库存可以消费,向等待的线程发送信号,开始执行。

    运行效果见下图:

    image.png

6.Foundation源码了解锁的封装

前面提到NSLockNSCondtionNSRecursiveLock底层都是对pthread的封装。下面来探索一下其底层的实现。由于OC环境中锁相关的内容都是封装在Fundation框架下的,Fundation框架并不开源,只能查看一些声明:

image.png image.png

在实现,定义了一个NSLocking协议,并且提供了lockunlock方法。所以我们使用的⽐如条件锁,递归锁都会有对应的lock方法和unlock方法。

SwiftFundation框架是开源的,我们可以通过Swift环境的Fundation框架探索锁的实现原理。

  • NSLock

    源码中全局搜索NSLock:,获取NSLock锁定义的地方,见下图:

    image.png

    在构造函数init中,底层调用了pthread_mutex_init函数。见下图:

    image.png

    lock方法中,调用了pthread_mutex_lock函数;unlock方法中,调用了pthread_mutex_lock函数。底层就是对pthread的封装。

  • NSRecursiveLock

    采用相同的方式,搜索递归锁NSRecursiveLock。见下图:

    image.png image.png 发现其也是对pthread的封装,并且通过在init方法中,通过pthread_mutexattr_settype(attrs, Int32(PTHREAD_MUTEX_RECURSIVE))进行递归设置。

  • NSCondition

    NSCondition底层也是对pthread进行了封装,见下图:

    image.pngimage.png

    除了进行pthread_mutex互斥处理外,还对pthread_cond进行了处理,同时提供了waitsignalbroadcase方法。

  • NSConditionLock

    NSConditionLock底层没有直接操作pthread_mutex,见下图:

    image.png

    但是实现中提供了一个NSCondition属性和一个pthread_t属性,通过这两个属性,实现加锁和线程方面的相关处理。

7.NSConditionLock分析

NSConditionLock也是一种条件锁,⼀旦⼀个线程获得锁,其他线程⼀定等待。

  • 相关API

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

    引入一个案例来了解NSConditionLock。见下面代码:

    - (void)lg_testConditonLock{
    
        // 创建条件锁 - 需要满足条件2,否则不执行
        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];
        });
    }
    

    上面的案例执行顺序是怎样的呢?运行结果见下图:

    image.png

    关键是lockWhenCondition方法所起到的作用是什么!下面进行分析:

    • NSConditionLock创建时,设置的条件时2,也就是说需要满足条件2,否则不执行;
    • 线程 1调⽤[NSConditionLock lockWhenCondition:1],此时因为不满⾜当前条件,所以会进⼊waiting状态,当前进⼊到waiting时,会释放当前的互斥锁;
    • 此时当前的线程 3调⽤[NSConditionLock lock:],本质上是调⽤[NSConditionLock lockBeforeDate:],这⾥不需要⽐对条件值,所以线程 3会打印;
    • 接下来线程 2执⾏[NSConditionLock lockWhenCondition:2],因为满⾜条件值,所以线程 2会打印,打印完成后会调⽤[NSConditionLock unlockWithCondition:1],这个时候将value设置为1,并发送boradcast, 此时线程 1接收到当前的信号,唤醒执⾏并打印;
    • ⾃此当前打印为 线程 3->线程 2->线程 1
    • [NSConditionLock lockWhenCondition:]:这⾥会根据传⼊的condition值和Value值进⾏对⽐,如果不相等,这⾥就会阻塞,进⼊线程池,否则的话就继续代码执⾏;
    • [NSConditionLock unlockWithCondition:]: 这⾥会先更改当前的value值,然后进⾏⼴播,唤醒当前的线程。

8.读写锁

  • 概念理解

    读写锁实际是⼀种特殊的⾃旋锁,它把对共享资源的访问者划分成读者和写者,读者只对共享资源进⾏读访问,写者则需要对共享资源进⾏写操作。

    这种锁相对于⾃旋锁⽽⾔,能提⾼并发性,因为在多处理器系统中,它允许同时有多个读者来访问共享资源,最⼤可能的读者数为实际的逻辑CPU数。写者是排他性的,⼀个读写锁同时只能有⼀个写者或多个读者(与CPU数相关),但不能同时既有读者⼜有写者。在读写锁保持期间也是抢占失效的。

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

    ⼀次只有⼀个线程可以占有写模式的读写锁, 但是可以有多个线程同时占有读模式的读写锁. 正是因为这个特性,当读写锁是写加锁状态时, 在这个锁被解锁之前, 所有试图对这个锁加锁的线程都会被阻塞.当读写锁在读加锁状态时, 所有试图以读模式对它进⾏加锁的线程都可以得到访问权, 但是如果线程希望以写模式对此锁进⾏加锁, 它必须直到所有的线程释放锁.

    通常, 当读写锁处于读模式锁住状态时, 如果有另外线程试图以写模式加锁, 读写锁通常会阻塞随后的读模式锁请求, 这样可以避免读模式锁⻓期占⽤, ⽽等待的写模式锁请求⻓期阻塞.读写锁适合于对数据结构的读次数⽐写次数多得多的情况. 因为, 读模式锁定时可以共享, 以写模式锁住时意味着独占, 所以读写锁⼜叫共享-独占锁.

  • API

    • pthread_rwlock_t lock; // 结构
    • pthread_rwlock_init(&lock, null); // 初始化
    • pthread_rwlock_rdlock(&lock); // 读加锁
    • pthread_rwlock_tryrdlock(&lock); // 读尝试加锁
    • pthread_rwlock_wdlock(&lock); // 写加锁
    • pthread_rwlock_trywdlock(&lock); // 写尝试加锁
    • pthread_rwlock_unlock(&lock); // 解锁
    • pthread_rwlock_destory(&lock); // 销毁
  • pthread_rwlock_t的使用

    引入下面的案例,开启十个线程,同时进行读写操作, 要求:

    • 可以实现多读,多读不互斥
    • 单写,读写互斥
    • 写写互斥

    见下面实现:

    #import <Pthread.h>
    
    @interface ViewController ()
    
    @property (nonatomic, assign) NSUInteger ticketCount;
    @property (nonatomic,assign) pthread_rwlock_t lock;
    
    @end
    
    @implementation ViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        
        self.ticketCount = 0;
        [self rwTest];
    }
    
    - (void)rwTest {
        // 初始化
        pthread_rwlock_init(&_lock, NULL);
        // 全局队列
        dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
        // 开启读写
        for (int i = 0; i<10; i++) {
    
            dispatch_async(queue, ^{
                [self read];
            });
    
            dispatch_async(queue, ^{
                [self read];
            });
            
            // 写
            dispatch_async(queue, ^{
                [self write];
            });
        }
    }
    
    // 读流程
    -(void)read{
        // 读加锁
        pthread_rwlock_rdlock(&_lock);
    
        sleep(1);
        NSLog(@"读……%zd", self.ticketCount);
        
        // 解锁
        pthread_rwlock_unlock(&_lock);
    }
    
    // 写
    -(void)write{
        // 写加锁
        pthread_rwlock_wrlock(&_lock);
    
        sleep(1);
        NSLog(@"写……%zd", ++self.ticketCount);
    
        // 解锁
        pthread_rwlock_unlock(&_lock);
    }
    
    @end
    

    运行结果见下图:

    image.png

    通过上面的案例可以反映出读写锁同时只能有⼀个写者,并且可以保证多读同时进行。

  • GCD栅栏函数的使用

    通过栅栏函数也可以满足以上的需求。

    #import <pthread.h>
    @interface ViewController ()
    @property (nonatomic, assign) NSUInteger ticketCount;
    // 并发队列-多读
    @property (nonatomic, strong) dispatch_queue_t qCONCURRENT;
    // 串行队列-限制读取顺序
    @property (nonatomic, strong) dispatch_queue_t qSERIAL;
    @end
    
    @implementation ViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
    
        self.ticketCount = 0;
    
        // 队列初始化
        self.qCONCURRENT = dispatch_queue_create("selfCONCURRENT", DISPATCH_QUEUE_CONCURRENT);
    
        self.qSERIAL = dispatch_queue_create("selfSERIAL", DISPATCH_QUEUE_SERIAL);
    
        [self go_testReadWrite];
    }
    
    - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
        // 写  栅栏函数,保证读写的互斥
        dispatch_barrier_async(self.qCONCURRENT, ^{
            [self writeAction];
        });
    }
    
    #pragma read wirte
    - (void)go_testReadWrite{
        // 多线程读
        for (int i = 0; i < 2000; i++) {
            dispatch_async(self.qCONCURRENT, ^{
                [self readAction];
            });
    
            dispatch_async(self.qCONCURRENT, ^{
                [self readAction];
            });
    
            dispatch_async(self.qCONCURRENT, ^{
                [self readAction];
            });
        }
    }
    
    - (void)readAction {
    
        // 保证读取顺序
        dispatch_async(self.qSERIAL, ^{
            sleep(1);
            NSLog(@"读 ..... %ld ------ %@", self.ticketCount, [NSThread currentThread]);
        });
    }
    
    - (void)writeAction {
        sleep(1);
        NSLog(@"写 ..... %ld ------ %@", ++self.ticketCount, [NSThread currentThread]);
    }
    
    

    运行结果见下图:

    image.png