底层探索-锁

150 阅读16分钟

讲到锁这个话题,开头先推荐大佬的博客 不再安全的 OSSpinLock

一:基础介绍

什么是互斥锁

互斥锁:顾名思义,互相排斥。线程A获取到锁,在释放锁之前,其他线程都获取不到锁。互斥锁也分为两种:

  • 递归锁:可重入锁,同一个线程在锁释放前可再次获取锁,即可以递归调用
  • 非递归锁:不可重入,必须等锁释放后才能再次获取锁

什么是自旋锁

自旋锁:线程A获取到锁,在释放锁之前,线程B又来获取锁,此时获取不到,线程B就会不断的进入循环,一直检查锁是否已被释放,处于忙等状态。如果释放,则能获取到锁

自旋锁和互斥锁的特点

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

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

  • 自旋锁优缺点

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

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

锁的性能比较

性能方面,常见的锁性能如下:

image.png

锁的作用

在编程中,特别是多线程开发者中,来保证共享数据操作的完整性。假如有 ABC三条甚至更多的线程,同时去访问资源,那么读的话是没有问题,要是写的话,就可能出问题,同时修改了某一个数据,这样就破坏的数据的完整性了。

加锁的话,就是同一个时间,只能有一个个线程访问,其他的靠边等待,可以给每个对象都对应于一个可称为" 互斥锁" 的标记,这个标记用来保证在任一时刻,只能有一个线程访问该对象。

卖票示例,A,B两个窗口卖票
不加锁的情况

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

- (void)saleTicket{
    // A窗口
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        for (int i = 0; i < 10; i++) {
            [self detailSaleTicket];
        }
    });

    // B窗口
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        for (int i = 0; i < 10; i++) {
            [self detailSaleTicket];
        }
    });
}

- (void)detailSaleTicket{
//    @synchronized (self) {

//    }

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

打印结果:

**2021-09-03 09:47:10.241853+0800 001-@synchronized分析[56982:8372629] 当前余票还剩:18张**

**2021-09-03 09:47:10.241856+0800 001-@synchronized分析[56982:8372618] 当前余票还剩:18张**

**2021-09-03 09:47:10.241911+0800 001-@synchronized分析[56982:8372629] 当前余票还剩:17张**

**2021-09-03 09:47:10.241994+0800 001-@synchronized分析[56982:8372618] 当前余票还剩:16张**

**2021-09-03 09:47:10.242322+0800 001-@synchronized分析[56982:8372629] 当前余票还剩:15张**

**2021-09-03 09:47:10.242388+0800 001-@synchronized分析[56982:8372629] 当前余票还剩:13张**

**2021-09-03 09:47:10.242325+0800 001-@synchronized分析[56982:8372618] 当前余票还剩:14张**

**2021-09-03 09:47:10.242491+0800 001-@synchronized分析[56982:8372629] 当前余票还剩:12张**

**2021-09-03 09:47:10.242550+0800 001-@synchronized分析[56982:8372618] 当前余票还剩:11张**

**2021-09-03 09:47:10.242583+0800 001-@synchronized分析[56982:8372629] 当前余票还剩:10张**

**2021-09-03 09:47:10.242663+0800 001-@synchronized分析[56982:8372618] 当前余票还剩:9张**

......

从打印结果来看,肯定是有问题的,出现了票数混乱

加锁的情况

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

- (void)saleTicket{
    // A窗口
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        for (int i = 0; i < 10; i++) {
            [self detailSaleTicket];
        }
    });

    // B窗口
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        for (int i = 0; i < 10; i++) {
            [self detailSaleTicket];
        }
    });
}

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

打印结果:

**2021-09-03 10:09:24.989503+0800 001-@synchronized分析[57069:8390521] 当前余票还剩:19张**

**2021-09-03 10:09:24.989558+0800 001-@synchronized分析[57069:8390521] 当前余票还剩:18张**

**2021-09-03 10:09:24.989624+0800 001-@synchronized分析[57069:8390521] 当前余票还剩:17张**

**2021-09-03 10:09:24.989658+0800 001-@synchronized分析[57069:8390521] 当前余票还剩:16张**

**2021-09-03 10:09:24.989708+0800 001-@synchronized分析[57069:8390521] 当前余票还剩:15张**

**2021-09-03 10:09:24.989908+0800 001-@synchronized分析[57069:8390525] 当前余票还剩:14张**

**2021-09-03 10:09:24.989961+0800 001-@synchronized分析[57069:8390525] 当前余票还剩:13张**

**2021-09-03 10:09:24.989996+0800 001-@synchronized分析[57069:8390525] 当前余票还剩:12张**

**2021-09-03 10:09:24.990029+0800 001-@synchronized分析[57069:8390525] 当前余票还剩:11张**

**2021-09-03 10:09:24.990061+0800 001-@synchronized分析[57069:8390525] 当前余票还剩:10张**

**2021-09-03 10:09:24.990127+0800 001-@synchronized分析[57069:8390525] 当前余票还剩:9张**

**2021-09-03 10:09:24.990222+0800 001-@synchronized分析[57069:8390525] 当前余票还剩:8张**

**2021-09-03 10:09:24.990289+0800 001-@synchronized分析[57069:8390525] 当前余票还剩:7张**

**2021-09-03 10:09:24.990339+0800 001-@synchronized分析[57069:8390525] 当前余票还剩:6张**

**2021-09-03 10:09:24.990416+0800 001-@synchronized分析[57069:8390525] 当前余票还剩:5张**

**2021-09-03 10:09:24.990503+0800 001-@synchronized分析[57069:8390521] 当前余票还剩:4张**

**2021-09-03 10:09:24.993305+0800 001-@synchronized分析[57069:8390521] 当前余票还剩:3张**

**2021-09-03 10:09:24.993433+0800 001-@synchronized分析[57069:8390521] 当前余票还剩:2张**

**2021-09-03 10:09:24.993556+0800 001-@synchronized分析[57069:8390521] 当前余票还剩:1张**

**2021-09-03 10:09:24.993617+0800 001-@synchronized分析[57069:8390521] 当前余票还剩:0张**

从结果来看,加锁之后票按次减少,线程安全。

二: 自旋锁

1. OSSpinLock

自从OSSpinLock出现了安全问题之后就废弃了。自旋锁之所以不安全,是因为自旋锁由于获取锁时,线程会一直处于忙等待状态,造成了任务的优先级反转

OSSpinLock忙等的机制就可能造成高优先级一直running等待,占用CPU时间片;而低优先级任务无法抢占时间片,变成迟迟完不成,不释放锁的情况。

2. atomic

  • atomic: 是原子属性,是为多线程开发准备的,是默认属性!仅仅在属性的 setter 方法中,增加了锁(自旋锁),能够保证同一时间,只有一条线程对属性进行操作,同一时间 单(线程)写多(线程)读的线程处理技术。

  • nonatomic: 是非原子属性,没有加锁操作!性能高!

源码分析

setter方法,最后都会统一调用reallySetProperty方法

image.png

看下对是否存在atomic的判断:

  • 原子性修饰的属性进行了spinlock加锁处理,此处spinlock内部实际用os_unfair_lock替代了OSSpinLock
  • 非原子性的属性除了没加锁,其他逻辑与atomic一般无二

gettersetter相似

image.png

因此atomic只能保证setter、getter方法的线程安全,并不能保证数据安全,我们来看个🌰

    self.index = 0;

    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        for (NSInteger i = 0; i < 10000; i++) {
            self.index = self.index + 1;
            NSLog(@"%ld === %ld",(long)i,(long)self.index);
        }
    });
    
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        for (NSInteger i = 0; i < 10000; i++) {
            self.index = self.index + 1;
            NSLog(@"%ld === %ld",(long)i,(long)self.index);
        }
    });

image.png

理想情况下,最终index应该是20000,但最终结果却不是。

  • atomic保证变量在取值和赋值时的线程安全
  • 但不能保证self.index+1也是安全的
  • 如果改成self.index=i是能保证setter方法的线程安全的

3. 读写锁

读写锁实际是一种特殊的自旋锁,它把对共享资源的访问者划分成读者和写者,读者只对共享资源进行读访问,写者则需要对共享资源进行写操作。这种锁相对于自旋锁而言,能提高并发性,因为在多处理器系统中,它允许同时有多个读者来访问共享资源,最大可能的读者数为实际的CPU数。

  • 写者是排他性的,⼀个读写锁同时只能有⼀个写者或多个读者(与CPU数相关),但不能同时既有读者⼜有写者。在读写锁保持期间也是抢占失效的

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

  • 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实现多读单写功能:

image.png

image.png

从结果来看,写的时候都说单独存在,读的时候可以多个线程读取。

当然我们也可以使用GCD栅栏函数dispatch_barrier_async来实现

image.png

image.png

三: 互斥锁

1. @synchronized

@synchronized用法简单,但是需要注意的是它的性能是最差的。

@synchronized (self) {
    // 🌹要执行的代码
}

上文中卖票示例加了@synchronized卖票就正常了,那么问题来了,为什么加了一把@synchronized锁之后,数据就安全了呢?为什么传入的参数是 self 呢?传入 nil 行不行呢?

定位源码

我们先cdViewController所在文件夹,使用xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc ViewController.m -o ViewController.cpp 命令生成.cpp文件`

image.png

可以看到,调用了objc_sync_enter方法,并且使用了try-catch,在正常处理流程中,提供了_SYNC_EXIT结构体,最后也会调用对应的析构函数objc_sync_exit。这里最重要的其实就是如下两个方法

  • objc_sync_enter
  • objc_sync_exit

如下图位置打上断点,运行起来看汇编代码

image.png

通过汇编,我们发现调用了objc_sync_enterobjc_sync_exit image.png

这也验证了底层确实调用了objc_sync_enterobjc_sync_exit

知道调用了什么方法依然无法解决我们的困惑,objc_sync_enterobjc_sync_exit做了些什么呢?

添加符号断点,最终定位到libobjc.A.dylib

image.png

相关数据结构

  • SyncData
typedef struct SyncData {
    struct SyncData* nextData; //这个是一个单链表结构,其中包含了一个相同的数据结构。
    object; //这里是使用了`DisguisedPtr`进行了包装,方便计算和传递。
    threadCount; //线程的数量(多线程的时候使用),有多少个线程对该对象进行加锁的操作。
    recursive_mutex_t mutex;  //递归互斥锁。
} SyncData;

该数据结构为 @synchronized实现原理中最基本的数据结构,其中记录了提供的用于加锁的变量,使用该变量加锁的线程数以及与该变量一一对应的一个锁。

  • SyncCacheItem
typedef struct {
    SyncData *data;             //该缓存条目对应的SyncData
    unsigned int lockCount;     //该对象在该线程中被加锁的次数
} SyncCacheItem;

该数据结构用来记录某个 SyncData在某个线程中被加锁的记录,由定义可知,一个 SyncData可以被多个 SyncCacheItem持有。

  • SyncCache
typedef struct SyncCache {
    unsigned int allocated;     //该缓存此时对应的缓存大小
    unsigned int used;          //该缓存此时对应的已使用缓存大小
    SyncCacheItem list[0];      //SyncCacheItem数组
} SyncCache;

该数据结构用来记录某个线程中所有 1SyncCacheItem1,并且记录了缓存大小以及已使用缓存大小。

  • StripedMap<SyncList>
struct SyncList {
    SyncData *data;     //SyncData数组
    spinlock_t lock;    //自旋锁

    SyncList() : data(nil) { }
};

#define LOCK_FOR_OBJ(obj) sDataLists[obj].lock
#define LIST_FOR_OBJ(obj) sDataLists[obj].data
static StripedMap<SyncList> sDataLists;

sDataListsStripedMap<SyncList>类型的一个静态变量。其中 StripedMap为一个最大可以存储64个变量的字典,LOCK_FOR_OBJ(obj)LIST_FOR_OBJ(obj)两个宏可以根据 obj的内存地址来获取对应的 SyncList中的 datalock

  • _objc_pthread_data

iOS中,每个线程都维护一个 _objc_pthread_data的结构体,该结构体下维护一个 SyncCache,该 SyncCache初始大小为 4SyncData大小,当 SyncCache缓存填满时,会以上次大小的 2倍进行扩充。

  • TLS

TLS全称为 Thread Local Storage,在iOS中,每个线程都拥有自己的 TLS,负责保存本线程的一些变量, TLS无需锁保护。

tls_get_direct/ tls_set_direct提供了快速从当前线程获取/设置对应变量的方法。

iOS中内设了两个宏,SYNC_DATA_DIRECT_KEY/ SYNC_COUNT_DIRECT_KEY,它们的用是与tsl_get_direct/ tls_set_direct配合,分别对 SyncCacheItem.dataSyncCacheItem.lockCount进行读取与设置。

另外, _objc_pthread_data其实也是保存在 tls中的,它对应的读取关键字为 _objc_pthread_key

源码分析

objc_sync_enter
根据官方的注释,objc_sync_enter内部使用了递归互斥锁。

image.png

objc_sync_exit

image.png

  • 无论是objc_sync_enter还是objc_sync_exit有一个共同操作,都会对@synchronized()传入的objc进行判断,如果objc为空,则什么都不会做。
  • objc_sync_enterobjc_sync_exit遥相呼应,一个加锁,一个解锁
  • objc不为空,在objc_sync_enter中,通过id2data方法获取一个SyncData类型的data,并对调用mutex属性进行上锁lock()操作。在objc_sync_exit中,同样获取对应的SyncData对象,然后调用data->mutex.tryUnlock()进行解锁。
  • SyncData是一个结构体(上面已经介绍)。

alignas(CacheLineSize)上层是一个StripedMap,前面已经介绍。 image.png

多线程下整体结构如下,其中SyncList就是一条线程

image.png

因此,从上述信息来看,@synchronized支持递归锁,并且支持多线程访问。

objc_sync_enter中,通过id2data获取到的data,先看下它的源码实现,代码相对较长,我们折叠起来看下整体,然后再展开看细节。

image.png

主要分四个部分:

  • 从线程缓存中获取当前线程的SyncData
  • 从缓存中获取SyncData
  • 缓存中没有找到的情况,内部处理。
  • 进行缓存。

先看第一部分,从线程缓存中获取当前线程的SyncData,展开代码来看

image.png

再看第二部分,从缓存中获取SyncData

image.png

再看第三部分,当缓存中都找不到时候,内部处理

image.png

最后第四部分,进行缓存

image.png

  • @synchronized底层封装了recursive_mutex_t,之所以能够可重入,在于拓展了这个递归锁,增加了lockCount,为了防止多线程重入,又增加了threadCount进行处理,而这两点也是@synchronized的关键核心所在。

  • @synchronized之所以性能低,原因在于链表的查询,下层缓存的不断查找。

使用注意

我们先看下面一段代码

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

这段代码运行起来肯定会崩,原因在于testArray不断的初始化,会调用setter方法,对新值retain,对旧值release,然而在这个过程中,会出现上一个还未release,下一个已经准备release,这样会导致野指针的产生。

我们使用@synchronized来处理。

-(void)test {
    for (int i = 0; i < 200000; i++) {
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            @synchronized (_testArray) {
                _testArray = [NSMutableArray array];
            }
        });
    }
}

然而......还是崩了,因为使用_testArray并没有实际意思,在释放的过程中某一个时刻_testArraynil,那相当于没有锁,自然还是崩溃。

我们把_testArray换成self试试

-(void)test {
    for (int i = 0; i < 200000; i++) {
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            @synchronized (self) {
                _testArray = [NSMutableArray array];
            }
        });
    }
}

完美解决问题,这里有个疑问,为什么使用self就可以了呢?原因在于self拥有长生命周期,并且self持有testArray,传入的对象self不会为nil。

总结

  • @synchronized在底层封装的是一把递归锁,所以这个锁是递归互斥锁

  • synchronized 哈希表 - 拉链法 存储SyncData sDataLists里面是一个 array存储的是 SyncListSyncList 里面是绑定的objectobjc_sync_enter / exit 对称 递归锁。两种存储 : TLS/ Cache 。第⼀次的时候 SyncData 才用头插法 -链表 ,标记 thracount = 1 , 然后下次再进来会判断是不是同⼀个对象 ,是同一个对象TLS --> lockCount ++ ,不是同一个的话TLS 找不到 就会去创建一个SyncData threadCount ++ objc_sync_exit 的话就是lockCount-- threadCount--

  • @synchronized的可重入,即可嵌套,主要是由于lockCount 和 threadCount的搭配

  • @synchronized使用链表的原因是链表方便下一个data的插入

  • 但是由于底层中链表查询、缓存的查找以及递归,是非常耗内存以及性能的,导致性能低,所以在前文中,该锁的排名在最后

  • 但是目前该锁的使用频率仍然很高,主要是因为方便简单,且不用解锁

  • 不能使用非OC对象作为加锁对象,因为其object的参数为id

  • @synchronized (self)这种适用于嵌套次数较少的场景。这里锁住的对象也并不永远是self,这里需要读者注意

  • 如果锁嵌套次数较多,即锁self过多,会导致底层的查找非常麻烦,因为其底层是链表进行查找,所以会相对比较麻烦,所以此时可以使用NSLock、信号量

2. NSLock

先看上面的示例,使用NSLock同样能解决崩溃的问题

NSLock *jw_lock = [[NSLock alloc]init];
for (int i = 0; i < 200000; i++) {
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            [jw_lock lock];
            _testArray = [NSMutableArray array];
            [jw_lock unlock];
        });
    }

源码分析

通过下符号断点的方式,定位到NSLock的源码在Foundation当中,但其并不开源,我们使用Swift版本swift-corelibs-foundation,来窥探OC下的实现逻辑。

image.png

从源码来看,实现其实比较简单,就是对pthread_mutex的封装使用。

使用注意

先看一段代码

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) {
                  testMethod(value - 1);
                }
            };
            testMethod(10);
        });
    }

打印结果:

image.png

看结果肯定是有问题的,我们使用NSLock来加锁,先看正确做法。

for (int i= 0; i<100; i++) {
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            static void (^testMethod)(int);
            [lock lock];
            testMethod = ^(int value){
                if (value > 0) {
                  NSLog(@"current value = %d",value);
                  testMethod(value - 1);
                }
            };
            testMethod(10);
            [lock unlock];
        });
    }

打印结果:

image.png

再来看错误的做法

for (int i= 0; i<100; i++) {
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            static void (^testMethod)(int);
            testMethod = ^(int value){
                [lock lock];
                if (value > 0) {
                  NSLog(@"current value = %d",value);
                  testMethod(value - 1);
                }
                [lock unlock];
            };
            testMethod(10);
        });
    }

打印结果:

image.png

只打印了10,就什么都没有了,难道是卡死了吗?当然不是,这段代码是block内部出现了嵌套使用,互斥锁在递归调用时会造成堵塞,并非死锁——这里的问题是后面的代码无法执行下去。

  • 第一次加完锁之后还没出锁就进行递归调用
  • 第二次加锁就堵塞了线程(因为不会查询缓存)

解决:使用递归锁NSRecursiveLock替换NSLock

3. NSRecursiveLock

源码分析

image.png

NSRecursiveLockNSLock源码类似,最重要的地方在上图标出,传入PTHREAD_MUTEX_RECURSIVE,标记它是递归锁。

for (int i= 0; i<100; i++) {
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            static void (^testMethod)(int);
            [recursiveLock lock];
            testMethod = ^(int value){
                if (value > 0) {
                  NSLog(@"current value = %d",value);
                  testMethod(value - 1);
                }
                [recursiveLock unlock];
            };
            testMethod(10);
        });
    }

打印结果:

image.png

4. NSCondition

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

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

源码分析

image.png

底层也是封装了互斥锁。

使用

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

- (void)jw_producer{
    self.ticketCount = self.ticketCount + 1;
    NSLog(@"生产一个 现有 count %zd",self.ticketCount);
}

- (void)jw_consumer{
    if (self.ticketCount == 0) {
        NSLog(@"等待 count %zd",self.ticketCount);
    }

    self.ticketCount -= 1;
    NSLog(@"消费一个 还剩 count %zd ",self.ticketCount);
}

image.png

查看打印结果的最后,这肯定是有问题的,读写混乱。可以看到出现了生产和消费对不上的情况,消费了一个还剩一个,再生产一个是2,却出现了3,这就是线程不安全访问的事故了。

所以我们要保证生产线、消费线数据的安全,就需要进行加锁处理,以保证多线程安全,但这只是它们内部的得到保证了,但是它们之间存在消费关系,比如生产的库存没有了,不得通知,消费者进行等待,生产好了再通知消费者来消费买单

下面我们使用NSCondition来解决这个问题

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

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

- (void)jw_consumer{
    [_testCondition lock];  🌹// 操作的多线程影响
    if (self.ticketCount == 0) {
        NSLog(@"等待 count %zd",self.ticketCount);
        [_testCondition wait];🌹// 数量为0 ,只能等待生产
    }

    self.ticketCount -= 1;
    NSLog(@"消费一个 还剩 count %zd ",self.ticketCount);
    [_testCondition unlock];🌹
}

image.png

使用NSCondition之后,打印的数据生产消费数量正确,解决了读写混乱的问题。

5. NSConditionLock

  • NSConditionLock 是锁,⼀旦⼀个线程获得锁,其他线程⼀定等待

  • [xxxx lock]: 表示 xxx 期待获得锁,如果没有其他线程获得锁(不需要判断内部的condition) 那它能执⾏此⾏以下代码,如果已经有其他线程获得锁(可能是条件锁,或者⽆条件锁),则等待,直⾄其他线程解锁

  • [xxx lockWhenCondition:A条件]: 表示如果没有其他线程获得该锁,但是该锁内部的condition不等于A条件,它依然不能获得锁,仍然等待。如果内部的condition等于A条件,并且没有其他线程获得该锁,则进⼊代码区,同时设置它获得该锁,其他任何线程都将等待它代码的完成,直⾄它解锁。

  • [xxx unlockWithCondition:A条件: 表示释放锁,同时把内部的condition设置为A条件

  • return = [xxx lockWhenCondition:A条件 beforeDate:A时间]: 表示如果被锁定(没获得锁),并超过该时间则不再阻塞线程。但是注意:返回的值是NO,它没有改变锁的状态,这个函数的⽬的在于可以实现两种状态下的处理

  • 所谓的condition就是整数,内部通过整数⽐较条件

源码分析

image.png

使用

- (void)jw_testConditonLock{
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
        NSLog(@"线程 1");
    });
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
        NSLog(@"线程 2");
    });
    
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
       NSLog(@"线程 3");
    });
}

上面这段代码由于多线程的原因,打印肯定是无序的,下面我们使用NSConditionLock来控制打印顺序。

- (void)jw_testConditonLock{
   NSConditionLock *conditionLock = [[NSConditionLock alloc] initWithCondition:2];
   
   dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
        [conditionLock lockWhenCondition:1];🌹// condition = 1 内部进行条件匹配,如果不相同就不执行,会往下走到线程2 
        NSLog(@"线程 1");
        [conditionLock unlockWithCondition:0];
    });
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
        [conditionLock lockWhenCondition:2]; 🌹// 条件condition= 2 与外界条件相同,执行
        NSLog(@"线程 2");
        [conditionLock unlockWithCondition:1];🌹 // 执行完成 设置condition = 1
    });
    
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        [conditionLock lock]; 🌹// 线程3之所以最先打印,是因为压根就没有条件,能正常执行,只是局部加减锁 
         NSLog(@"线程 3");
        [conditionLock unlock];
    });
}

打印结果一直是321。原因代码里已经标明。

四:参考

synchronized实现原理及缺陷分析

不再安全的 OSSpinLock

iOS底层学习 - 多线程之中的锁

iOS开发中的11种锁以及性能对比

本篇章对多线程中进行了学习和记录,好记性不如烂笔头,以备不时之需!!!