前面几篇我们探索了iOS
使用频率很高的多线程技术GCD
,本篇我们探索多线程中一个重要的概念锁
。
锁的分类 锁主要分为两大类自旋锁
和互斥锁
。
自旋锁 在自旋锁中,线程会反复检查变量是否可用
。由于线程这个过程中一致保持执行,所以是一种忙等待
。 一旦获取了自旋锁,线程就会一直保持该锁
,直到显式释放自旋锁。自旋锁避免了进程上下文的调度开销,因此对于线程只会阻塞很短时间的场合
是有效
的。对于iOS属性的修饰符atomic
,自带一把自旋锁
互斥锁 互斥锁
是一种用于多线程编程
中,防止两条线程同时对同一公共资源(例如全局变量)进行读写的机制
,该目的是通过将代码切成一个个临界区
而达成。
读写锁 读写锁实际是一种特殊的互斥锁,它把对共享资源的访问者划分成读者和写者,读者只对共享资源 进行读访问,写者则需要对共享资源进行写操作。这种锁相对于自旋锁而言,能提高并发性,因为在多处理器系统中,它允许同时有多个读者来访问共享资源,最大可能的读者数为实际的逻辑CPU 数。写者是排他性的,一个读写锁同时只能有一个写者或多个读者(与CPU数相关),但不能同时既有读者又有写者。在读写锁保持期间也是抢占失效的。
如果读写锁当前没有读者,也没有写者,那么写者可以立刻获得读写锁,否则它必须自旋在那里, 直到没有任何写者或读者。如果读写锁没有写者,那么读者可以立即获得该读写锁,否则读者必须自旋在那里,直到写者释放该读写锁。
当读写锁是写加锁状态时, 在这个锁被解锁之前, 所有试图对这个锁加锁的线程都会被阻塞.当读写锁在读加锁状态时, 所有试图以读模式对它进行加锁的线程都可以得到访问权, 但是如果线程希望以写模式对此锁进行加锁, 它必须直到所有的线程释放锁.通常, 当读写锁处于读模式锁住状态时, 如果有另外线程试图以写模式加锁, 读写锁通常会阻塞随后的读模式锁请求, 这样可以避免读模式锁⻓期占用, 而等待的写模式锁请求⻓期阻塞.读写锁适合于对数据结构的读次数比写次数多得多的情况. 因为, 读模式锁定时可以共享, 以写模式锁住时意味着独占, 所以读写锁又叫共享-独占锁.
几种锁的性能对比 我们通过代码打印的方式比较各种锁性能:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 int js_runTimes = 100000; /** OSSpinLock 性能 */ { OSSpinLock js_spinlock = OS_SPINLOCK_INIT; double_t js_beginTime = CFAbsoluteTimeGetCurrent(); for (int i=0 ; i < js_runTimes; i++) { OSSpinLockLock(&js_spinlock); //解锁 OSSpinLockUnlock(&js_spinlock); } double_t js_endTime = CFAbsoluteTimeGetCurrent() ; JSLog(@"OSSpinLock: %f ms",(js_endTime - js_beginTime)*1000); } /** dispatch_semaphore_t 性能 */ { dispatch_semaphore_t js_sem = dispatch_semaphore_create(1); double_t js_beginTime = CFAbsoluteTimeGetCurrent(); for (int i=0 ; i < js_runTimes; i++) { dispatch_semaphore_wait(js_sem, DISPATCH_TIME_FOREVER); dispatch_semaphore_signal(js_sem); } double_t js_endTime = CFAbsoluteTimeGetCurrent() ; JSLog(@"dispatch_semaphore_t: %f ms",(js_endTime - js_beginTime)*1000); } /** os_unfair_lock_lock 性能 */ { os_unfair_lock js_unfairlock = OS_UNFAIR_LOCK_INIT; double_t js_beginTime = CFAbsoluteTimeGetCurrent(); for (int i=0 ; i < js_runTimes; i++) { os_unfair_lock_lock(&js_unfairlock); os_unfair_lock_unlock(&js_unfairlock); } double_t js_endTime = CFAbsoluteTimeGetCurrent() ; JSLog(@"os_unfair_lock_lock: %f ms",(js_endTime - js_beginTime)*1000); } /** pthread_mutex_t 性能 */ { pthread_mutex_t js_metext = PTHREAD_MUTEX_INITIALIZER; double_t js_beginTime = CFAbsoluteTimeGetCurrent(); for (int i=0 ; i < js_runTimes; i++) { pthread_mutex_lock(&js_metext); pthread_mutex_unlock(&js_metext); } double_t js_endTime = CFAbsoluteTimeGetCurrent() ; JSLog(@"pthread_mutex_t: %f ms",(js_endTime - js_beginTime)*1000); } /** NSlock 性能 */ { NSLock *js_lock = [NSLock new]; double_t js_beginTime = CFAbsoluteTimeGetCurrent(); for (int i=0 ; i < js_runTimes; i++) { [js_lock lock]; [js_lock unlock]; } double_t js_endTime = CFAbsoluteTimeGetCurrent() ; JSLog(@"NSlock: %f ms",(js_endTime - js_beginTime)*1000); } /** NSCondition 性能 */ { NSCondition *js_condition = [NSCondition new]; double_t js_beginTime = CFAbsoluteTimeGetCurrent(); for (int i=0 ; i < js_runTimes; i++) { [js_condition lock]; [js_condition unlock]; } double_t js_endTime = CFAbsoluteTimeGetCurrent() ; JSLog(@"NSCondition: %f ms",(js_endTime - js_beginTime)*1000); } /** PTHREAD_MUTEX_RECURSIVE 性能 */ { pthread_mutex_t js_metext_recurive; pthread_mutexattr_t attr; pthread_mutexattr_init (&attr); pthread_mutexattr_settype (&attr, PTHREAD_MUTEX_RECURSIVE); pthread_mutex_init (&js_metext_recurive, &attr); double_t js_beginTime = CFAbsoluteTimeGetCurrent(); for (int i=0 ; i < js_runTimes; i++) { pthread_mutex_lock(&js_metext_recurive); pthread_mutex_unlock(&js_metext_recurive); } double_t js_endTime = CFAbsoluteTimeGetCurrent() ; JSLog(@"PTHREAD_MUTEX_RECURSIVE: %f ms",(js_endTime - js_beginTime)*1000); } /** NSRecursiveLock 性能 */ { NSRecursiveLock *js_recursiveLock = [NSRecursiveLock new]; double_t js_beginTime = CFAbsoluteTimeGetCurrent(); for (int i=0 ; i < js_runTimes; i++) { [js_recursiveLock lock]; [js_recursiveLock unlock]; } double_t js_endTime = CFAbsoluteTimeGetCurrent() ; JSLog(@"NSRecursiveLock: %f ms",(js_endTime - js_beginTime)*1000); } /** NSConditionLock 性能 */ { NSConditionLock *js_conditionLock = [NSConditionLock new]; double_t js_beginTime = CFAbsoluteTimeGetCurrent(); for (int i=0 ; i < js_runTimes; i++) { [js_conditionLock lock]; [js_conditionLock unlock]; } double_t js_endTime = CFAbsoluteTimeGetCurrent() ; JSLog(@"NSConditionLock: %f ms",(js_endTime - js_beginTime)*1000); } /** @synchronized 性能 */ { double_t js_beginTime = CFAbsoluteTimeGetCurrent(); for (int i=0 ; i < js_runTimes; i++) { @synchronized(self) {} } double_t js_endTime = CFAbsoluteTimeGetCurrent() ; JSLog(@"@synchronized: %f ms",(js_endTime - js_beginTime)*1000); }
在iPhone 12pro模拟器打印的结果为:
在iPhone12 mini真机的结果如下:
可以看到模拟器上@synchronized 锁性能是比较差的,但12系列(xr经过测试并没提高)手机的性能有很大提升,我们项目中会比较常见,我们就从@synchronized 开始探索。
@synchronized xcrun分析 我们在main.m
文件里写一个锁:
1 2 3 4 5 6 7 8 9 10 11 int main(int argc, char * argv[]) { NSString * appDelegateClassName; @autoreleasepool { // Setup code that might create autoreleased objects goes here. appDelegateClassName = NSStringFromClass([AppDelegate class]); @synchronized (appDelegateClassName) { } } return UIApplicationMain(argc, argv, nil, appDelegateClassName); }
使用xcrun
命令将其编译成.cpp
文件
xcrun -sdk iphoneos clang -arch arm64e -rewrite-objc main.m
在main.cpp
文件最下方找到main
函数的实习,定位到@synchronized
代码块
将代码排版之后:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 { 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);} }
加锁成功的情况我们只需要关注try
代码块及以上的代码。经过我们的简化后的代码:
1 2 3 id _sync_obj = (id)appDelegateClassName; objc_sync_enter(_sync_obj); objc_sync_exit(sync_exit);
可以看到主要就是执行了两个函数objc_sync_enter
和objc_sync_exit
。
libobjc源码分析 通过打符号断点objc_sync_enter
,我们可以知道objc_sync_enter
在libobjc
源码中。
接下来我们在libobjc
源码中全局搜索objc_sync_enter
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 int objc_sync_enter (id obj) { int result = OBJC_SYNC_SUCCESS; if (obj) { SyncData* data = id2data(obj, ACQUIRE); ASSERT(data); data->mutex.lock(); } else { if (DebugNilSync) { _objc_inform("NIL SYNC DEBUG: @synchronized(nil); set a breakpoint on objc_sync_nil to debug" ); } objc_sync_nil(); } return result; }
如果传入obj
为nil
,会什么也不做。主要看if
代码块的代码,可以看到加锁是通过data->mutex.lock()
,也就是SyncData
的实例,所以我们先探究一下SyncData
类型的结构:
SyncData 1 2 3 4 5 6 typedef struct alignas (CacheLineSize) SyncData { struct SyncData * nextData ; DisguisedPtr<objc_object> object; int32_t threadCount; recursive_mutex_t mutex; } SyncData;
nextData
:链表结构下一个节点
object
:参照关联对象的结构,哈希表
threadCount
:使用block的线程数量
mutex
:递归锁(单用多线程会出现问题)
在初始化SyncData
实例的时候使用的是id2data
函数,我们接下来探索这个函数。
id2data函数 id2data
函数有150+行,我们先隐藏代码块,总览一下结构:
函数的最开始两行有两个宏,它们的定义如下:
1 2 3 #define LOCK_FOR_OBJ(obj) sDataLists[obj].lock #define LIST_FOR_OBJ(obj) sDataLists[obj].data static StripedMap<SyncList> sDataLists;
sDataLists
是一个静态哈希表结构,我们使用lldb
查看它的数据结构:
经过断点调试,第一次进来执行的代码是:
1 2 3 4 5 6 posix_memalign((void **)&result, alignof (SyncData), sizeof (SyncData)); result->object = (objc_object *)object; result->threadCount = 1 ; new (&result->mutex) recursive_mutex_t (fork_unsafe_lock);result->nextData = *listp; *listp = result;
这里有个细节就是listp
使用的头插法
将新的元素添加到链表,它的可递归性的实现依赖了这种数据结构的使用。
小结
synchronized的数据结构是哈希表,采用的拉链法处理哈希冲突
sDataLists arrary key是和对象相关的,拉链链表里的元素是同一个对象的锁。
objc_sync_enter和objc_sync_exit是对称的,它是一把递归锁。
会有两种存储结构:tls和catch
第一次访问syncData采用的是头插法链表结构 标记threadCount = 1
后续访问,会判断是不是同一个对象,同一对象lockcount++,不是同一个对象threadCount++1。
synchronized是一种可重入、递归的多线程锁,原因
tls保障 threadCount 可以有多个线程对这个锁对象加锁
lock++会记录总共锁了多少次。
NSLock和NSRecursiveLock使用 我们以一个实例分析这两种锁的区别:
1 2 3 4 5 6 7 8 9 10 11 12 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 ); }); }
上面这个代码有多线程冲突的问题,打印的结果无序,不是我们想要的结果
使用NSLock
解决问题 使用NSLock
解决的方法其实很简单,就是在我们调用方法testMethod
前后加锁。
我们再看打印结果就正常了。
NSLock
适用的是在最外层加锁,如果我们能写的代码只能在testMethod
操作,这个时候加NSLock
就不会正常工作了。
使用NSRecursiveLock 上面我们知道NSLock
在testMethod
无法解决问题,我们尝试用NSRecursiveLock
解决。
发现NSRecursiveLock
并不能解决问题,而且还会偶现崩溃。NSRecursiveLock
是一把递归锁,但是它并不支持多线程递归。
使用@synchronized
使用@synchronized解决了业务代码里的问题,说明@synchronized是一把支持多线程的递归锁。
NSCondition NSCondition 的对象实际上作为一个锁和一个线程检查器:锁主要 为了当检测条件时保护数据源,执行条件引发的任务;线程检查器 主要是根据条件决定是否继续运行线程,即线程是否被阻塞。它主要有四个方法
[condition lock] :一般用于多线程同时访问、修改同一个数据源,保证在同一 时间内数据源只被访问、修改一次,其他线程的命令需要在lock 外等待,只到 unlock ,才可访问
[condition unlock];// 与lock 同时使用
[condition wait];// 让当前线程处于等待状态
[condition signal];//CPU 发信号告诉线程不用在等待,可以继续执行
它的一个应用场景之一就是生产者-消费者
模型。也就是通过多线程进行生产和销售产品,当产品数量为0的时候就只能等待,具体代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 - (void )js_testConditon{ _testCondition = [[NSCondition alloc] init]; for (int i = 0 ; i < 50 ; i++) { dispatch_async (dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0 ), ^{ [self js_producer]; }); dispatch_async (dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0 ), ^{ [self js_consumer]; }); dispatch_async (dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0 ), ^{ [self js_consumer]; }); dispatch_async (dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0 ), ^{ [self js_producer]; }); } } - (void )js_producer{ [_testCondition lock]; self .ticketCount = self .ticketCount + 1 ; NSLog (@"生产一个 现有 count %zd" ,self .ticketCount); [_testCondition signal]; [_testCondition unlock]; } - (void )js_consumer{ [_testCondition lock]; if (self .ticketCount == 0 ) { NSLog (@"等待 count %zd" ,self .ticketCount); [_testCondition wait]; } self .ticketCount -= 1 ; NSLog (@"消费一个 还剩 count %zd " ,self .ticketCount); [_testCondition unlock]; }
Foundation源码看锁的封装 我们在swift-corelibs-foundation
源码中探索。
通过源码我们看到,NSLock
等锁都实现了一个协议就是NSLocking
1 2 3 4 public protocol NSLocking { func lock () func unlock () }
它们都是对pthread
的封装
NSRecursiveLock,也类似,它和NSLock
的区别是pthread_mutexattr_settype(attrs, Int32(PTHREAD_MUTEX_RECURSIVE))
NSConditionLock
NSConditionLock 是锁,一旦一个线程获得锁,其他线程一定等待
lock函数:表示 对象期待获得锁,如果没有其他线程获得锁(不需要判断内部的 condition) 那它能执行此行以下代码,如果已经有其他线程获得锁(可能是条件锁,或者无条件 锁),则等待,直至其他线程解锁
[xx lockWhenCondition ::A 条件]方法:表示如果没有其他线程获得该锁,但是该锁内部的 condition 不等于A 条件,它依然不能获得锁,仍然等待。如果内部的condition 等于A 条件,并且 没有其他线程获得该锁,则进入代码区,同时设置它获得该锁,其他任何线程都将等待它代码的 完成,直至它解锁。
[xxx unlockWithCondition:A 条件]; 表示释放锁,同时把内部的condition 设置为A 条件
return = [xxx lockWhenCondition:A 条件 beforeDate:A 时间]; 表示如果被锁定(没获得 锁),并超过该时间则不再阻塞线程。但是注意:返回的值是NO, 它没有改变锁的状态,这个函 数的目的在于可以实现两种状态下的处理。
栅栏函数实现读写锁 读写锁主要要实现以下功能:
多读单写功能。
写入和写入互斥。
读和写入互斥。
写入不能阻塞主线程任务执行。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 @interface ViewController ()@property (nonatomic , strong ) dispatch_queue_t js_currentQueue;@property (nonatomic , strong ) NSMutableDictionary *mDict;@end @implementation ViewController - (void )viewDidLoad { [super viewDidLoad]; self .js_currentQueue = dispatch_queue_create("jscurrent" , DISPATCH_QUEUE_CONCURRENT); self .mDict = [[NSMutableDictionary alloc] init]; [self js_safeSetter:@"123" time:10 ]; [self js_safeSetter:@"456" time:5 ]; [self js_safeSetter:@"789" time:3 ]; } - (void )touchesBegan:(NSSet <UITouch *> *)touches withEvent:(UIEvent *)event{ for (int i = 0 ; i < 5 ; i++) { dispatch_async (dispatch_get_global_queue(0 , 0 ), ^{ NSLog (@"读取,name = %@ thread---%@" ,[self js_safeGetter],[NSThread currentThread]); }); } } - (void )js_safeSetter:(NSString *)name time:(int )time{ dispatch_barrier_async(self .js_currentQueue, ^{ sleep(time); [self .mDict setValue:name forKey:@"name" ]; NSLog (@"写入,name = %@ thread---%@" ,name,[NSThread currentThread]); }); } - (NSString *)js_safeGetter{ __block NSString *result; dispatch_sync (self .js_currentQueue, ^{ result = self .mDict[@"name" ]; }); return result; } @end
程序运行我们就开始点击屏幕(读操作),最后看打印结果: