iOS Developer

iOS开发中的锁

2017-12-07  本文已影响126人  赵梦楠

前言

在多线程开发中,常会遇到多个线程访问修改数据。为了防止数据不一致或数据污染,通常采用加锁机制来保证线程安全。


概述

锁是多线程开发中最基本的同步工具。开发中常用的锁通常分为以下几种类型:

以上大致介绍了锁的分类,下面将介绍Objective-C中各种实现锁的方式。

一、@synchronized指令

简介

@synchronized指令是Objective-C中易用性和可读性最好的创建互斥锁的方式。我们不用去直接创建锁和锁定对象,它会像其它互斥锁一样,防止其它线程获取同一个锁。传递给@synchronized的对象是区分保护块的唯一标识。它的简单用法是这样

- (void)myMethod:(id)anObj
{
    @synchronized(anObj)
    {
        //需要加锁的内容
    }
}

实现原理

编译器将@synchronized转化成了一对objc_sync_enter()objc_sync_exit()的调用,通过查看源码我们可以分析得出:

详细的分析可以参见这篇博客:正确使用多线程同步锁@synchronized()

使用注意

通过上面的分析, 我们可以得出@synchronized使用中应该注意的几个问题

@synchronized (obj) {
    NSLog(@"1st sync");
    @synchronized (obj) {
        NSLog(@"2nd sync");
    }
}
@synchronized (objectA) {
    [arrA addObject:obj];
}

@synchronized (objectB) {
    [arrB addObject:obj];
}

作为预防措施,@synchronized块隐式地向受保护的代码添加异常处理程序。如果引发异常,该处理程序会自动释放互斥锁。这意味着为了使用@synchronized指令,还必须在代码中启用Objective-C异常处理。如果您不想由隐式异常处理程序引起额外开销,则应考虑使用锁类。

二、pthread_mutex

简介

pthread 表示 POSIX thread,定义了一组跨平台的线程相关的 API,pthread_mutex 表示互斥锁。相关函数:

__API_AVAILABLE(macos(10.4), ios(2.0))
int pthread_mutex_destroy(pthread_mutex_t *);

__API_AVAILABLE(macos(10.4), ios(2.0))
int pthread_mutex_getprioceiling(const pthread_mutex_t * __restrict,
        int * __restrict);

__API_AVAILABLE(macos(10.4), ios(2.0))
int pthread_mutex_init(pthread_mutex_t * __restrict,
        const pthread_mutexattr_t * _Nullable __restrict);

__API_AVAILABLE(macos(10.4), ios(2.0))
int pthread_mutex_lock(pthread_mutex_t *);

__API_AVAILABLE(macos(10.4), ios(2.0))
int pthread_mutex_setprioceiling(pthread_mutex_t * __restrict, int,
        int * __restrict);

__API_AVAILABLE(macos(10.4), ios(2.0))
int pthread_mutex_trylock(pthread_mutex_t *);

__API_AVAILABLE(macos(10.4), ios(2.0))
int pthread_mutex_unlock(pthread_mutex_t *);

它的简单用法如下:

    pthread_mutexattr_t attr;
    pthread_mutexattr_init(&attr);
    pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_NORMAL);// 定义锁的属性

    pthread_mutex_t mutex;
    pthread_mutex_init(&mutex, &attr); // 创建锁

    pthread_mutex_lock(&mutex); // 申请锁
    
    //需要加锁的代码
    
    pthread_mutex_unlock(&mutex); // 释放锁

使用注意

三、pthread_rwlock

简介

pthread_rwlockpthread中定义的读写锁,相关函数如下:

__API_AVAILABLE(macos(10.4), ios(2.0))
int pthread_rwlock_destroy(pthread_rwlock_t * ) __DARWIN_ALIAS(pthread_rwlock_destroy);

__API_AVAILABLE(macos(10.4), ios(2.0))
int pthread_rwlock_init(pthread_rwlock_t * __restrict,
        const pthread_rwlockattr_t * _Nullable __restrict)
        __DARWIN_ALIAS(pthread_rwlock_init);

__API_AVAILABLE(macos(10.4), ios(2.0))
int pthread_rwlock_rdlock(pthread_rwlock_t *) __DARWIN_ALIAS(pthread_rwlock_rdlock);

__API_AVAILABLE(macos(10.4), ios(2.0))
int pthread_rwlock_tryrdlock(pthread_rwlock_t *) __DARWIN_ALIAS(pthread_rwlock_tryrdlock);

__API_AVAILABLE(macos(10.4), ios(2.0))
int pthread_rwlock_trywrlock(pthread_rwlock_t *) __DARWIN_ALIAS(pthread_rwlock_trywrlock);

__API_AVAILABLE(macos(10.4), ios(2.0))
int pthread_rwlock_wrlock(pthread_rwlock_t *) __DARWIN_ALIAS(pthread_rwlock_wrlock);

__API_AVAILABLE(macos(10.4), ios(2.0))
int pthread_rwlock_unlock(pthread_rwlock_t *) __DARWIN_ALIAS(pthread_rwlock_unlock);

__API_AVAILABLE(macos(10.4), ios(2.0))
int pthread_rwlockattr_destroy(pthread_rwlockattr_t *);

__API_AVAILABLE(macos(10.4), ios(2.0))
int pthread_rwlockattr_getpshared(const pthread_rwlockattr_t * __restrict,
        int * __restrict);

__API_AVAILABLE(macos(10.4), ios(2.0))
int pthread_rwlockattr_init(pthread_rwlockattr_t *);

__API_AVAILABLE(macos(10.4), ios(2.0))
int pthread_rwlockattr_setpshared(pthread_rwlockattr_t *, int);

使用注意

四、NSLock

简介

NSLock是典型的面向对象的锁,遵循Objective-CNSLocking协议接口,该协议定义了lockunlock。此外NSLock类还增加了tryLocklockBeforeDate:方法。tryLock方法尝试获取锁,如果锁不可用返回NOlockBeforeDate:尝试在指定时间内获取锁,如不成功返回NO

    NSLock *lock = [[NSLock alloc] init];
    //线程1
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        NSLog(@"线程1 准备加锁ing...");
        [lock lock];
        NSLog(@"线程1 锁定成功");
        sleep(5);//睡眠5秒
        NSLog(@"线程1 准备解锁");
        [lock unlock];
        NSLog(@"线程1 解锁成功");
    });
    
    //线程2
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        NSLog(@"线程2 尝试加锁ing...");
        BOOL x =  [lock lockBeforeDate:[NSDate dateWithTimeIntervalSinceNow:4]];
        if (x) {
            NSLog(@"线程2 锁定成功");
            [lock unlock];
            NSLog(@"线程2 解锁成功");
        }else{
            NSLog(@"线程2 加锁失败");
        }
    });

实现原理

使用注意

五、NSRecursiveLock

简介

NSRecursiveLock是面向对象的递归锁,同样遵循Objective-CNSLocking协议接口。该锁可被同一线程多次获取,而不会造成死锁。只是记录获取锁成功的次数,只有调用解锁的次数与锁定次数相同时,锁才会被真正释放,此时才能被其它线程获取。

NSRecursiveLock *rLock = [NSRecursiveLock new];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    static void (^RecursiveBlock)(int);
    RecursiveBlock = ^(int value) {
        [rLock lock];
        if (value > 0) {
            NSLog(@"线程%d", value);
            RecursiveBlock(value - 1);
        }
        [rLock unlock];
    };
    RecursiveBlock(4);
});

实现原理

使用注意

六、NSCondition

简介

NSCondition同样遵循NSLocking协议,NSCondition提供了单独的信号量管理接口。

- (void)wait;//阻塞当前线程直到条件锁发出信号为止,在调用此方法之前必须锁定接收器。
- (BOOL)waitUntilDate:(NSDate *)limit;//阻塞当前线程直到条件锁发出信号或达到指定的时间限制为止。
- (void)signal;//条件锁的信号,唤醒一个等待的线程,可多次调用唤醒多个线程,如没有被锁定的线程,则不起任何作用。为了避免竞争条件锁,应该仅在接收器锁定时调用此方法。
- (void)broadcast;//唤醒全部等待的线程。

实现原理

使用注意

//等待事件触发的线程  
[cocoaCondition lock];  
while (timeToDoWork <= 0)  
    [cocoaCondition wait];  
   
timeToDoWork--;  
   
// Do real work here.     
[cocoaCondition unlock];  

  
//出发事件的线程  
[cocoaCondition lock];  
timeToDoWork++;  
[cocoaCondition signal];  
[cocoaCondition unlock]; 
        NSLog(@"thread1:等待发送1");
        [cocoaCondition lock];
        [cocoaCondition wait];
        
        NSLog(@"thread1:发送1");
        [self.condition unlock];
        
    });
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
        sleep(1);
        
        NSLog(@"thread2:等待发送2");
        [cocoaCondition lock];
        [cocoaCondition wait];
        
        NSLog(@"thread2:发送2");
        [cocoaCondition unlock];
        
    });
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        sleep(2);
        [cocoaCondition lock];
        NSLog(@"thread3:收到数据");
        [cocoaCondition signal];
        [cocoaCondition unlock];
        
    });
    

    打印结果:
     thread1:等待发送1
     thread2:等待发送2
     thread3:收到数据
     thread1:发送1

七、NSConditionLock

简介

NSConditionLock遵循NSLocking协议,可以与用户自定义的条件相关联的互条件锁,是互斥锁的变种。提供了更加直观、方便的条件管理接口,可以更方便的实现生产者-消费者模式。

- (instancetype)initWithCondition:(NSInteger)condition;//初始化一个条件锁,并设置条件;
- (void)lockWhenCondition:(NSInteger)condition;//当条件满足时,获取锁。
- (BOOL)tryLock;//不考虑条件,直接尝试获取锁,成功返回YES,反之为NO。
- (BOOL)tryLockWhenCondition:(NSInteger)condition;//条件满足时尝试锁定,成功返回YES,反之为NO。
- (void)unlockWithCondition:(NSInteger)condition;//解锁并重新设置条件。
- (BOOL)lockBeforeDate:(NSDate *)limit;//在限制期限内获取锁,成功返回YES,反之为NO。
- (BOOL)lockWhenCondition:(NSInteger)condition beforeDate:(NSDate *)limit;//当条件满足时,在指定期限内获取锁,此方法会阻塞线程,直达获取锁(返回YES)或超时(返回NO)。

NSConditionLock可以很方便的实现线程间的依赖关系:

    id condLock = [[NSConditionLock alloc] initWithCondition:0];
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        [condLock lockWhenCondition:2];
        NSLog(@"线程1");
        [condLock unlockWithCondition:0];
        
    });
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        [condLock lockWhenCondition:1];
        NSLog(@"线程2");
        [condLock unlockWithCondition:2];
        
    });
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        [condLock lockWhenCondition:0];
        NSLog(@"线程3");
        [condLock unlockWithCondition:1];
        
    });
    
    打印结果:
    线程3
    线程2
    线程1

实现原理

NSConditionLock 借助 NSCondition 来实现 内部持有一个NSCondition对象,和一个_condition_value属性,调用lockWhenCondition:时,只有_condition_value条件值相等时,才能获得锁。

八、dispatch_semaphore

简介

dispatch_semaphore信号量,GCD中基于信号控制访问资源的线程数量。当限定的线程数量为一时,就起到了和同步锁相同的效果;信号量主要的函数如下:

dispatch_semaphore_create(long value);//用初始值创建新的计数信号量。注意:value值必须>=0,否则返回NULL。
dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout);//等待(递减)信号量。
dispatch_semaphore_signal(dispatch_semaphore_t dsema);//信号(增加)信号量。

信号量的简单用法如下:

    //value表示最多几个资源可访问
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(3);
    dispatch_queue_t quene = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    
    //线程1
    dispatch_async(quene, ^{
        dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
        NSLog(@"线程1:启动");
        sleep(1);
        NSLog(@"线程1:完成");
        dispatch_semaphore_signal(semaphore);
    });
    //线程2
    dispatch_async(quene, ^{
        dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
        NSLog(@"线程2:启动");
        sleep(1);
        NSLog(@"线程2:完成");
        dispatch_semaphore_signal(semaphore);
    });
    //线程3
    dispatch_async(quene, ^{
        dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
        NSLog(@"线程3:启动");
        sleep(1);
        NSLog(@"线程3:完成");
        dispatch_semaphore_signal(semaphore);
    });
    
    当value = 3时,打印结果:
    线程1:启动
    线程2:启动
    线程3:启动
    
    线程1:完成
    线程2:完成
    线程3:完成
    
    当value = 2时,打印结果:
    线程1:启动 
    线程2:启动
 
    线程1:完成
    线程2:完成
    线程3:启动
    
    线程3:完成
    
    当value = 1时,打印结果:
    线程1:启动
    
    线程1:完成
    线程2:启动
    
    线程2:完成
    线程3:启动
    
    线程3:完成

当信号计数大于0时,每条进来的线程使调用dispatch_semaphore_wait函数使计数减1;直到信号技术为0,阻塞其他线程,直到执行的线程调用dispatch_semaphore_signal函数使级数加1,信号技术大于0,允许阻塞的线程启动;可见,当信号计数控制为1时,可实现同步锁的作用。

实现原理

想要深入的理解dispatch_semaphore,可以参考这篇博客源码

使用注意

九、OSSpinLock

简介

OSSpinLock是iOS/MacOS自有的自旋锁,其特点是线程在等待取锁时,不会被挂起,而是保持空转,这避免了上下文切换等锁的操作。适用于临界区耗时短的操作,如果等待取锁的时间过长,轮训操作会消耗大量CPU资源。其主要函数和简单适用如下:

typedef int32_t OSSpinLock;

bool    OSSpinLockTry( volatile OSSpinLock *__lock );

void    OSSpinLockLock( volatile OSSpinLock *__lock );

void    OSSpinLockUnlock( volatile OSSpinLock *__lock );

__block OSSpinLock spinLock = OS_SPINLOCK_INIT;
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        OSSpinLockLock(& spinLock);
        NSLog(@"线程1");
        sleep(10);
        OSSpinLockUnlock(& spinLock);
        NSLog(@"线程1解锁成功");
    });
    
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        sleep(1);
        OSSpinLockLock(& spinLock);
        NSLog(@"线程2");
        OSSpinLockUnlock(& spinLock);
    });

因优先级反转的问题,在不同优先级的线程中OSSpinLock已不在安全,详细的介绍可参见这篇博客不再安全的 OSSpinLock,这里不在做过多介绍。

性能对比

这里贴出一张ibireme测试的结果图:


性能对比

总结

以上介绍的几种加锁方式,在原理、用法和性能等方面上各有不同。我们不能单从性能方面评论孰好孰坏。应该根据不同的需求和场景,选取合适的加锁方式。

参考资料

Synchronization

More than you want to know about @synchronized

pthread_mutex_lock.c 源码

深入理解 iOS 开发中的锁

深入理解GCD

pthread的各种同步机制

不再安全的 OSSpinLock

上一篇 下一篇

猜你喜欢

热点阅读