- 本文涉及:
@synchronized
,NSLock
,NSRecursiveLock
,NSCondition
,NSConditionLock
,以及部分pthread
什么是互斥锁 mutex
在处理一些关键数据时,我们不希望这个数据此时不能被外界操作,直到处理完成。 (否则我们和外界都可能同时对该数据处理,导致数据失真,或者说这个操作是线程不安全的)
解决办法是:在处理数据的代码前后,设置一组 红绿灯 (代码前:红灯;代码后:绿灯)
不要把
互斥锁
想象成一把锁
@synchronized
-
简单使用,递归也能用,输出完美
while (1) { dispatch_async(dispatch_get_global_queue(0, 0), ^{ static void (^testMethod)(int); testMethod = ^(int value) { @synchronized (self) { if (value > 0) { NSLog(@"A current value = %d",value); testMethod(value - 1); } } }; testMethod(3); }); }
2020-04-27 17:50:48.903259+0800 003-NSLock分析[86846:4489612] A current value = 3 2020-04-27 17:50:48.903376+0800 003-NSLock分析[86846:4489612] A current value = 2 2020-04-27 17:50:48.903461+0800 003-NSLock分析[86846:4489612] A current value = 1 2020-04-27 17:50:48.903562+0800 003-NSLock分析[86846:4489633] A current value = 3 2020-04-27 17:50:48.903649+0800 003-NSLock分析[86846:4489633] A current value = 2 2020-04-27 17:50:48.903736+0800 003-NSLock分析[86846:4489633] A current value = 1 2020-04-27 17:50:48.903844+0800 003-NSLock分析[86846:4489614] A current value = 3
@synchronized 在 .cpp 的真面目
结论:相当于
objc_sync_enter
和objc_sync_exit
-
main.m
#import
#import "AppDelegate.h" int main(int argc, char * argv[]) { NSString * appDelegateClassName; @autoreleasepool { // Setup code that might create autoreleased objects goes here. appDelegateClassName = NSStringFromClass([AppDelegate class]); @synchronized (appDelegateClassName) { NSLog(@"hello!!"); } } return UIApplicationMain(argc, argv, nil, appDelegateClassName); } -
在 main.m 所在位置打开终端执行
clang -x objective-c -rewrite-objc -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk main.m
-
main.cpp
// @synchronized (appDelegateClassName) 开始 { id _rethrow = 0; id _sync_obj = (id)appDelegateClassName; objc_sync_enter(_sync_obj); try { struct _SYNC_EXIT { // 构造函数 _SYNC_EXIT(id arg) : sync_exit(arg) {} // try 结束时,这个结构体被销毁会调用这个 析构函数 ~_SYNC_EXIT() {objc_sync_exit(sync_exit);} id sync_exit; // 属性 } _sync_exit(_sync_obj); // 等于 NSLog(@"hello!!"); NSLog((NSString *)&__NSConstantStringImpl__var_folders_sf_4y610g716zd3clg8kh9c355h0000gn_T_main_76882b_mi_0); } 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 (appDelegateClassName) 结束
使用 @synchronized 的注意事项
注意传入
object
的生命周期,一旦为nil
,相当于解锁;不过,此特性也可以在某些场合加以利用,以达到提前解锁的目的性能低 (也不是那么的低,只是比其他
互斥锁
低)
@synchronized 的底层实现和优势
-
从
objc_sync_enter
和objc_sync_exit
的源码得知,实现逻辑是将传入的obj
存入一个 邻接表 维护,它记录了- 多少条线程使用了这个
obj
(多少条线程锁了这个blcok
)---线程间安全问题 - 某线程使用了这个
obj
多少次 (某线程锁了这个block
多少次)---线程内递归问题
- 多少条线程使用了这个
目前来看,是比较可靠的 (尤其是避免了
递归锁
的多线程坑,下面会提到)
Posix 多线程入门 (pthread)
- 必看
锁1
和锁2
两篇文章,以更深入理解后面的内容
多线程详解
https://www.ibm.com/developerworks/cn/linux/thread/posix_thread1/index.html
锁1
https://www.ibm.com/developerworks/cn/linux/thread/posix_thread2/index.html
锁2
https://www.ibm.com/developerworks/cn/linux/thread/posix_thread3/index.html
NSLock 和 NSRecursiveLock 的一些区别
以下将 NSLock 称为 互斥锁,NSRecursiveLock 称为 递归锁 (可重入锁)
互斥锁
互斥锁
表面上可以在多条线程lock()
和unlock()
,但有可能造成未知问题。正确的使用方法是在同一线程中成对的调用lock()
和unlock()
。-
某线程持有
互斥锁
且lock()
的次数 == 1 即可在线程安全的情况下执行代码
次数 == 0 就是没上锁
-
次数 > 1 的线程 则进入休眠。如果某个线程持有互斥锁 且 (因为该互斥锁而) 进入休眠,则 一直堵塞,譬如 ↓
NSLock *myLock = [[NSLock alloc] init]; dispatch_async(dispatch_get_global_queue(0, 0), ^{ NSLog(@"aaa"); [myLock lock]; [myLock lock]; [myLock unlock]; [myLock unlock]; [myLock unlock]; NSLog(@"bbb"); });
2020-04-25 22:39:02.273102+0800 003-NSLock分析[82070:3747886] aaa
-
但此时在其他线程
unlock()
,可以使互斥锁
的次数 -1 ↓ (只为了说明问题,在实际开发中是禁忌)dispatch_async(dispatch_get_global_queue(0, 0), ^{ NSLog(@"aaa"); [myLock lock]; [myLock lock]; [myLock unlock]; [myLock unlock]; [myLock unlock]; NSLog(@"bbb"); }); sleep(1); [myLock unlock]; NSLog(@"ccc");
2020-04-25 22:44:09.836811+0800 003-NSLock分析[82096:3752167] aaa 2020-04-25 22:44:10.837882+0800 003-NSLock分析[82096:3751906] ccc 2020-04-25 22:44:10.837922+0800 003-NSLock分析[82096:3752167] bbb
递归锁
-
在递归代码使用
互斥锁
NSLock *myLock = [[NSLock alloc] init]; dispatch_async(dispatch_get_global_queue(0, 0), ^{ static void (^testMethod)(int); testMethod = ^(int value){ [myLock lock]; if (value > 0) { NSLog(@"current value = %d",value); testMethod(value - 1); } [myLock unlock]; }; testMethod(3); });
2020-04-25 22:51:22.371629+0800 003-NSLock分析[82133:3758245] current value = 4
-
↑ 这种情况,使用
互斥锁
,相当于单一线程 连续多次lock()
,堵塞了,除非下面代码里,其他线程帮忙unlock()
(只为了说明问题,在实际开发中是禁忌)sleep(1); NSLog(@"AAA"); [myLock unlock]; NSLog(@"BBB"); [myLock unlock]; NSLog(@"CCC"); [myLock unlock]; NSLog(@"DDD");
2020-04-25 22:56:47.114367+0800 003-NSLock分析[82174:3763253] current value = 3 2020-04-25 22:56:47.114347+0800 003-NSLock分析[82174:3763077] AAA 2020-04-25 22:56:47.114444+0800 003-NSLock分析[82174:3763077] BBB 2020-04-25 22:56:47.114450+0800 003-NSLock分析[82174:3763253] current value = 2 2020-04-25 22:56:47.114534+0800 003-NSLock分析[82174:3763077] CCC 2020-04-25 22:56:47.114612+0800 003-NSLock分析[82174:3763077] DDD 2020-04-25 22:56:47.114554+0800 003-NSLock分析[82174:3763253] current value = 1
使用
递归锁
则不需要依靠其他线程也可输出 value 3 2 1,改下创建myLock
的代码即可,此处不再贴代码和控制台打印。-
↓ 很好的测试用例,说明:
- 持有
递归锁
的线程,lock()
多少次,该线程都不会被堵塞 - 持有
递归锁
的线程,才能对该递归锁
进行有效地unlock()
- 线程1 已
lock()
,如果 线程2 也lock()
,则线程2 休眠,等待递归锁
被有效地unlock()
,unlock()
的次数要和当时lock()
的次数一样 - ↓ 如果手写多几次 第一个
dispatch_asyn
,程序正常;但如果对第一个dispatch_asyn
使用for或while循环包裹,则崩溃,原因不明(使用@synchronized
则正常)
NSRecursiveLock *myLock = [[NSRecursiveLock alloc] init]; dispatch_async(dispatch_get_global_queue(0, 0), ^{ static void (^testMethod)(int); testMethod = ^(int value){ [myLock lock]; if (value > 0) { sleep(1); NSLog(@"current value = %d",value); testMethod(value - 1); } [myLock unlock]; }; testMethod(3); }); dispatch_async(dispatch_get_global_queue(0, 0), ^{ sleep(2); NSLog(@"aaa"); [myLock unlock]; [myLock unlock]; [myLock unlock]; [myLock unlock]; [myLock lock]; [myLock lock]; NSLog(@"bbb"); NSLog(@"ccc"); });
2020-04-25 23:07:53.702057+0800 003-NSLock分析[82249:3772456] current value = 3 2020-04-25 23:07:53.702280+0800 003-NSLock分析[82249:3772456] aaa 2020-04-25 23:07:54.702480+0800 003-NSLock分析[82249:3772456] current value = 2 2020-04-25 23:07:55.706407+0800 003-NSLock分析[82249:3772456] current value = 1 2020-04-25 23:07:55.706807+0800 003-NSLock分析[82249:3772453] bbb 2020-04-25 23:07:55.707167+0800 003-NSLock分析[82249:3772453] ccc
- 持有
NSCondition
-
用法示例 ↓
NSCondition *myCondition = [[NSCondition alloc] init]; __block int count = 0; for (int i = 0; i<1500; i++) { dispatch_async(dispatch_get_global_queue(0, 0), ^{ [myCondition lock]; // 不能用 if while (count == 0) { // wait 前必须进入 lock 状态 // wait 进入休眠时,会自动 unlock() // wait 被唤醒后,会自动 lock(),在后面记得 unlock() [myCondition wait]; } assert(count>0); // 这里是线程安全的 count--; [myCondition unlock]; }); dispatch_async(dispatch_get_global_queue(0, 0), ^{ // lock/unlock 保证线程安全 [myCondition lock]; count++; [myCondition unlock]; // 唤醒1个 (有时会是多个 ╮(╯▽╰)╭ ) wait [myCondition signal]; // 如需唤醒所有的 wait,将 [myCondition signal] 改为 [myCondition broadcast] }); }
-
注意事项:因为
signal
有点傻,会唤醒多个wait
休眠状态 (signal
和wait
不是一对一的关系),所以在被唤醒时需要再次判断条件变量
,不能用if
直接执行后面的代码 (否则有可能assert
失败)。这是
pthread_cond_wait
和pthread_cond_signal()
的毛病,和lock()
/unlock()
,没有关系。也有一种说法是无故醒来:不
signal
也不broadcast
的情况下,wait
会自己醒来。(我反对,但暂时没有证据)
NSConditionLock
-
示例代码 ↓
NSConditionLock *myLock = [[NSConditionLock alloc] initWithCondition: 100]; __block int count = 3; // 生产,condition==0 时 被唤醒 // 若 count为3,修改 condition 为 100 (可随意自定,和消费对应即可); // 否则 condition 为 0,继续等待下次生产 dispatch_async(dispatch_get_global_queue(0, 0), ^{ while(1) { // sleep(1); [myLock lockWhenCondition:0]; count++; NSLog(@"+++++ now is %d", count); [myLock unlockWithCondition: count==3? 100: 0]; } }); // 消费,condition==100 时 被唤醒 // 若 count为0,修改 condition 为 0 (可随意自定,和生产对应即可); // 否则 condition 为 100,继续等待下次消费 dispatch_async(dispatch_get_global_queue(0, 0), ^{ while(1) { // sleep(1); [myLock lockWhenCondition:100]; count--; NSLog(@"----- now is %d", count); [myLock unlockWithCondition: count==0? 0: 100]; } });
2020-04-27 12:19:34.421670+0800 003-NSLock分析[86071:4358304] ----- now is 2 2020-04-27 12:19:34.421781+0800 003-NSLock分析[86071:4358304] ----- now is 1 2020-04-27 12:19:34.421875+0800 003-NSLock分析[86071:4358304] ----- now is 0 2020-04-27 12:19:34.421951+0800 003-NSLock分析[86071:4358303] +++++ now is 1 2020-04-27 12:19:34.422030+0800 003-NSLock分析[86071:4358303] +++++ now is 2 2020-04-27 12:19:34.422135+0800 003-NSLock分析[86071:4358303] +++++ now is 3 2020-04-27 12:19:34.422241+0800 003-NSLock分析[86071:4358304] ----- now is 2 2020-04-27 12:19:34.422323+0800 003-NSLock分析[86071:4358304] ----- now is 1 2020-04-27 12:19:34.422411+0800 003-NSLock分析[86071:4358304] ----- now is 0 2020-04-27 12:19:34.422619+0800 003-NSLock分析[86071:4358303] +++++ now is 1
-
NSConditionLock
是对NSCondition
的封装,内部有_cond: NSCondition
-
_value: Int
条件变量,上面注释常说的 condition -
_thread: _swift_CFThreadRef?
锁所在线程(正在执行的线程),有时为nil
unlockWithCondition:
的原理是:置空_thread
、_value
改为传入的新值、_cond
进行broadcast()
上面提过,
NSCondition
即使signal()
1次,也可能唤醒多个wait
。而NSConditionLock
干脆抛弃signal()
,使用broadcast()
,用_value
(也就是condition) 控制wait
。经 初步 测试,确实可靠。