美文网首页
深入浅出iOS多线程(五)——多线程锁

深入浅出iOS多线程(五)——多线程锁

作者: struggle3g | 来源:发表于2019-07-07 02:20 被阅读0次

    深入浅出iOS多线程(一)——线程的概念
    深入浅出iOS多线程(二)——pthraed和NSThread的使用
    深入浅出iOS多线程(三)——GCD多线程
    深入浅出iOS多线程(四)——NSOperation多线程
    深入浅出iOS多线程(五)——多线程锁

    线程锁

    使用多线程能提高程序的执行效率,但也同时也给程序带来一些线程安全上面的问题,比如是数据竞争关系(data race).

    最常见的线程/进程同步的方法

    1. 临界资源
      • 各进程(线程)采取互斥的方式,实现共享的资源称作临界资源,也可以说是临界区所要访问的资源
    2. 临界区
      • 访问临界资源的一块代码
    3. 自旋锁
      • 用于多线程同步的一种锁,线程反复的去检测锁变量是否可用
      • 由于这一过程中一直执行,属于忙等待
      • 这种锁在阻塞时间短的场合下使用
    4. 互斥锁
      • 防止两条线程同时对同一资源进行读写的机制
      • 通常是将代码切成一个一个的临界区
      • 递归锁、非递归锁
    5. 读写锁
      • 读操作可以并发读取
      • 写操作是互斥的
      • 通常是使用互斥锁、条件变量、信号量实现
    6. 信号量锁
      • 更高级的一种锁,在semahpore去值为0/1的时候为互斥锁,
      • 通过取值的范围,来实现更加复杂的同步效果
    7. 条件锁:
      • 条件变量,当某些资源要求不满足的时候进入休眠,
      • 当资源要求满足的时候条件锁打开,

    iOS中的线程锁

    解释锁的时候,首先来实现一个简单数据竞态代码:

    dispatch_queue_t q = dispatch_get_global_queue(0, 0);
    __block int count = 0;
    for (int i = 0; i<10000; i++) {
        dispatch_async(q, ^{
            count ++;
            NSLog(@"%d",count);
        });
    }
    

    打印结果:

     9995
     9996
     9997
     9998
    

    最终结果应该是10000,最后的结果是9998,少+了2,说明线程之间有数据竞态的情况

    一、互斥锁

    1. NSLock<NSLocking>

    NSLock是Foundation框架中一种锁,代码如下:

    @protocol NSLocking
    
    - (void)lock;//加锁
    - (void)unlock;//解锁
    
    @end
    
    @interface NSLock : NSObject <NSLocking> {
    @private
        void *_priv;
    }
    
    //尝试加锁,不会阻塞线程。YES则加锁成功,NO则失败,说明其他线程在加锁中这个方法无论如何都会立即返回。
    - (BOOL)tryLock;
    
    //尝试在指定NSDate之前加锁,会阻塞线程。YES则加锁成功,NO则失败,说明其他线程在加锁中这个方法无论如何都会立即返回。在拿不到锁时不会一直在那等待。
    - (BOOL)lockBeforeDate:(NSDate *)limit;
    
    //name 是用来标识用的
    @property (nullable, copy) NSString *name API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
    
    @end
    

    一个简单的加锁实例代码;

    - (void)createLock{
        /*
         *  NSLock加锁互斥锁
         *
         **/
        __block int count = 0;
        //NSLock 初始化
        NSLock *lock = [[NSLock alloc]init];
        
        dispatch_queue_t q = dispatch_get_global_queue(0, 0);
      
        for (int i = 0; i<10000; i++) {
            dispatch_async(q, ^{
                
                //
                //加锁
                [lock lock];
                count ++ ;
                NSLog(@"%d",count);
                //解锁
                [lock unlock];
            });
        }
        //尝试加锁,返回YES加锁成功,返回NO获取锁失败,加锁失败
    //        [lock tryLock];
        //在nsdate之前加锁,加锁成功返回YES,加锁失败返回NO
    //        [lock lockBeforeDate:[[NSDate new]dateByAddingTimeInterval:10]];
        
    }
    
    

    打印代码:

     9995
     9996
     9997
     9998
     9999
     10000
    

    2. pthread_mutex

    pthread_mutex是跨平台,iOS系统自带多线程技术pthread的线程锁,它的简单使用方法如下:

    pthread_mutex_t pMutex;
    pthread_mutex_init(&pMutex, NULL);  //初始化pthread_mutex_t
    pthread_mutex_lock(&pMutex);        //加锁
    pthread_mutex_unlock(&pMutex);      //解锁
    

    代码例子:

    
        pthread_mutex_t pthreadmutex;
        
        /*
         #define PTHREAD_MUTEX_NORMAL           0     默认
         #define PTHREAD_MUTEX_ERRORCHECK       1     检错锁
         #define PTHREAD_MUTEX_RECURSIVE        2  递归锁
         #define PTHREAD_MUTEX_DEFAULT        PTHREAD_MUTEX_NORMAL  默认
         */
    
        pthread_mutex_init(&pthreadmutex, PTHREAD_MUTEX_NORMAL);
        dispatch_queue_t q = dispatch_get_global_queue(0, 0);
        __block int count = 0;
        for (int i = 0; i<10000; i++) {
            dispatch_async(q, ^{
                pthread_mutex_lock(&pthreadmutex);
                count ++;
                NSLog(@"%d",count);
                pthread_mutex_unlock(&pthreadmutex);
            });
        }
        pthread_mutex_destroy(&pthreadmutex);
    
    

    打印结果:

     9995
     9996
     9997
     9998
     9999
     10000
    

    3. @synchronized

    synchronized属于互斥锁中的另一个变种:递归锁(重入锁),上面有递归锁的概念介绍,synchronized在苹果已经开源了有关代码,可以这里查看synchronized还有

    __block int count = 0;
    dispatch_queue_t q = dispatch_get_global_queue(0, 0);
    
    for (int i = 0; i<10000; i++) {
        dispatch_async(q, ^{
            
            //递归锁
            @synchronized (self) {
                count ++ ;
                NSLog(@"%d",count);
            }
        });
    }
    

    打印内容:

     9995
     9996
     9997
     9998
     9999
     10000
    

    递归锁

    递归锁有一个特点,就是同一个线程可以加锁N次而不会引发死锁。重入锁

    首先来尝试一个重入死锁的代码:

        __block int count = 0;
        dispatch_queue_t q = dispatch_queue_create("struggle3g", DISPATCH_QUEUE_CONCURRENT);
    
        NSLock *lock1 = [[NSLock alloc]init];
        dispatch_async(q, ^{
            for (int j = 0; j<5; j++) {
                [lock1 lock];
                int num = count;
                sleep(1);
                num ++;
                count = num;
                NSLog(@"%d",count);
            }
        });
    

    上述内容就是造成死锁的条件

    1. NSRecursiveLock<NSLocking>
        NSRecursiveLock *lock = [[NSRecursiveLock alloc]init];
        __block int count = 0;
        dispatch_queue_t q = dispatch_queue_create("struggle3g", DISPATCH_QUEUE_CONCURRENT);
    
        dispatch_async(q, ^{
            for (int j = 0; j<5; j++) {
                [lock lock];
                int num = count;
                sleep(1);
                num ++;
                count = num;
                NSLog(@"%d",count);
            }
        });
        
        //[lock unlock]; //解锁
        //尝试加锁,返回YES加锁成功,返回NO获取锁失败,加锁失败
        //        [lock tryLock];
        //在nsdate之前加锁,加锁成功返回YES,加锁失败返回NO
        //        [lock lockBeforeDate:[[NSDate new]dateByAddingTimeInterval:10]];
        
    

    上述代码发现不会死锁。

    2. pthread_mutex(recursive)
    pthread_mutex锁也支持递归,只需要设置PTHREAD_MUTEX_RECURSIVE即可
    
    pthread_mutex_t lock;
    pthread_mutexattr_t attr;
    pthread_mutexattr_init(&attr);
    pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
    pthread_mutex_init(&lock, &attr);
    pthread_mutexattr_destroy(&attr);
    pthread_mutex_lock(&lock);
    pthread_mutex_unlock(&lock);
    
    

    pthread_mutex实现递归锁代码如下:

    
    - (void)doing{
        
    //    for (int j = 0; j<5; j++) {
    
            int num = _Mycount;
            sleep(1);
            num ++;
            _Mycount = num;
            NSLog(@"%d",_Mycount);
    
    //    }
        
    }
    
    _Mycount = 0;
    __block pthread_mutex_t lock;
        
    //初始化锁属性
    pthread_mutexattr_t attr;
    pthread_mutexattr_init(&attr);
    //设置该锁为递归锁
    pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
        
    //初始化锁
    pthread_mutex_init(&lock, &attr);
        
        
      
    dispatch_queue_t q = dispatch_queue_create("struggle3g", DISPATCH_QUEUE_CONCURRENT);
    
    dispatch_async(q, ^{
        
        for (int j = 0; j<5; j++) {
            pthread_mutex_lock(&lock);
            [self doing];
        }
        pthread_mutex_unlock(&lock);  //如果加了递归锁,不释放锁,同一个线程还是可以使用锁。
    
        NSLock *lock2 = [[NSLock alloc]init];
        dispatch_async(q, ^{
            for (int j = 0; j<5; j++) {
                [lock2 lock];
                [self doing];
                [lock2 unlock];
            }
        });
    });
        
    //销毁锁
    pthread_mutexattr_destroy(&attr);
    

    上述代码:我在一个循环之中一直加锁,但是并没有死锁。

    二 自旋锁

    何时使用自旋锁,何时使用互斥锁:

    • 当预计线程等待锁的时间很短,或者加锁的代码(临界区)经常被调用,但竞争情况很少发生,再或者CPU资源不紧张,拥有多核处理器的时候使用自旋锁比较合适。
    • 而当预计线程等待锁的时间较长,CPU是单核处理器,或者临界区有IO操作,或者临界区代码复杂或者循环量大,临界区竞争非常激烈的时候使用互斥锁比较合适

    OSSpinLock自旋锁,因为不在安全,优先级反转。在iOS10以后被弃用,因为有可能会因为优先级反转导致线程不在安全。 苹果给出的替代方案是os\_unfair\_lock

    os\_unfair\_lock猜测:

    • 在优先级反转的时候,应该是做了一些相应的处理,比如说直接释放低优先级的锁,在重新加入到线程当中。
    • 或者直接使用互斥锁代替,也可能是优化后的互斥锁

    os_unfair_lock

    __block os_unfair_lock _osunfairLock;
    // 初始化os_unfair_lock
    _osunfairLock = OS_UNFAIR_LOCK_INIT;
        
    dispatch_queue_t q = dispatch_queue_create("struggle3g", DISPATCH_QUEUE_CONCURRENT);
    __block int count = 0;
    for (int i = 0; i<10000; i++) {
        dispatch_async(q, ^{
            // 加锁
            os_unfair_lock_lock(&(_osunfairLock));
            count ++;
            NSLog(@"%d",count);
            // 解锁
            os_unfair_lock_unlock(&(_osunfairLock));
        });
    }
       
    /*
     *如果锁当前由调用线程拥有,这个函数返回。
     *
     *如果锁被其他线程解锁或拥有,这个函数断言和终止进程。
     **/
    //    void os_unfair_lock_assert_owner
    /*
     *如果锁被其他线程解锁或拥有,这个函数
     *返回。
     *
     *如果锁当前由当前线程拥有,则此函数断言
     并终止进程。
     **/
    //    void os_unfair_lock_assert_not_owner
        
    /*
     尝试获取锁如果其他线程持有锁,返回NO,如果获取到锁,返回YES
     */
    //    bool os_unfair_lock_trylock
    

    三、读写锁

    是计算机程序的并发控制的一种同步机制,也称“共享-互斥锁”、多读者-单写者锁) 用于解决多线程对公共资源读写问题。读操作可并发重入,写操作是互斥的。 读写锁通常用互斥锁、条件变量、信号量实现。

    pthread的API:

    pthread_rwlock
    //加读锁
    pthread_rwlock_rdlock(&rwlock);
    //解锁
    pthread_rwlock_unlock(&rwlock);
    //加写锁
    pthread_rwlock_wrlock(&rwlock);
    //解锁
    pthread_rwlock_unlock(&rwlock);
    
    

    四、条件锁

    1. NSCondition<NSLocking>

    //条件锁NSCondition的API

    wait  //等待
    waitUntilDate:(NSDate *)limit //
    signal
    broadcast
    

    遵循NSLocking协议,使用的时候同样是lock,unlock加解锁,wait是傻等,waitUntilDate:方法是等一会,都会阻塞掉线程,signal是唤起一个在等待的线程,broadcast是广播全部唤起,使用的时候感觉跟信号量类似。

    dispatch_queue_t q = dispatch_queue_create("struggle3g", DISPATCH_QUEUE_CONCURRENT);
        
    NSCondition *lock = [[NSCondition alloc] init];
    //第一个线程
    __block BOOL finished = NO;
    dispatch_async(q, ^{
        [lock lock];
        while (!finished) {
            [lock wait];
            NSLog(@"第一个线程得到第二个线程的通知");
        }
        [lock unlock];
        NSLog(@"第一个线程使用完毕");
    });
        
    //第二个线程
    dispatch_async(q, ^{
        [lock lock];
        sleep(2);
        finished = YES;
        NSLog(@"我做了一些事情,告诉第一个线程");
        [lock signal];
        [lock unlock];
        NSLog(@"第二个线程使用完毕");
    });
    

    2. NSConditionLock<NSLocking>

    条件锁的API

    @property (readonly) NSInteger condition;
    //初始化时必须放入条件condition
    - (instancetype)initWithCondition:(NSInteger)condition;
    //加锁时添加条件
    - (void)lockWhenCondition:(NSInteger)condition;
    //尝试加锁
    - (BOOL)tryLock;
    //尝试加锁的条件
    - (BOOL)tryLockWhenCondition:(NSInteger)condition;
    //解锁附加条件
    - (void)unlockWithCondition:(NSInteger)condition;
    //limit时间之前加锁
    - (BOOL)lockBeforeDate:(NSDate *)limit;
    //limit时间之前加锁附加条件
    - (BOOL)lockWhenCondition:(NSInteger)condition beforeDate:(NSDate *)limit;
    
    

    这个条件锁,跟NSLock使用类似,是不过API附加了condition

    五、信号量

    1. dispatch_semaphore_t

    • 创建信号量dispatch_semaphore_create(value),value代表信号量的值
    • dispatch_semaphore_wait(<dispatch_semaphore_t>,DISPATCH_TIME_FOREVER),
    • dispatch_semaphore_signal(<dispatch_semaphore_t>)
      1. 上面两个方法可以理解为一个完整的信号量,dispatch_semaphore_signal +1 信号量的值,而dispatch_semaphore_wait -1 信号量的值
      2. 信号量的值如果等于0dispatch_semaphore_wait就会阻塞该方法以下的内容,当调用dispatch_semaphore_signal,信号量的值 +1, dispatch_semaphore_wait就会收到信号,信号量的值大于0就继续向下执行,直到信号量的值为0位置。
      • 从上述的两个解释可以得出结论:
        • dispatch_semaphore_create的信号量的值必须大于等于0,信号量的值为0时,dispatch_semaphore_wait 阻塞,必须调用dispatch_semaphore_signal信号量值+1,不能再次调用dispatch_semaphore_wait让信号量小于0

    所以通过信号量的值设置为0,在异步方法之后添加dispatch_semaphore_wait,信号量的值设置为0阻塞,这时异步方法执行完成执行dispatch_semaphore_signal,会执行dispatch_semaphore_wait信号量-1,达到同步线程的目的。

    验证代码如下:

    __block int count = 0;
    
    dispatch_semaphore_t sema = dispatch_semaphore_create(0);
    dispatch_queue_t q = dispatch_queue_create("struggle3g", DISPATCH_QUEUE_CONCURRENT);
        
    dispatch_async(q, ^{
        count ++;
        NSLog(@"%d",count);
        dispatch_semaphore_signal(sema);
    });
    NSLog(@"第一次wait");
    dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
        
       
    dispatch_async(q, ^{
        count ++;
        NSLog(@"%d",count);
        dispatch_semaphore_signal(sema);
    });
    NSLog(@"第二次wait");
    dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
        
    dispatch_async(q, ^{
        count ++;
        NSLog(@"%d",count);
        dispatch_semaphore_signal(sema);
    });
    NSLog(@"第三次wait");
    dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
        
        
    dispatch_async(q, ^{
        count ++;
        NSLog(@"%d",count);
        dispatch_semaphore_signal(sema);
    });
    NSLog(@"第四次wait");
    dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
    

    打印内容:

     第一次wait
     1
     第二次wait
     2
     第三次wait
     3
     第四次wait
     4
    

    线程锁遇到的问题

    1. 死锁

    所谓死锁,通常指有两个线程A和B都卡住了,并等待对方完成某些操作。A不能完成是因为它在等待B完成,B不能完成,因为它在等待A完成,于是就是死锁了。

    2. 产生死锁的四个必要条件

    四个必要条件

    • 互斥条件:一个锁每次只能被一个线程获取
    • 请求与保持条件:一个线程因请求获取锁而阻塞时,对已获得的锁保持
    • 不可剥夺条件:线程已获得的锁,在未使用完之前,不能强行释放
    • 环路等待条件:必然存在一个获取锁的环形链,即A等待获取b的锁,B等待获取C的锁,而C等待获取A的锁。这就形成了一个环

    3. 死锁的方式

    一般死锁

    • 如何造成这种死锁
      • 线程A1,A2都需要同时获取锁B1、B2锁才能正常地完成功能
      • 但是由于线程A1先持有了B1锁,而线程A2先获取了B2的锁
      • 线程A1等待A2释放B2的锁才能完成任务解锁,而线程A2等待A1释放A1的锁才能完成任务解锁
      • 这就造成了死锁
    • 解决方法:
        1. 等其中一条线程完全执行完之后再执行另外一条线程。
        1. 设置优先级,如果运行多条线程出现死锁,优先级低的回退,优先级高的先执行这样即可解决死锁问题。

    递归死锁(重入死锁)

    • 如何造成这种死锁

      • 一个线程持有一个对象的锁,在没有释放这个锁之前又获取了一次锁,这也就造成了线程的死锁
    • 解决方法:

        1. 在第二次回去锁的时候先讲第一次获取的锁匙放
        1. 使用递归锁,进行加锁

    总结

    性能总结

    参考 不在安全的OSSpinLock

    OSSpinLock(弃用)                     0.097348s
    dispatch_semaphore                  0.155043s
    os_unfair_lock                      0.171789s
    pthread_mutex                       0.262592s
    NSLock                              0.283196s
    pthread_mutex(recursive)            0.372398s
    NSRecursiveLock                     0.473536s
    NSConditionLock                     0.950285s
    @synchronized                       1.101924s
    

    相关文章

      网友评论

          本文标题:深入浅出iOS多线程(五)——多线程锁

          本文链接:https://www.haomeiwen.com/subject/aupvhctx.html