美文网首页ios进阶ios面试汇总将来跳槽用
iOS底层原理总结 - 多线程的锁

iOS底层原理总结 - 多线程的锁

作者: 二斤寂寞 | 来源:发表于2019-05-22 15:13 被阅读56次

    目录:
    1.为什么要线程安全
    2.多线程安全隐患分析
    3.多线程安全隐患的解决方案
    4.锁的分类-13种锁
    4.1.1OSSpinLock
    4.1.2os_unfair_lock
    4.1.3pthread_mutex
    4.1.4NSLock
    4.1.5NSRecursiveLock
    4.1.6NSCondition
    4.1.7NSConditionLock
    4.1.8dispatch_semaphore
    4.1.9dispatch_queue
    4.1.10@synchronized
    4.1.11atomic
    4.1.12pthread_rwlock
    4.1.13dispatch_barrier_async
    5.性能对比
    6.常见的死锁


    1.为什么要线程安全

    多个线程访问同一块资源的时候,很容易引发数据混乱问题。

    2.多线程安全隐患分析

    01.png

    3.多线程安全隐患的解决方案

    02.png

    4.13种锁

    1 OSSpinLock

    何谓自旋锁?它是为实现保护共享资源而提出一种锁机制。其实,自旋锁与互斥锁比较类似,它们都是为了解决对某项资源的互斥使用。无论是互斥锁,还是自旋锁,在任何时刻,最多只能有一个保持者,也就说,在任何时刻最多只能有一个执行单元获得锁。但是两者在调度机制上略有不同。对于互斥锁,如果资源已经被占用,资源申请者只能进入睡眠状态。但是自旋锁不会引起调用者睡眠,如果自旋锁已经被别的执行单元保持,调用者就一直循环在那里看是否该自旋锁的保持者已经释放了锁,"自旋"一词就是因此而得名。

    OSSpinLock叫做”自旋锁”,使用时需要导入头文件#import <libkern/OSAtomic.h>
    使用方式:

    //初始化
    OSSpinLock lock = OS_SPINLOCK_INIT;
    //加锁
    OSSpinLockLock(&lock);
    //解锁
    OSSpinLockUnlock(&lock);
    

    demo:

    #import "OSSpinLockDemo.h"
    #import <libkern/OSAtomic.h>
    
    @interface OSSpinLockDemo()
    @property (assign, nonatomic) OSSpinLock ticketLock;
    @end
    
    @implementation OSSpinLockDemo
    
    - (instancetype)init {
        
        self = [super init];
        if (self) {
            self.ticketLock = OS_SPINLOCK_INIT;
        }
        return self;
    }
    
    
    //卖票
    - (void)sellingTickets {
        OSSpinLockLock(&_ticketLock);
        
        [super sellingTickets];
        
        OSSpinLockUnlock(&_ticketLock);
    }
    
    @end
    

    运行结果:

    image.png

    OSSpinLock在iOS10.0以后就被弃用了,可以使用os_unfair_lock_lock替代。而且还有一些安全性问题,具体参考不再安全的 OSSpinLock


    2 os_unfair_lock

    os_unfair_lock用于取代不安全的OSSpinLock,从iOS10开始才支持 从底层调用看,等待os_unfair_lock锁的线程会处于休眠状态,并非忙等 需要导入头文件#import <os/lock.h>

    使用方式:

    //初始化
    os_unfair_lock lock = OS_UNFAIR_LOCK_INIT;
    //加锁
    os_unfair_lock_lock(&lock);
    //解锁
    os_unfair_lock_unlock(&lock);
    

    demo:

    #import "os_unfair_lockDemo.h"
    #import <os/lock.h>
    @interface os_unfair_lockDemo()
    @property (assign, nonatomic) os_unfair_lock ticketLock;
    @end
    
    @implementation os_unfair_lockDemo
    - (instancetype)init
    {
    self = [super init];
    if (self) {
    self.ticketLock = OS_UNFAIR_LOCK_INIT;
    }
    return self;
    }
    
    //卖票
    - (void)sellingTickets{
    os_unfair_lock_lock(&_ticketLock);
    
    [super sellingTickets];
    
    os_unfair_lock_unlock(&_ticketLock);
    }
    @end
    

    3 pthread_mutex

    pthread除了创建互斥锁,还可以创建递归锁、读写锁、once等锁。

    • 静态初始化: pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER
    • 动态初始化: pthread_mutex_init()函数是以动态方式创建互斥锁的,参数 attr 指定了新建互斥锁的属性。如果参数 attr 为 NULL ,使用默认的属性,返回0代表初始化成功。这种方式可以初始化普通锁、递归锁(同 NSRecursiveLock ), 初始化方式有些复杂。

    此类初始化方法可设置锁的类型,PTHREAD_MUTEX_ERRORCHECK 互斥锁不会检测死锁, PTHREAD_MUTEX_ERRORCHECK 互斥锁可提供错误检查, PTHREAD_MUTEX_RECURSIVE 递归锁, PTHREAD_PROCESS_DEFAULT 映射到 PTHREAD_PROCESS_NORMAL

    使用方式:
    mutex叫做”互斥锁”,等待锁的线程会处于休眠状态。需要导入头文件#import <pthread.h>使用步骤

    • 1、初始化锁的属性
    pthread_mutexattr_t attr;
    pthread_mutexattr_init(&attr);
    pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
    
    /*
    * Mutex type attributes
    */
    #define PTHREAD_MUTEX_NORMAL        0
    #define PTHREAD_MUTEX_ERRORCHECK    1
    #define PTHREAD_MUTEX_RECURSIVE        2
    #define PTHREAD_MUTEX_DEFAULT        PTHREAD_MUTEX_NORMAL
    
    • 2、初始化锁
    // 初始化锁
    pthread_mutex_init(mutex, &attr);
    
    • 3、初始化锁结束以后,销毁属性
    // 销毁属性
    pthread_mutexattr_destroy(&attr);
    
    • 4、加锁解锁
    pthread_mutex_lock(&_mutex);
    pthread_mutex_unlock(&_mutex);
    
    • 5、销毁锁
    pthread_mutex_destroy(&_mutex);
    

    备注:我们可以不初始化属性,在传属性的时候直接传NULL,表示使用默认属性
    PTHREAD_MUTEX_NORMAL。pthread_mutex_init(mutex, NULL);

    死锁 我们稍微的修改一下代码

    //卖票
    - (void)sellingTickets{
    pthread_mutex_lock(&_ticketMutex);
    [super sellingTickets];
    [self sellingTickets2];
    pthread_mutex_unlock(&_ticketMutex);
    }
    
    - (void)sellingTickets2{
    pthread_mutex_lock(&_ticketMutex);
    NSLog(@"%s",__func__);
    pthread_mutex_unlock(&_ticketMutex);
    }
    

    上面的代码就会造成线程死锁,因为方法sellingTickets的结束需要sellingTickets2解锁,方法sellingTickets2的结束需要sellingTickets解锁,相互引用造成死锁
    但是pthread_mutex_t里面有一个属性可以解决这个问题PTHREAD_MUTEX_RECURSIVE
    PTHREAD_MUTEX_RECURSIVE递归锁:允许同一个线程对同一把锁进行重复加锁。要考重点同一个线程和同一把锁

    - (instancetype)init
    {
    self = [super init];
    if (self) {
    // 初始化属性
    pthread_mutexattr_t attr;
    pthread_mutexattr_init(&attr);
    pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
    // 初始化锁
    pthread_mutex_init(&(_ticketMutex), &attr);
    // 销毁属性
    pthread_mutexattr_destroy(&attr);
    }
    return self;
    }
    
    

    对于上面的问题还有一个解决方案就是在方法sellingTickets2中重新在创建一把新的锁,两个方法的锁对象不同,就不会造成线程死锁了。

    image.png
    • 条件
    // 初始化属性
    pthread_mutexattr_t attr;
    pthread_mutexattr_init(&attr);
    pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
    // 初始化锁
    pthread_mutex_init(&_mutex, &attr);
    // 销毁属性
    pthread_mutexattr_destroy(&attr);
    
    // 初始化条件
    pthread_cond_t condition
    pthread_cond_init(&_cond, NULL);
    
    // 等待条件
    pthread_cond_wait(&_cond, &_mutex);
    
    //激活一个等待该条件的线程
    pthread_cond_signal(&_cond);
    //激活所有等待该条件的线程
    pthread_cond_broadcast(&_cond);
    
    //销毁资源
    pthread_mutex_destroy(&_mutex);
    pthread_cond_destroy(&_cond);
    

    使用案例:假设我们有一个数组,里面有两个线程,一个是添加数组,一个是删除数组,我们先调用删除数组,在调用添加数组,但是在数组为空的时候不调用删除数组。

    demo:

    #import "pthread_mutexDemo1.h"
    #import <pthread.h>
    
    @interface pthread_mutexDemo1()
    @property (assign, nonatomic) pthread_mutex_t mutex;
    @property (assign, nonatomic) pthread_cond_t cond;
    @property (strong, nonatomic) NSMutableArray *data;
    @end
    
    @implementation pthread_mutexDemo1
    
    - (instancetype)init
    {
    if (self = [super init]) {
    // 初始化属性
    pthread_mutexattr_t attr;
    pthread_mutexattr_init(&attr);
    pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
    // 初始化锁
    pthread_mutex_init(&_mutex, &attr);
    // 销毁属性
    pthread_mutexattr_destroy(&attr);
    
    // 初始化条件
    pthread_cond_init(&_cond, NULL);
    
    self.data = [NSMutableArray array];
    }
    return self;
    }
    - (void)otherTest
    {
    [[[NSThread alloc] initWithTarget:self selector:@selector(__remove) object:nil] start];
    
    [[[NSThread alloc] initWithTarget:self selector:@selector(__add) object:nil] start];
    }
    
    // 线程1
    // 删除数组中的元素
    - (void)__remove
    {
    pthread_mutex_lock(&_mutex);
    NSLog(@"__remove - begin");
    
    if (self.data.count == 0) {
    // 等待
    pthread_cond_wait(&_cond, &_mutex);
    }
    
    [self.data removeLastObject];
    NSLog(@"删除了元素");
    
    pthread_mutex_unlock(&_mutex);
    }
    
    // 线程2
    // 往数组中添加元素
    - (void)__add
    {
    pthread_mutex_lock(&_mutex);
    
    sleep(1);
    
    [self.data addObject:@"Test"];
    NSLog(@"添加了元素");
    
    // 激活一个等待该条件的线程
    pthread_cond_signal(&_cond);
    
    pthread_mutex_unlock(&_mutex);
    }
    
    - (void)dealloc
    {
    pthread_mutex_destroy(&_mutex);
    pthread_cond_destroy(&_cond);
    }
    

    为了准确测试我们可以在__add中sleep(1) //删除操作再添加了元素之后才进行的。
    示例结果:

    image.png
    4 NSLock
    • NSLock是对mutex普通锁的封装。pthread_mutex_init(mutex, NULL);
    • NSLock 遵循 NSLocking 协议。Lock 方法是加锁,unlock 是解锁,tryLock 是尝试加锁,如果失败的话返回 NO,lockBeforeDate: 是在指定Date之前尝试加锁,如果在指定时间之前都不能加锁,则返回NO

    使用方式:

    @protocol NSLocking
    - (void)lock;
    - (void)unlock;
    @end
    
    @interface NSLock : NSObject <NSLocking> {
    @private
    void *_priv;
    }
    
    - (BOOL)tryLock;
    - (BOOL)lockBeforeDate:(NSDate *)limit;
    @property (nullable, copy) NSString *name
    @end
    

    demo:

    #import "LockDemo.h"
    @interface LockDemo()
    @property (strong, nonatomic) NSLock *ticketLock;
    @end
    @implementation LockDemo
    //卖票
    - (void)sellingTickets{
    [self.ticketLock lock];
    [super sellingTickets];
    [self.ticketLock unlock];
    }
    @end
    

    5 NSRecursiveLock(递归锁)

    NSRecursiveLock是对mutex递归锁的封装,API跟NSLock基本一致

    demo:

    #import "RecursiveLockDemo.h"
    @interface RecursiveLockDemo()
    @property (nonatomic,strong) NSRecursiveLock *ticketLock;
    @end
    @implementation RecursiveLockDemo
    //卖票
    - (void)sellingTickets{
    [self.ticketLock lock];
    [super sellingTickets];
    [self.ticketLock unlock];
    }
    @end
    

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

    • NSCondition是对mutex和cond的封装,更加面向对象,我们使用起来也更加的方便简洁

    • NSCondition同样实现了NSLocking协议,所以它和NSLock一样,也有NSLocking协议的lock和unlock方法,可以当做NSLock来使用解决线程同步问题,用法完全一样。

    • NSCondition提供更高级的用法。waitsignal,和条件信号量类似。比如我们要监听imageNames数组的个数,当imageNames的个数大于0的时候就执行清空操作。思路是这样的,当imageNames个数大于0时执行清空操作,否则,wait等待执行清空操作。当imageNames个数增加的时候发生signal信号,让等待的线程唤醒继续执行。

    • NSCondition和NSLock、@synchronized等是不同的是,NSCondition可以给每个线程分别加锁,加锁后不影响其他线程进入临界区。这是非常强大。但是正是因为这种分别加锁的方式,NSCondition使用wait并使用加锁后并不能真正的解决资源的竞争。比如我们有个需求:不能让m<0。假设当前m=0,线程A要判断到m>0为假,执行等待;线程B执行了m=1操作,并唤醒线程A执行m-1操作的同时线程C判断到m>0,因为他们在不同的线程锁里面,同样判断为真也执行了m-1,这个时候线程A和线程C都会执行m-1,但是m=1,结果就会造成m=-1.

    使用方式:

    @interface NSCondition : NSObject <NSLocking> {
    - (void)wait;
    - (BOOL)waitUntilDate:(NSDate *)limit;
    - (void)signal;
    - (void)broadcast;
    @property (nullable, copy) NSString *name 
    @end
    

    对于上面那个数组操作的案例我们就可以变成这个样子了
    demo:

    #import "NSConditionDemo.h"
    
    @interface NSConditionDemo ()
    
    @property (nonatomic,strong) NSCondition *condition;
    @property (strong, nonatomic) NSMutableArray *data;
    @end
    
    @implementation NSConditionDemo
    
    - (instancetype)init {
        if (self = [super init]) {
            
            self.data = [NSMutableArray array];
        }
        return self;
    }
    
    - (NSCondition *)condition {
        if (_condition == nil) {
            _condition = [[NSCondition alloc] init];
        }
        return _condition;
    }
    
    - (void)otherTest {
        [[[NSThread alloc] initWithTarget:self selector:@selector(__remove) object:nil] start];
        [[[NSThread alloc] initWithTarget:self selector:@selector(__add) object:nil] start];
    }
    
    // 线程1
    // 删除数组中的元素
    - (void)__remove {
        [self.condition lock];
        NSLog(@"__remove - begin");
        if (self.data.count == 0) {
            // 等待
            [self.condition wait];
        }
        [self.data removeLastObject];
        NSLog(@"删除了元素");
        [self.condition unlock];
    }
    
    // 线程2
    // 往数组中添加元素
    - (void)__add {
        [self.condition lock];
        sleep(1);
        [self.data addObject:@"Test"];
        NSLog(@"添加了元素");
        // 信号
        [self.condition signal];
        [self.condition unlock];
    }
    
    @end
    

    7 NSConditionLock
    • lock不分条件,如果锁没被申请,直接执行代码
    • unlock不会清空条件,之后满足条件的锁还会执行
    • unlockWithCondition:我的理解就是设置解锁条件(同一时刻只有一个条件,如果已经设置条件,相当于修改条件)
    • lockWhenCondition:满足特定条件,执行相应代码
    • NSConditionLock同样实现了NSLocking协议,试验过程中发现性能很低。
    • NSConditionLock也可以像NSCondition一样做多线程之间的任务等待调用,而且是线程安全的。
      NSConditionLock是对NSCondition的进一步封装,可以设置具体的条件值

    使用方式:

    @interface NSConditionLock : NSObject <NSLocking> {
     
    - (instancetype)initWithCondition:(NSInteger)condition;
    
    @property (readonly) NSInteger condition;
    - (void)lockWhenCondition:(NSInteger)condition;
    - (BOOL)tryLock;
    - (BOOL)tryLockWhenCondition:(NSInteger)condition;
    - (void)unlockWithCondition:(NSInteger)condition;
    - (BOOL)lockBeforeDate:(NSDate *)limit;
    - (BOOL)lockWhenCondition:(NSInteger)condition beforeDate:(NSDate *)limit;
    @property (nullable, copy) NSString *name;
    @end
    

    里面有三个常用的方法

    • 1、initWithCondition:初始化Condition,并且设置状态值
    • 2、lockWhenCondition:(NSInteger)condition:当状态值为condition的时候加锁
    • 3、unlockWithCondition:(NSInteger)condition当状态值为condition的时候解锁

    demo:

    @interface NSConditionLockDemo()
    @property (strong, nonatomic) NSConditionLock *conditionLock;
    @end
    @implementation NSConditionLockDemo
    - (instancetype)init
    {
    if (self = [super init]) {
    self.conditionLock = [[NSConditionLock alloc] initWithCondition:1];
    }
    return self;
    }
    
    - (void)otherTest
    {
    [[[NSThread alloc] initWithTarget:self selector:@selector(__one) object:nil] start];
    [[[NSThread alloc] initWithTarget:self selector:@selector(__two) object:nil] start];
    }
    
    - (void)__one
    {
    [self.conditionLock lock];
    NSLog(@"__one");
    sleep(1);
    [self.conditionLock unlockWithCondition:2];
    }
    
    - (void)__two
    {
    [self.conditionLock lockWhenCondition:2];
    NSLog(@"__two");
    [self.conditionLock unlockWithCondition:3];
    }
    @end
    

    示例结果:(即使test方法one和two顺序换了也还是先执行one)

    image.png
    8 dispatch_semaphore
    • semaphore叫做”信号量”
    • 信号量的初始值,可以用来控制线程并发访问的最大数量
    • 信号量的初始值为1,代表同时只允许1条线程访问资源,保证线程同步

    使用方式:

    //表示最多开启5个线程
    dispatch_semaphore_create(5);
    // 如果信号量的值 > 0,就让信号量的值减1,然后继续往下执行代码
    // 如果信号量的值 <= 0,就会休眠等待,直到信号量的值变成>0,就让信号量的值减1,然后继续往下执行代码
    dispatch_semaphore_wait(self.semaphore, DISPATCH_TIME_FOREVER);
    // 让信号量的值+1
    dispatch_semaphore_signal(self.semaphore);
    

    demo:

    @interface dispatch_semaphoreDemo()
    @property (strong, nonatomic) dispatch_semaphore_t semaphore;
    @end
    @implementation dispatch_semaphoreDemo
    - (instancetype)init
    {
    if (self = [super init]) {
    self.semaphore = dispatch_semaphore_create(1);
    }
    return self;
    }
    - (void)otherTest
    {
    for (int i = 0; i < 20; i++) {
    [[[NSThread alloc] initWithTarget:self selector:@selector(test) object:nil] start];
    }
    }
    - (void)test
    {
    // 如果信号量的值 > 0,就让信号量的值减1,然后继续往下执行代码
    // 如果信号量的值 <= 0,就会休眠等待,直到信号量的值变成>0,就让信号量的值减1,然后继续往下执行代码
    dispatch_semaphore_wait(self.semaphore, DISPATCH_TIME_FOREVER);
    
    sleep(2);
    NSLog(@"test - %@", [NSThread currentThread]);
    
    // 让信号量的值+1
    dispatch_semaphore_signal(self.semaphore);
    }
    @end
    

    示例结果:


    image.png

    我们在运行代码打印的时候发现,每隔一秒出现一次打印。虽然我们同时开启20个线程,但是一次只能访问一条线程的资源


    9 dispatch_queue

    使用GCD的串行队列也可以实现线程同步的

    使用方式:

    dispatch_queue_t queue = dispatch_queue_create("test", DISPATCH_QUEUE_SERIAL);
    dispatch_sync(queue, ^{
    // 追加任务1
    for (int i = 0; i < 2; ++i) {
    NSLog(@"1---%@",[NSThread currentThread]);
    }
    });
    
    dispatch_sync(queue, ^{
    // 追加任务2
    for (int i = 0; i < 2; ++i) {
    NSLog(@"2---%@",[NSThread currentThread]);
    }
    });
    

    10 @synchronized(互斥锁)

    在编程中,引入对象互斥锁的概念,来保证共享数据操作的完整性。每个对象都对应于一个可称为“互斥锁”的标记,这个标记用来保证在任一时刻,只能有一个线程访问对象。

    @synchronized 关于 @synchronized,这儿比你想知道的还要多

    @synchronized是对mutex递归锁的封装, @synchronized(obj)内部会生成obj对应的递归锁,然后进行加锁、解锁操作

    //卖票
    - (void)sellingTickets{
    @synchronized ([self class]) {
    [super sellingTickets];
    }
    }
    

    对是实现底层我们可以在objc4的objc-sync.mm文件中找到 synchronized就是在开始和结束的时候调用了objc_sync_enter&objc_sync_exit方法。
    objc_sync_enter实现

    int objc_sync_enter(id obj)
    {
    int result = OBJC_SYNC_SUCCESS;
    
    if (obj) {
    SyncData* data = id2data(obj, ACQUIRE);
    assert(data);
    data->mutex.lock();
    } else {
    // @synchronized(nil) does nothing
    if (DebugNilSync) {
    _objc_inform("NIL SYNC DEBUG: @synchronized(nil); set a breakpoint on objc_sync_nil to debug");
    }
    objc_sync_nil();
    }
    
    return result;
    }
    

    就是根据id2data方法找到一个data对象,然后在对data对象进行mutex.lock()加锁操作。我们点击进入id2data方法继续查找

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

    发现获取data对象的方法其实就是根据sDataLists[obj].data这个方法来实现的,也就是一个哈希表。


    11 atomic
    • atomic用于保证属性setter、getter的原子性操作,相当于在getter和setter内部加了线程同步的锁
    • 可以参考源码objc4的objc-accessors.mm
    • 它并不能保证使用属性的过程是线程安全的

    12 pthread_rwlock

    pthread_rwlock经常用于文件等数据的读写操作,需要导入头文件#import <pthread.h>
    iOS中的读写安全方案需要注意一下场景

    1、同一时间,只能有1个线程进行写的操作
    2、同一时间,允许有多个线程进行读的操作
    3、同一时间,不允许既有写的操作,又有读的操作

    使用方式:

    //初始化锁
    pthread_rwlock_t lock;
    pthread_rwlock_init(&_lock, NULL);
    
    //读加锁
    pthread_rwlock_rdlock(&_lock);
    //读尝试加锁
    pthread_rwlock_trywrlock(&_lock)
    
    //写加锁
    pthread_rwlock_wrlock(&_lock);
    //写尝试加锁
    pthread_rwlock_trywrlock(&_lock)
    
    //解锁
    pthread_rwlock_unlock(&_lock);
    //销毁
    pthread_rwlock_destroy(&_lock);
    

    demo:

    #import <pthread.h>
    @interface pthread_rwlockDemo ()
    @property (assign, nonatomic) pthread_rwlock_t lock;
    @end
    
    @implementation pthread_rwlockDemo
    
    - (instancetype)init
    {
    self = [super init];
    if (self) {
    // 初始化锁
    pthread_rwlock_init(&_lock, NULL);
    }
    return self;
    }
    
    - (void)otherTest{
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    
    for (int i = 0; i < 10; i++) {
    dispatch_async(queue, ^{
    [self read];
    });
    dispatch_async(queue, ^{
    [self write];
    });
    }
    }
    - (void)read {
    pthread_rwlock_rdlock(&_lock);
    sleep(1);
    NSLog(@"%s", __func__);
    pthread_rwlock_unlock(&_lock);
    }
    - (void)write
    {
    pthread_rwlock_wrlock(&_lock);
    sleep(1);
    NSLog(@"%s", __func__);
    pthread_rwlock_unlock(&_lock);
    }
    - (void)dealloc
    {
    pthread_rwlock_destroy(&_lock);
    }
    @end
    

    示例结果:

    image.png
    我们可以发现读操作1s有可能出现多次,但是写操作不会
    13 dispatch_barrier_async

    这个函数传入的并发队列必须是自己通过dispatch_queue_cretate创建的 如果传入的是一个串行或是一个全局的并发队列,那这个函数便等同于dispatch_async函数的效果

    //初始化
    self.queue = dispatch_queue_create("rw_queue", DISPATCH_QUEUE_CONCURRENT);
    //读操作
    dispatch_async(self.queue, ^{
    });
    //写操作
    dispatch_barrier_async(self.queue, ^{
     
    });
    

    5.性能对比

    基础表现-所操作耗时

    image.png
    上图是常规的锁操作性能测试(iOS7.0SDK,iPhone6模拟器,Yosemite 10.10.5),垂直方向表示耗时,单位是秒,总耗时越小越好,水平方向表示不同类型锁的锁操作,具体又分为两部分,左边的常规lock操作(比如NSLock)或者读read操作(比如ANReadWriteLock),右边则是写write操作,图上仅有ANReadWriteLockANRecursiveRWLock支持,其它不支持的则默认为0,图上看出,单从性能表现,原子操作是表现最佳的(0.057412秒),@synchronized则是最耗时的(1.753565秒) (测试代码) 。

    多线程锁删除数组性能测试

    • 模拟器环境:i5 2.6GH+8G 内存,xcode 7.2.1 (7C1002)+iPhone6SP(9.2)
    image.png
    • 真机环境:xcode 7.2.1 (7C1002)+iPhone6(国行)


      image.png

    通过测试发现模拟器和真机的区别还是很大的,模拟器上明显的阶梯感,真机就没有,模拟器上NSConditionLock的性能非常差,我没有把它的参数加在表格上,不然其他的就看不到了。不过真机上面性能还好。

    这些性能测试只是一个参考,没必要非要去在意这些,毕竟前端的编程一般线程要求没那么高,可以从其他的地方优化。线程安全中注意避坑,另外选择自己喜欢的方式,这样你可以研究的更深入,使用的更熟练。

    声明: 测试结果仅仅代表一个参考,因为各种因素的影响,并没有那么准确。
    综合比较


    image.png

    可以看到除了 OSSpinLock 外,dispatch_semaphore 和 pthread_mutex 性能是最高的。有消息称,苹果在新的系统中已经优化了 pthread_mutex 的性能,所有它看上去和 dispatch_semaphore 差距并没有那么大了。


    6.常见的死锁

    案例一:
    NSLog(@"1"); // 任务1
    dispatch_sync(dispatch_get_main_queue(), ^{
        NSLog(@"2"); // 任务2
    });
    NSLog(@"3"); // 任务3
    

    结果,控制台输出:

    1
    

    分析

    • dispatch_sync表示一个同步线程;
    • dispatch_get_main_queue表示运行在主线程中的主队列;
    • 任务2是同步线程的任务。

    首先执行任务1,这是肯定没问题的,只是接下来,程序遇到了同步线程,那么它会进入等待,等待任务2执行完,然后执行任务3。但这是队列,有任务来,当然会将任务加到队尾,然后遵循FIFO原则执行任务。那么,现在任务2就会被加到最后,任务3排在了任务2前面,问题来了:

    任务3要等任务2执行完才能执行,任务2由排在任务3后面,意味着任务2要在任务3执行完才能执行,所以他们进入了互相等待的局面。【既然这样,那干脆就卡在这里吧】这就是死锁。

    image.png
    案例二
    NSLog(@"1"); // 任务1
    dispatch_sync(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
        NSLog(@"2"); // 任务2
    });
    NSLog(@"3"); // 任务3
    

    结果,控制台输出:

    1
    2
    3
    

    分析
    首先执行任务1,接下来会遇到一个同步线程,程序会进入等待。等待任务2执行完成以后,才能继续执行任务3。从dispatch_get_global_queue可以看出,任务2被加入到了全局的并行队列中,当并行队列执行完任务2以后,返回到主队列,继续执行任务3。

    image.png
    案例三
    dispatch_queue_t queue = dispatch_queue_create("com.demo.serialQueue", DISPATCH_QUEUE_SERIAL);
    NSLog(@"1"); // 任务1
    dispatch_async(queue, ^{
        NSLog(@"2"); // 任务2
        dispatch_sync(queue, ^{  
            NSLog(@"3"); // 任务3
        });
        NSLog(@"4"); // 任务4
    });
    NSLog(@"5"); // 任务5
    

    结果,控制台输出:

    1
    5
    2
    // 5和2的顺序不一定复制代码
    

    分析
    这个案例没有使用系统提供的串行或并行队列,而是自己通过dispatch_queue_create函数创建了一个DISPATCH_QUEUE_SERIAL的串行队列。

    • 执行任务1;
    • 遇到异步线程,将【任务2、同步线程、任务4】加入串行队列中。因为是异步线程,所以在主线程中的任务5不必等待异步线程中的所有任务完成;
    • 因为任务5不必等待,所以2和5的输出顺序不能确定;
    • 任务2执行完以后,遇到同步线程,这时,将任务3加入串行队列;
    • 又因为任务4比任务3早加入串行队列,所以,任务3要等待任务4完成以后,才能执行。但是任务3所在的同步线程会阻塞,所以任务4必须等任务3执行完以后再执行。这就又陷入了无限的等待中,造成死锁。
    image.png
    案例四
    NSLog(@"1"); // 任务1
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        NSLog(@"2"); // 任务2
        dispatch_sync(dispatch_get_main_queue(), ^{
            NSLog(@"3"); // 任务3
        });
        NSLog(@"4"); // 任务4
    });
    NSLog(@"5"); // 任务5
    

    结果,控制台输出:

    1
    2
    5
    3
    4
    // 5和2的顺序不一定复制代码
    

    分析

    • 首先,将【任务1、异步线程、任务5】加入Main Queue中,异步线程中的任务是:【任务2、同步线程、任务4】。
    • 所以,先执行任务1,然后将异步线程中的任务加入到Global Queue中,因为异步线程,所以任务5不用等待,结果就是2和5的输出顺序不一定。
    • 然后再看异步线程中的任务执行顺序。任务2执行完以后,遇到同步线程。将同步线程中的任务加入到Main Queue中,这时加入的任务3在任务5的后面。
    • 当任务3执行完以后,没有了阻塞,程序继续执行任务4。
    • 从以上的分析来看,得到的几个结果:1最先执行;2和5顺序不一定;4一定在3后面。
    image.png
    案例五
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        NSLog(@"1"); // 任务1
        dispatch_sync(dispatch_get_main_queue(), ^{
            NSLog(@"2"); // 任务2
        });
        NSLog(@"3"); // 任务3
    });
    NSLog(@"4"); // 任务4
    while (1) {
    }
    NSLog(@"5"); // 任务5
    

    结果,控制台输出:

    1
    4
    // 1和4的顺序不一定复制代码
    

    分析

    • 和上面几个案例的分析类似,先来看看都有哪些任务加入了Main Queue:【异步线程、任务4、死循环、任务5】。
    • 在加入到Global Queue异步线程中的任务有:【任务1、同步线程、任务3】。
      第一个就是异步线程,任务4不用等待,所以结果任务1和任务4顺序不一定。
    • 任务4完成后,程序进入死循环,Main Queue阻塞。但是加入到Global Queue的异步线程不受影响,继续执行任务1后面的同步线程。
    • 同步线程中,将任务2加入到了主线程,并且,任务3等待任务2完成以后才能执行。这时的主线程,已经被死循环阻塞了。所以任务2无法执行,当然任务3也无法执行,在死循环后的任务5也不会执行。
    • 最终,只能得到1和4顺序不定的结果。
    image.png

    总结

      1. 总的来看,推荐pthread_mutex作为实际项目的首选方案;
      1. 对于耗时较大又易冲突的读操作,可以使用读写锁代替pthread_mutex;
      1. 如果确认仅有set/get的访问操作,可以选用原子操作属性;
      1. 对于性能要求苛刻,可以考虑使用OSSpinLock,需要确保加锁片段的耗时足够小;
      1. 条件锁基本上使用面向对象的NSCondition和NSConditionLock即可;
      1. @synchronized则适用于低频场景如初始化或者紧急修复使用;

    苹果为多线程、共享内存提供了多种同步解决方案(锁),对于这些方案的比较,大都讨论了锁的用法以及锁操作的开销。个人认为最优秀的选用还是看应用场景,高频接口VS低频接口、有限冲突VS激烈竞争、代码片段耗时的长短,都是选择的重要依据,选择适用于当前应用场景的方案才是王道。

    参考文章:
    iOS多线程安全-13种线程锁
    谈谈iOS多线程的锁

    相关文章

      网友评论

        本文标题:iOS底层原理总结 - 多线程的锁

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