21:iOS锁深究详解之一:互斥锁 mutex

  • 本文涉及:@synchronizedNSLockNSRecursiveLockNSConditionNSConditionLock,以及部分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_enterobjc_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_enterobjc_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休眠状态 (signalwait不是一对一的关系),所以在被唤醒时需要再次判断条件变量,不能用if直接执行后面的代码 (否则有可能assert失败)。

    这是pthread_cond_waitpthread_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。经 初步 测试,确实可靠。

你可能感兴趣的:(21:iOS锁深究详解之一:互斥锁 mutex)