线程锁

线程锁

是为了解决多个线程之间共享同一资源时,对资源的占用控制,防止多个线程之间同时修改同一资源信息,导致不可预知的问题。

锁的实现方式大致可以分为以下两种:

  • 阻塞
  • 忙等

阻塞:如果锁对象被其他线程所持有,那么请求访问的线程就会被加入到等待队列中,因而被阻塞。这就意味着被阻塞的线程放弃了时间片,调度器会将CPU让给下一个执行的的线程。当锁可用的时候,调度器会得到通知,然后根据情况将线程从等待队列取出来,并重新调度。

忙等:线程不放弃CPU时间片,而是继续重复的尝试访问锁,直到锁可用。
 
iOS开发中包括以下锁:
 
OSSpinLock(自旋锁)
os_unfair_lock(互斥锁,iOS10之后,用于替代OSSpinLock的锁)
dispatch_semaphore_t(信号量)
pthread_mutex(互斥锁)
NSLock(互斥锁,对pthread_mutex互斥锁的OC封装)
pthread_mutex(递归锁)
NSRecursiveLock(递归锁,对pthread_mutex递归锁的OC封装)
NSCondition(条件锁)
NSConditionLock(条件锁,在NSCondition基础上实现的)
@synchronized(递归锁):其本质是调用objc_sync_enter和objc_sync_exit
Atomic(原子操作)
pthread_rwlock_t(读写锁)
GCD栅栏(可以用于读写锁)
 
 

OSSpinLock(自旋锁)

自旋锁与互斥锁有点类似,只是自旋锁不会引起调用者睡眠,如果自旋锁已经被别的执行单元保持,调用者就一直循环在那里看是否该自旋锁的保持者已经释放了锁,"自旋"一词就是因此而得名。 由于自旋锁使用者一般保持锁时间非常短,因此选择自旋而不是睡眠是非常必要的,自旋锁的效率远高于互斥锁。适用于处理耗时较短的任务。
新版 iOS 中,系统维护了 5 个不同的线程优先级/QoS: backgroundutilitydefaultuser-initiateduser-interactive。高优先级线程始终会在低优先级线程前执行,一个线程不会受到比它更低优先级线程的干扰。这种线程调度算法会产生潜在的优先级反转问题,从而破坏了 spin lock
因此苹果在iOS10的时候废弃了OSSpinLock,采用os_unfair_lock代替。
#import <libkern/OSAtomic.h>
@interface DrLock : NSObject {
    
    OSSpinLock _lock;
}

- (void)lock;
- (int)tryLock;
- (void)unlock;

@end
@implementation DrLock

- (instancetype)init{
    self = [super init];
    if (self) {
        _lock = OS_SPINLOCK_INIT;
    }
    return self;
}

/// 上锁
- (void)lock {
    OSSpinLockLock(&_lock);
}

/// 上锁,失败返回:NO,成功返回:YES
- (int)tryLock {
    return OSSpinLockTry(&_lock);
}

/// 解锁
- (void)unlock {
    OSSpinLockUnlock(&_lock);
}

@end

关于tryLock的使用,当返回NO时,表示当前锁已经被锁住(即:有一个线程在访问边界值时被当前锁锁住了),此时我们需要调用lock将新的线程锁住。因此我们可以通过这个方法来判断当前是否存在多个线程同时访问边界值。

 

os_unfair_lock(互斥锁)

os_unfair_lock是用于替代OSSpinLock的,与OSSpinLock的不同点在于等待锁的线程不会处于忙等状态,而是阻塞当前线程,使其处于休眠状态。在使用方面与OSSpinLock一样,代码如下:
#import <os/lock.h>
@interface DrLock : NSObject {
    
    os_unfair_lock _lock;
}

- (void)lock;
- (int)tryLock;
- (void)unlock;

@end
@implementation DrLock

- (instancetype)init{
    self = [super init];
    if (self) {
        _lock = OS_UNFAIR_LOCK_INIT;
    }
    return self;
}

/// 上锁
- (void)lock {
    os_unfair_lock_lock(&_lock);
}

/// 上锁,失败返回:NO,成功返回:YES
- (int)tryLock {
    return os_unfair_lock_trylock(&_lock);
}

/// 解锁
- (void)unlock {
    os_unfair_lock_unlock(&_lock);
}

@end

关于tryLock的使用,当返回NO时,表示当前锁已经被锁住(即:有一个线程在访问边界值时被当前锁锁住了),此时我们需要调用lock将新的线程锁住。因此我们可以通过这个方法来判断当前是否存在多个线程同时访问边界值。

 

dispatch_semaphore_t(信号量)

信号量是通过维护一个值a,初始化时设置这个值a,当调用wait方法时,使a-=1,此时如果a<=0,当前线程将被阻塞,至于休眠状态。当调用signal方法时,使a+=1,此时如果a>0或wait设置的timeout时间到了,被阻塞的线程会被唤起,继续处理任务。因此在处理并发任务时,我们可以通过初始化时设置一个值,来控制并发的个数。
@interface DrLock : NSObject {
    
    dispatch_semaphore_t _lock;
}

- (void)lock;
- (void)unlock;

@end
@implementation DrLock

- (instancetype)init{
    self = [super init];
    if (self) {
        _lock = dispatch_semaphore_create(1);
    }
    return self;
}

/// 上锁
- (void)lock {
    dispatch_semaphore_wait(_lock, DISPATCH_TIME_FOREVER);
}

/// 解锁
- (void)unlock {
    dispatch_semaphore_signal(_lock);
}

@end

 

 

pthread_mutex(互斥锁)

pthread_mutex的使用和os_unfair_lock差不多,唯一不同就是需要提供属性设置,这里attr有几种type可以设置,包括:PTHREAD_MUTEX_NORMALPTHREAD_MUTEX_ERRORCHECKPTHREAD_MUTEX_RECURSIVE,一般就使用PTHREAD_MUTEX_NORMAL就可以了。
在使用pthread_mutex时需要注意,在不需要这个锁的时候,需要手动释放锁资源
#import <pthread.h>
@interface DrLock : NSObject {
    
    pthread_mutex_t _lock;
}

- (void)lock;
- (int)tryLock;
- (void)unlock;

@end
@implementation DrLock

- (void)dealloc{
    pthread_mutex_destroy(&_lock); // 注意这里要销毁锁
}

- (instancetype)init{
    self = [super init];
    if (self) {
//        pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;// 静态初始化方法
//        _lock = lock;
        
        // 动态初始化
        pthread_mutexattr_t attr;
        pthread_mutexattr_init(&attr);
        pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_NORMAL); // 设置锁的类型
        pthread_mutex_init(&_lock, &attr);
        pthread_mutexattr_destroy(&attr);
    }
    return self;
}

/// 上锁
- (void)lock {
    pthread_mutex_lock(&_lock);
}

/// 上锁,返回值==0:成功;返回值!=0:失败(当互斥量已经被锁住时调用该函数将返回错误代码EBUSY:16,此时需要调用lock对当前线程上锁)
- (int)tryLock {
    return pthread_mutex_trylock(&_lock);
}

/// 解锁
- (void)unlock {
    pthread_mutex_unlock(&_lock);
}

@end

关于tryLock的使用,当返回非0的值时,表示当前锁已经被锁住(即:有一个线程在访问边界值时被当前锁锁住了),此时我们需要调用lock将新的线程锁住。因此我们可以通过这个方法来判断当前是否存在多个线程同时访问边界值。

 
- (void)task:(int)ID {
    int res = [_lock tryLock];
    if (res != 0) {
        NSLog(@"加锁失败: %@", @(res));
        [_lock lock];
    }
    NSLog(@"执行任务:%@", @(ID));
    sleep(2);
    NSLog(@"执行任务:%@完成", @(ID));
    [_lock unlock];
}

 

pthread_mutex(递归锁) 

它与互斥锁类似,都是采用阻塞当前线程的方式实现加锁功能,区别在于它可以保证同一个线程可以多次进入同一块加锁区域一般情况下。一个线程只能申请一次锁,也只能在获得锁的情况下才能释放锁,多次申请锁或释放未获得的锁都会导致崩溃。假设在已经获得锁的情况下再次申请锁,线程会因为等待锁的释放而进入睡眠状态,因此就不可能再释放锁,从而导致死锁。

然而这种情况经常会发生,比如某个函数申请了锁,在临界区内又递归调用了自己(出现于递归调用的情况)。因此我们需要一个递归锁解决这种情况的调用。

#import <pthread.h>
@interface DrLock : NSObject {
    
    pthread_mutex_t _lock;
}

- (void)lock;
- (int)tryLock;
- (void)unlock;

@end
@implementation DrLock

- (void)dealloc{
    pthread_mutex_destroy(&_lock); // 注意这里要销毁锁
}

- (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(&_lock, &attr);
        pthread_mutexattr_destroy(&attr);
    }
    return self;
}

/// 上锁
- (void)lock {
    pthread_mutex_lock(&_lock);
}

/// 上锁,返回值==0:成功;返回值!=0:失败(当互斥量已经被锁住时调用该函数将返回错误代码EBUSY:16,此时需要调用lock对当前线程上锁)
- (int)tryLock {
    return pthread_mutex_trylock(&_lock);
}

/// 解锁
- (void)unlock {
    pthread_mutex_unlock(&_lock);
}

@end

递归锁与互斥锁的区别,仅仅在于设置的类型不一样,递归锁的类型为:PTHREAD_MUTEX_RECURSIVE。下面我们举个例子,介绍一下这个递归锁的用法。

下面是一个倒计时的任务,多个线程会调用该方法,每个线程调用时传入的参数不同。
- (void)task:(int)ID count:(int)count {
    int i = [_lock tryLock];
    if (i != 0) {
        NSLog(@"加锁失败: %@", @(i));
        [_lock lock];
    }
    if (count == 0) {
        NSLog(@"执行任务:%@完成", @(ID));
        sleep(1);
        [_lock unlock];
        return;
    }
    NSLog(@"执行任务:%@,倒计时:%@", @(ID), @(count));
    [self task:ID count:count-1];
    [_lock unlock];
}

需要注意的一点,lock与unlock一定要成对执行,即递归前后,需要保持lock与unlock的调用次数一致,否则会出现未解锁的情况,导致线程一直处于阻塞状态,后续的任务无法进行。

当然递归锁也可以用于非递归的调用方法中,只要确保lock与unlock调用次数一致即可。而在递归调用的方法中我们必须要使用递归锁了。

 

NSLock(互斥锁)

是对pthread_mutex(互斥锁)OC封装,其内部维护了一个pthread_mutex互斥锁,用法和pthread_mutex的使用一样。并且它提供了一个特殊的lock方法

- (BOOL)lockBeforeDate:(NSDate *)limit;

该方法用于指定一个时间,在该时间之前会对当前线程一直执行加锁。当超出指定时间后未收到unlock,会返回NO,加锁失败,相当于给当前锁设置了一个超时时间。
- (void)task:(int)ID {
    if (![_lock lockBeforeDate:[NSDate dateWithTimeIntervalSinceNow:2]]) {
        NSLog(@"加锁失败");
        [_lock lock];
    }
    NSLog(@"执行任务:%@", @(ID));
    sleep(2);
    NSLog(@"执行任务:%@完成", @(ID));
    [_lock unlock];
}

以上任务会执行2秒钟,我们在加锁的地方采用lockBeforeDate方法,设置一个锁的超时时间2秒,当2秒后未收到unlock,该方法将不再阻塞线程,返回NO,因此我们增加了一个lock方法,防止多个线程同时调用该方法,导致lockBeforeDate超过2秒。

以上代码一旦同时存在两个或以上线程同时调用,就会出现lockBeforeDate超时,导致加锁失败。

 

 NSRecursiveLock(递归锁)

 是对pthread_mutex(递归锁)的OC封装,其内部维护了一个pthread_mutex递归锁,用法与pthread_mutex一样。同样它也提供了一个与NSLock一样的方法

- (BOOL)lockBeforeDate:(NSDate *)limit;

 它的作用于NSLock的一样。
 
 

NSCondition(条件锁)

它实际上既是一个锁对象,又是一个线程检查器:锁主要为了当检测条件时保护数据源,执行条件引发的任务;线程检查器主要是根据条件决定是否继续运行线程,即线程是否被阻塞。通俗的说,也就是条件成立,才会执行锁住的代码。条件不成立时,线程就会阻塞,直到另一个线程向条件对象发出信号解锁为止。
它除了锁必须的一些方法外,还提供了如下的方法,用于阻塞线程与唤起线程
- (void)wait; // 阻塞线程
- (BOOL)waitUntilDate:(NSDate *)limit; // 阻塞线程,并设置一个超时时间
- (void)signal; // 唤醒一个阻塞中的线程
- (void)broadcast; // 唤醒全部阻塞中的线程

 该锁适用于生产者与消费者模式,例如:一个公司负责生产商品,一些消费者负责购买这些商品

 

/// 生产商品
- (void)production:(NSString *)goodsName {
    [_condition lock];
    [_goodsList addObject:goodsName];
    [_condition unlock];
//    [_condition signal]; // 通知一个正在排队的消费者,有货了,可以购买了
    [_condition broadcast]; // 通知所有正在排队的消费者,有货了,可以购买了
}

/// 购买商品
- (void)buy {
    
    [_condition lock];
    while (_goodsList.count == 0) {
        NSLog(@"%@ 正在排队......", [NSThread currentThread].name);
        // 没商品了,排队等待
//        [_condition wait];// 这里会让当前线程阻塞,并休眠,直到发出signal或broadcast被唤醒,继续向下执行
//        if (![_condition waitUntilDate:[NSDate dateWithTimeIntervalSinceNow:6]]) { // 最多等6秒,我就不等了
//            break; // 等待超时waitUntilDate会返回NO,而通过signal或broadcast唤醒,它会返回YES。因此如果是超时了,那就break跳出循环,向下执行
//        }
    }
    
    if (_goodsList.count == 0) {
        NSLog(@"%@ 没有买到商品,回家了......", [NSThread currentThread].name);
    }else {
        
        NSString *goods = [_goodsList lastObject];
        [_goodsList removeLastObject];
        NSLog(@"%@ 买到了 %@", [NSThread currentThread].name, goods);
    }
    [_condition unlock];
}



    // 消费者购买商品
    
    NSThread *thread1 = [[NSThread alloc] initWithTarget:self selector:@selector(buy) object:nil];
    thread1.name = @"jack";
    [thread1 start];
    
    NSThread *thread2 = [[NSThread alloc] initWithTarget:self selector:@selector(buy) object:nil];
    thread2.name = @"bob";
    [thread2 start];
    
    NSThread *thread3 = [[NSThread alloc] initWithTarget:self selector:@selector(buy) object:nil];
    thread3.name = @"drbox";
    [thread3 start];
    
    
    // 工人生产商品
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        dispatch_queue_t queue = dispatch_queue_create("workers1", DISPATCH_QUEUE_SERIAL);
        dispatch_async(queue, ^{
            [self production:@"苹果iPhoneX 编号:1001"];
        });
    });
    
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(10 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        dispatch_queue_t queue = dispatch_queue_create("workers2", DISPATCH_QUEUE_SERIAL);
        dispatch_async(queue, ^{
            [self production:@"苹果iPhoneX 编号:1002"];
        });
    });
    
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        dispatch_queue_t queue = dispatch_queue_create("workers3", DISPATCH_QUEUE_SERIAL);
        dispatch_async(queue, ^{
            [self production:@"苹果iPhoneX 编号:1003"];
        });
    });

 

以上为典型的生产者与消费者模式,当生产者生产出一件商品,通知一个或全部排队中的消费者购买。注意,这里有两点注意事项:

  1. 由于生产商品为多线程,因此需要对goodsList操作前加锁。
  2. 通知消费者时调用signalbroadcast的不同,signal只会唤起一个休眠中的线程,而broadcast则会唤起全部休眠中的线程,因此我们在buy方法中增加了一个while循环来判断,防止broadcast唤起全部休眠的线程,导致有些线程错误的处理产品是否为空的情况
 在消费者购买的buy方法中,当前没有商品时,调用wait或waitUntilDate让当前线程休眠,休眠后,wait或waitUntilDate后面的代码将不会执行,直到被唤醒或等待超时,才会继续执行后面的代码。注意:这里在处理超时的情况判断,只有当线程通过signal或broadcast被唤醒的情况,waitUntilDate才会返回YES,否则返回NO
 
 

pthread_mutex_t + pthread_cond_t(实现条件锁)

通过pthread_mutex_t与pthread_cond_t组合,可以实现与NSCondition一样的效果 ,实际上NSCondition底层就是通过这两个组合实现的,NSCondition就是对这两组合的OC封装,具体的代码实现如下:
#import <pthread.h>
@interface DrLock : NSObject {
    
    pthread_mutex_t _lock;
    pthread_cond_t _cond; // 条件
}

- (void)lock;
- (void)unlock;

@end
@implementation DrLock

- (void)dealloc{
    pthread_mutex_destroy(&_lock); // 注意这里要销毁锁
    pthread_cond_destroy(&_cond);
}

- (instancetype)init{
    self = [super init];
    if (self) {
        pthread_mutexattr_t attr;
        pthread_mutexattr_init(&attr);
        pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_DEFAULT); // 这里设置的类型为互斥锁
        pthread_mutex_init(&_lock, &attr);
        pthread_mutexattr_destroy(&attr);
        
        pthread_condattr_t conAttr;
        pthread_condattr_init(&conAttr);
        
        pthread_condattr_setpshared(&conAttr, PTHREAD_PROCESS_PRIVATE);
        pthread_cond_init(&_cond, &conAttr);
    }
    return self;
}

/// 上锁
- (void)lock {
    pthread_mutex_lock(&_lock);
}

/// 解锁
- (void)unlock {
    pthread_mutex_unlock(&_lock);
}

/// 阻塞当前线程
- (void)wait {
    pthread_cond_wait(&_cond, &_lock);
}

/// 阻塞当前线程
- (BOOL)waitUntilDate:(NSDate *)date {
    struct timespec time;
    time.tv_sec = date.timeIntervalSince1970;
    time.tv_nsec = 0; // 这里必须设置一个数,否则计时不起作用,具体设置多少我不知道这里如何设置,有清楚的可以回复我,谢谢!
    return pthread_cond_timedwait(&_cond, &_lock, &time) == 0;
}

/// 唤醒一个被阻塞的线程
- (void)signal {
    pthread_cond_signal(&_cond);
}

/// 唤醒全部阻塞中的线程
- (void)broadcast {
    pthread_cond_broadcast(&_cond);
}

@end

 

这里我们用到了一个pthread_cond_t条件(记得需要手动释放资源),它在初始化时,需要设置一个属性值,这里我们设置成PTHREAD_PROCESS_PRIVATE,实际上这个也是缺省值。

关于pthread_condattr_setpshared的值范围有以下说明:

 因为pthread_mutex属于 POSIX 线程下的互斥锁,因此它的取值包括:PTHREAD_PROCESS_SHAREDPTHREAD_PROCESS_PRIVATE

因为在客户端上使用条件锁处理的线程,大部分都是在同一个进程下创建的,因此我们这里使用PTHREAD_PROCESS_PRIVATE
 
 

NSConditionLock(升级版的条件锁)

该锁可以使得互斥锁在某种条件下进行锁定和解锁,它和NSCondition很像,但实现方式不同。它没有wait与signal等方法让线程休眠与唤醒,它是通过指定条件,让某个线程来获得锁。我们依然使用NSCondition中的例子来了解它的使用,我们只需要改动生产商品和购买商品这两个方法即可:
/// 生产商品
- (void)production:(NSString *)goodsName {
    [_conditionLock lockWhenCondition:1]; // 只有当前锁的条件为1时,当前线程才可以获得该锁来访问资源,否则会被阻塞。
    [_goodsList addObject:goodsName];
    [_conditionLock unlockWithCondition:2]; // 解除锁,并设置锁的条件为2
}

/// 购买商品
- (void)buy {
    
    NSLog(@"%@ 排队中......", [NSThread currentThread].name);
//    [_conditionLock lockWhenCondition:2]; // 只有当前锁的条件为2时,当前线程才可以获得该锁来访问资源,否则会被阻塞。
    // 我们还可以通过下面方法,来指定一个等待时间,当时间到了,如果当前线程依然没有获得锁,该线程会自动被唤起
    [_conditionLock lockWhenCondition:2 beforeDate:[NSDate dateWithTimeIntervalSinceNow:6]];
    
    if (_goodsList.count == 0) {
        NSLog(@"%@ 没有买到商品,回家了......", [NSThread currentThread].name);
    }else {
        
        NSString *goods = [_goodsList lastObject];
        [_goodsList removeLastObject];
        NSLog(@"%@ 买到了 %@", [NSThread currentThread].name, goods);
    }
    [_conditionLock unlockWithCondition:1]; // 解锁,并设置锁的条件为1,目的是将获取锁的权限交给了生产商品的线程
}

 

我们在初始化条件锁的时候,设置了一个条件值为1

if (!_conditionLock) {
    _conditionLock = [[NSConditionLock alloc] initWithCondition:1];
}

 从这个例子可知,我们可以通过设置一个条件值,来控制哪个线程可以获得锁,从而获取资源。这样一来,我们就可以通过这个条件值来保持多个线程的执行顺序了。

下面是该条件锁的api方法,如下:

- (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;
- (void)lock;
- (void)unlock;

我们需要注意的是,lock与unlock只是对当前线程进行的加锁与解锁,并不受条件值的影响。因此我们在使用lockWhenCondition加锁的时候,如果调用unlock解锁,那么只会解锁当前线程,并不会改变条件值,此时你需要再次调用unlockWithCondition来改变条件值,在实际使用中我想lockWhenCondition与unlockWithCondition应该是成对使用的,除非你有特殊的需求。

 

@synchronized(递归锁)

这个锁我们在开发中也经常看到,也实际使用过,实际上它只是一种语法糖,底层依然是我们上面用到的锁。

我们知道 @synchronized 后面需要紧跟一个 OC 对象,它实际上是把这个对象当做锁来使用。这是通过一个哈希表来实现的,OC 在底层使用了一个递归锁的数组(你可以理解为锁池),通过对对象取哈希值来得到对应的递归锁。(注意:有些文章中提到的这里是一个互斥锁,实际上是错误的,它是采用pthread_mutex实现的递归锁,为啥是递归锁,我们在实际使用中也可以发现,@synchronized是可以嵌套的,嵌套中的OC对象可以是相同的,既然是相同的,那么其对应的锁肯定也是一样的,我们知道,同一个锁不能被同一个线程捕获,而只有递归锁允许

其内部实现实则调用如下两个函数:

// 加锁
func objc_sync_enter(_ obj: Any) -> Int32

// 解锁
func objc_sync_exit(_ obj: Any) -> Int32

 Atomic(原子操作)

狭义上的原子操作表示一条不可打断的操作,也就是说线程在执行操作过程中,不会被操作系统挂起,而是一定会执行完。在单处理器环境下,一条汇编指令显然是原子操作,因为中断也要通过指令来实现。

然而在多处理器的情况下,能够被多个处理器同时执行的操作任然算不上原子操作。因此,真正的原子操作必须由硬件提供支持,比如 x86 平台上如果在指令前面加上 “LOCK” 前缀,对应的机器码在执行时会把总线锁住,使得其他 CPU不能再执行相同操作,从而从硬件层面确保了操作的原子性。

我们在iOS开发中经常使用的@property (atomic, assign)声明属性,其中atomic就是指定当前属性为原子操作。

还有#import <libkern/OSAtomic.h>这个库中的函数,也是原子操作的,如下:

int32_t res = OSAtomicIncrement32(&_count); // 对count进行原子+1的操作,返回操作后的结果

不过这个在iOS10之后被废弃了,改用#import<stdatomic.h>下的如下调用(具体可以看这里):

@property (nonatomic, assign) _Atomic(int) count;
int32_t res = atomic_fetch_add(&_count, 2); // 对count进行原子+2的操作,返回操作前的结果

 

pthread_rwlock_t(读写锁)

与互斥量类似,但读写锁允许更高的并行性。其特性为:写独占,读共享。读写锁存在如下三种状态:
  1. 读模式下加锁状态(读锁)
  2. 写模式下加锁状态(写锁)
  3. 不加锁状态
当读写锁以读模式锁住时,它是以共享模式锁住的;当它以写模式锁住时,它是以独占模式锁住的。写独占、读共享。
#import <pthread.h>
@interface DrLock : NSObject {
    
    pthread_rwlock_t _lock;
}

- (void)rdLock;
- (int)tryRdLock;
- (void)wrLock;
- (int)tryWrLock;
- (void)unlock;

@end
@implementation DrLock

- (void)dealloc{
    pthread_rwlock_destroy(&_lock);
}

- (instancetype)init{
    self = [super init];
    if (self) {
        
//        pthread_rwlock_t lock = PTHREAD_RWLOCK_INITIALIZER; // 静态初始化方法
//        _lock = lock;
        
        pthread_rwlockattr_t attr;
        pthread_rwlockattr_init(&attr);
        pthread_rwlockattr_setpshared(&attr, PTHREAD_PROCESS_PRIVATE); // 这里设置为仅适用于当前进程所创建的线程
        pthread_rwlock_init(&_lock, &attr);
        pthread_rwlockattr_destroy(&attr);
        
    }
    return self;
}

/// 读模式加锁,当前线程无法获取锁时,将会阻塞当前线程
- (void)rdLock{
    pthread_rwlock_rdlock(&_lock);
}

/// 读模式锁,当前线程无法获得锁时,不会阻塞当前线程,直接返回EBUSY
- (int)tryRdLock{ return pthread_rwlock_tryrdlock(&_lock); } /// 写模式加锁,当前线程无法获取锁时,将会阻塞当前线程 - (void)wrLock{ pthread_rwlock_wrlock(&_lock); }
/// 写模式锁,当前线程无法获得锁时,不会阻塞当前线程,直接返回EBUSY
- (int)tryWrLock{ return pthread_rwlock_trywrlock(&_lock); } /// 解锁 - (void)unlock{ pthread_rwlock_unlock(&_lock); } @end

当一个线程拥有了读锁时,执行读操作,此时仍然可以有多个线程拥有读锁。但是此时一个线程尝试拥有写锁时,这个线程将被阻塞,直到读锁被解锁为止。

当一个线程拥有写锁时,执行写操作,此时其他线程将不能获得写锁,并且其他线程也不能拥有读锁。

这就是写独占、读共享。

从上面的api中我们可以看到读锁和写锁拥有两种方法,一个是带try的,一个是不带try的。带try的方法在不能获得锁的情况下,不会阻塞当前线程,而是立刻返回一个EBUSY错误。而不带try的方法,在当前线程无法获得锁时,将会阻塞当前线程,直到其他线程解了锁。

读写锁何时会产生死锁?
即:当一个线程获得了写锁后,这个线程停止工作了,那么这个写锁永远不会被解锁了,由于写是独占的,因此就产生了死锁。
而一个线程获得了读锁后,停止工作了,这个读锁会被自动解锁,不会产生死锁。
 
 

dispatch_barrier_async(GCD栅栏实现读写锁)

GCD栅栏分为异步和同步,dispatch_barrier_async(异步栅栏),dispatch_barrier_sync(同步栅栏)。同步栅栏会阻塞在同一线程下的后面的代码执行,我们可以根据实际情况选择采用同步还是异步。
一般栅栏操作都执行在一个GCD的并行队列中,如果是一个串行队列,那么这个栅栏也就发挥不到其作用了。
栅栏的作用就是将栅栏操作的前后队列任务分离,仅当栅栏任务完成后,其后面的队列任务才会执行。
如下代码事例:
    // 并行队列
    dispatch_queue_t queue = dispatch_queue_create("CONCURRENT", DISPATCH_QUEUE_CONCURRENT);
    
    dispatch_async(queue, ^{
        NSLog(@"执行任务1");
        sleep(1);
    });
    
    dispatch_async(queue, ^{
        NSLog(@"执行任务2");
        sleep(1);
    });
    
    dispatch_barrier_async(queue, ^{ // 一个异步栅栏,它不会阻塞当前线程
        NSLog(@"执行栅栏操作1");
        sleep(1);
    });
    
    dispatch_barrier_sync(queue, ^{ // 一个同步栅栏,它会阻塞当前线程
        NSLog(@"执行栅栏操作2");
        sleep(1);
    });
    
    dispatch_async(queue, ^{
        NSLog(@"执行任务3");
        sleep(1);
    });
    
    dispatch_async(queue, ^{
        NSLog(@"执行任务4");
        sleep(1);
    });
    
    NSLog(@"代码执行完毕");

输入结果如下:

执行任务1
执行任务2
执行栅栏操作1
执行栅栏操作2
代码执行完毕 // 这里被同步栅栏阻塞了,因此在同步栅栏之后执行的
执行任务3
执行任务4

经过上面的事例,我们已经对栅栏有了了解,接下来我们举个实际应用:

@interface MessageManager : NSObject{
    
    NSMutableArray *_msgList;
    dispatch_queue_t _queue;
}

- (NSString *)getMessage;
- (void)setMessage:(NSString *)msg;

@end
@implementation MessageManager

- (instancetype)init {
    self = [super init];
    if (self) {
        _msgList = [@[] mutableCopy];
        _queue = dispatch_queue_create("lock", DISPATCH_QUEUE_CONCURRENT); // 为了提高读的效率,这里采用并行队列
    }
    return self;
}

- (NSString *)getMessage{
    __block NSString *msg = nil;
    dispatch_sync(_queue, ^{ // 采用同步读操作
        msg = [self -> _msgList componentsJoinedByString:@", "];
    });
    return msg;
}
- (void)setMessage:(NSString *)msg{
    dispatch_barrier_async(_queue, ^{ // 采用异步栅栏,执行写操作
        [self -> _msgList addObject:msg];
    });
}

@end

上面是一个利用GCD栅栏实现的一个读写消息的操作

  1. 为了提高读的效率,我们采用并行队列实现。
  2. 读消息是立即返回的,因此采用队列的同步操作。
  3. 写消息可能会花费一定时间,因此我们采用异步栅栏,这样不会阻塞当前线程

那么除了GCD的栅栏可以实现读写锁,可不可以采用GCD的group实现呢?答案是不可以。我们要清楚一点,读写锁的特点是什么?写独占,读共享,也就是多读少写。明白了这一点,我们就会知道为什么不能用group实现了。group是采用dispatch_group_waitdispatch_group_leave来实现同步操作的,这也就导致不能实现写独占,读共享了。

 
 
 
posted @ 2021-12-02 18:27  zbblogs  阅读(2185)  评论(0编辑  收藏  举报