美文网首页春天来咯iOS
iOS线程锁及其性能

iOS线程锁及其性能

作者: HuyaRC | 来源:发表于2019-03-28 17:10 被阅读81次

    分享是每个优秀的程序员所必备的品质


    注意:性能是在多线程环境中测试的结果!!!

    内容提要:

    • 基本概念
    • OSSpinLock (自旋锁)
    • os_unfair_lock (自旋锁)
    • dispatch_semaphore (信号量)
    • pthread_mutex (互斥锁)
    • NSLock (互斥锁、对象锁)
    • NSCondition (条件锁、对象锁)
    • NSConditionLock (条件锁、对象锁)
    • NSRecursiveLock (递归锁、对象锁)
    • pthread_mutex(recursive) (递归锁)
    • @synchronized (条件锁)
    基本概念

    锁:为了保证共享数据操作的完整性和安全性而提出一种机制。

    为什么要使用:
    以在一个并发队列中,添加异步任务,任务中对用nonatomic的字符串属性赋值为例:
    代码:

    // @property (nonatomic,copy)NSString *target;
    - (void)lockTest{
        dispatch_queue_t queue = dispatch_queue_create(NULL, DISPATCH_QUEUE_CONCURRENT);
        for(NSInteger i=0;i<1000;i++){
            dispatch_async(queue, ^{
                self.target = [NSString stringWithFormat:@"target--%ld",I];
            });
        }
    }
    

    运行结果:


    坏内存访问.png

    运行结果崩溃:向一个已经释放的对象发送消息。
    为什么会这样呢?很简单,这是一个赋值过程调用了target的set方法,内部实现:

    - (void)setTarget:(NSString *)target{
        if(_target != target){
            [_target release];
            [target retain];
            _target = target;
        }
    }
    

    因为是异步执行的,有些线程执行到[_target release]的时候,对象已经释放了;此时另外某些线程也执行[_target release]的时候,就会向一个已经释放的对象发送消息造成崩溃。

    针对以上问题,提出了的机制。在某一线程准备执行赋值操作时,加锁,赋值结束再解锁。其他线程无法访问已经加锁的资源。

    iOS中提供了以下10种锁:

    一、OSSpinLock

    自旋锁:和互斥锁类似为了保证线程安全,两者在调度机制上略有不同,对于互斥锁,当一个线程获得这个锁之后,其他想要获得此锁的线程将会被阻塞,直到该锁被释放。但自选锁不一样,当一个线程获得锁之后,其他线程将会一直循环在哪里查看是否该锁被释放。 OSSpinLock原理很简单,就是一直 do while 忙等。它的缺点是当等待时会消耗大量 CPU 资源,所以它不适用于较长时间的任务所以,此锁比较适用于锁的持有者保存时间较短的情况下。正是由于自旋锁使用者一般保持锁时间非常短,因此选择自旋而不是睡眠是非常必要的,自旋锁的效率远高于互斥锁。

    之前的YY大神ibireme也提到了关于OSSpinLock会发生优先级反转而不安全:不再安全的 OSSpinLock

    优先级反转(Priority Inversion): 简单来说由于多进程共享资源,具有最高优先权的进程被低优先级进程阻塞,反而使具有中优先级的进程先于高优先级的进程执行,导致系统的崩溃。

    例如有任务A、B、C对应高、中、低三个优先级。C占有一个资源X,A也想要访问X,此时又有B要执行,B优先级高于C,于是占有资源的任务会被挂起,但是资源仍被占有中,资源得不到释放,导致A一直无法执行,但是优先级比A低的B却可以执行,这就是优先级反转。

     // 初始化  OS_SPINLOCK_INIT 默认不加锁为0,加锁不为0
    OSSpinLock spinLock = OS_SPINLOCK_INIT
    // 加锁
    OSSpinLockLock(&spinLock)
    // 解锁
    OSSpinLockUnlock(&spinLock)
    // 尝试加锁
    OSSpinLockTry(&spinLock) 
    

    代码:需要导入头文件#import <libkern/OSAtomic.h>

    double begin;
        __block double end;
        NSInteger count = 1000;
        
        dispatch_queue_t queue = dispatch_queue_create(NULL, DISPATCH_QUEUE_CONCURRENT);
        dispatch_group_t group = dispatch_group_create();
        
        {
            begin = CACurrentMediaTime();
            __block OSSpinLock lock = OS_SPINLOCK_INIT;
            for(NSInteger i = 0;i < count; i++){
                dispatch_group_async(group, queue, ^{
                    OSSpinLockLock(&lock);
                    self.target = [NSString stringWithFormat:@"target--%ld",i];
                    OSSpinLockUnlock(&lock);
                });
                
            }
            dispatch_group_notify(group, queue, ^{
                end = CACurrentMediaTime();
                NSLog(@"OSSpinLock:           %8.2f ms",(end - begin) * 1000);
            });
        }
    

    还要一种使用场景:OSSpinLockTry(&lock) :即尝试加锁,可以加锁则立即加锁并返回 YES,反之返回 NO,trylock即使加锁失败,也可以继续执行其他任务。正常情况下我们追求的就是必须要加锁成功才会考虑之后的任务,就没必要轮询 使用 trylock

    注:苹果已经在iOS10.0以后废弃了这种锁机制,使用os_unfair_lock 替换

    二、os_unfair_lock

    自旋锁,苹果在iOS10.0以后用来替代OSSpinLock,需要导入头文件#import <os/lock.h>
    具体使用:

    // 初始化
    os_unfair_lock unfair_lock = OS_UNFAIR_LOCK_INIT
    // 加锁
    os_unfair_lock_lock(&unfair_lock)
     // 解锁
    os_unfair_lock_unlock(&unfair_lock) 
     // 尝试加锁
    os_unfair_lock_trylock(&unfair_lock)
    

    性能测试代码同OSSpinLock,替换对应锁代码即可。

    三、dispatch_semaphore

    GCD里面的信号量,使用的还是比较多的,实际上是通过限制线程并发的条数来控制线程安全的。

    // 初始化   long value :线程并发执行的数量
    dispatch_semaphore_t semaphore_t = dispatch_semaphore_create(1);
    // 加锁
    dispatch_semaphore_wait(semaphore_t,DISPATCH_TIME_FOREVER);
    // 解锁
    dispatch_semaphore_signal(semaphore_t);
    

    性能测试代码同上,替换对应锁代码即可。

    四、pthread_mutex(互斥锁)

    互斥锁:当一个线程获得这个锁之后,其他想要获得此锁的线程将会被阻塞,直到该锁被释放。

    使用需要导入头文件 #import <pthread/pthread.h>

    //  初始化 ,提供两种方式
    
    // 第一种
    pthread_mutex_t mutex_t;
    pthread_mutex_init(&mutex_t, NULL); 
    // 第二种,宏初始化
    pthread_mutex_t mutex =PTHREAD_MUTEX_INITIALIZER;
    
    // 加锁
    pthread_mutex_lock(&mutex_t);
    // 解锁
    pthread_mutex_unlock(&mutex_t);
    // 尝试加锁,这里和上面不同的是:当可以加锁返回的是 0,否则返回一个错误
    pthread_mutex_trylock(& mutex_t)
    

    性能测试代码同上,替换对应锁代码即可。

    五、NSLock

    互斥锁,使用也比较多

    // 初始化
    NSLock *lock = [[NSLock alloc]init];
    // 加锁
    [lock lock];
    // 解锁
    [lock unlock];
    // 尝试加锁,可以加锁则立即加锁并返回 YES,反之返回 NO
    [lock tryLock];
    

    性能测试代码同上。

    六、NSCondition

    是互斥锁和条件锁的结合体
    NSCondition 的对象实际上充当线程中的锁和检查器,锁主要为了当检测条件时保护数据源;线程检查器主要是根据条件决定是否继续运行线程,即线程是否被阻塞。执行条件引发的任务当条件不成立时,线程会阻塞。在另一个线程向条件对象发出信号之前,它一直保持阻塞状态。

    // // 初始化
    NSCondition *condition= [[NSCondition alloc]init];
    // 加锁
    [condition lock];
    // 解锁
    [condition unlock];
    /*
    其他功能
    wait 进入等待状态
    waitUntilDate:让一个线程等待一定的时间
    signal 唤醒一个等待的线程
    broadcast 唤醒所有等待的线程
    

    性能测试代码同上。

    七、NSConditionLock

    // 初始化
    NSConditionLock *_conditionLock = [[NSConditionLock alloc]init];
    // 加锁
    [_conditionLock lock];
    // 解锁
    [_conditionLock unlock];
    // 尝试加锁,可以加锁则立即加锁并返回 YES,反之返回 NO
    [_conditionLock tryLock];
    /*
    其他功能接口
    - (instancetype)initWithCondition:(NSInteger)condition NS_DESIGNATED_INITIALIZER; //初始化传入条件
    - (void)lockWhenCondition:(NSInteger)condition;//条件成立触发锁
    - (BOOL)tryLockWhenCondition:(NSInteger)condition;//尝试条件成立触发锁
    - (void)unlockWithCondition:(NSInteger)condition;//条件成立解锁
    - (BOOL)lockBeforeDate:(NSDate *)limit;//触发锁 在等待时间之内
    - (BOOL)lockWhenCondition:(NSInteger)condition beforeDate:(NSDate *)limit;//触发锁 条件成立 并且在等待时间之内
    */
    

    性能测试代码同上。

    八、NSRecursiveLock

    递归锁
    可以被同一线程多次请求,而不会引起死锁。严格上来说只是互斥锁的一个特例,同样只能有一个线程访问该对象,但允许同一个线程在未释放其拥有的锁时反复对该锁进行加锁操作。

    // 初始化
    NSRecursiveLock *_recursiveLock = [[NSRecursiveLock alloc]init];
    // 加锁
    [_recursiveLock lock];
    // 解锁
    [_recursiveLock unlock];
    // 尝试加锁,可以加锁则立即加锁并返回 YES,反之返回 NO
    [_recursiveLock tryLock];
    /*
    注: 递归锁可以被同一线程多次请求,而不会引起死锁。
    即在同一线程中在未解锁之前还可以上锁, 执行锁中的代码。
    这主要是用在循环或递归操作中。
    - (BOOL)lockBeforeDate:(NSDate *)limit;//触发锁 在等待时间之内
    */
    

    性能测试代码同上。

    九、pthread_mutex(recursive)

    // 初始化
    pthread_mutex_t mutex_t;
    pthread_mutexattr_t attr;
    pthread_mutexattr_init(&attr); //初始化attr并且给它赋予默认pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE); //设置锁类型,这边是设置为递归锁
    pthread_mutex_init(&mutex_t, &attr);
    pthread_mutexattr_destroy(&attr); //销毁一个属性对象,在重新进行初始化之前该结构不能重新使用
    // 加锁
    pthread_mutex_lock(&mutex_t);
    // 解锁
    pthread_mutex_unlock(&mutex_t);
    /*
    注: 递归锁可以被同一线程多次请求,而不会引起死锁。
    即在同一线程中在未解锁之前还可以上锁, 执行锁中的代码。
    这主要是用在循环或递归操作中。
    */
    

    性能测试代码同上。

    十、@synchronized

    相比于使用 其他方式例如NSLock 创建锁对象、加锁和解锁来说,@synchronized 用着更方便,可读性更高。

    {
            begin = CACurrentMediaTime();
            NSObject *lock = [NSObject new];
            for(NSInteger i = 0;i < count; i++){
                dispatch_group_async(group, queue, ^{
                    @synchronized (lock) {
                        self.target = [NSString stringWithFormat:@"target--%ld",i];
                    }
                });
            }
            dispatch_group_notify(group, queue, ^{
                end = CACurrentMediaTime();
                NSLog(@"@synchronized:           %8.2f ms",(end - begin) * 1000);
            });
    }
    
    

    相关文章

      网友评论

        本文标题:iOS线程锁及其性能

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