备忘录之-iOS保证线程安全的锁和方法

只要系统中存在多线程,存在共享资源,那么锁就是一个绕不过去的概念,像后台数据库读写数据就要用到读写锁,来保证数据的一致性;iOS中也一样,也需要各种各样的锁来保证多线程的正常执行。之前一直用@synchronized,NSLock和信号量比较多,其实还有很多加锁的方式,总结一下,便于以后查找。

1. OSSpinLock

OSSpinLock由于存在优先级反转问题,已经不再安全,在iOS 10的时候也被苹果爸爸给抛弃了。简单来说,在iOS中,线程的执行会按照优先级来安排,高优先级的任务不会被低优先级的任务干扰,这就存在一个很大的问题,比如一个低优先级的线程获取了锁并执行,这时候一个高优先级的任务到来,就会优先得到执行,由于pin lock自旋锁的特性,它会一直占据CPU来尝试获取锁,处于忙等状态,而低优先级的线程又得不到执行,也无法释放锁。优先级反转的问题并不是理论上假设会出现的问题,libobjc已经遇到过很多次这个问题了,所以苹果的工程师不得不放弃了OSSpinLock。而iOS 10以后,推荐使用os_unfair_lock。这里就简单看下OSSpinLock的几个方法,就不写了

OSSpinLock  lock  =  OS_SPINLOCK_INIT;   // 初始化锁
bool  result  =  OSSpinLockTry(&_lock);  // 尝试加锁(加锁成功会返回true,否则返回false,如果加锁失败,推荐直接使用下面的加锁方法)

OSSpinLockLock(&_lock);  // 加锁
OSSpinLockUnlock(&_lock);  // 解锁

2. os_unfair_lock

由于OSSpinLock的安全性问题,新的替代品os_unfair_lock在iOS 10出现了。os_unfair_lock和OSSpinLock的不同点在于等待锁的线程不会处于忙等状态,而是在内核休眠等待。下面是一个小:

#import 
@property (nonatomic, assign) os_unfair_lock lock_os_unfair_lock;

// 初始化
self.lock_os_unfair_lock = OS_UNFAIR_LOCK_INIT;

if (!os_unfair_lock_trylock(&_lock_os_unfair_lock)) {
    os_unfair_lock_lock(&_lock_os_unfair_lock);
}
    
//  访问共享资源的操作
.......
    
// 解锁
os_unfair_lock_unlock(&_lock_os_unfair_lock);

这里需要注意的是 os_unfair_lock_trylock()的使用,如果这个函数加锁成功的话,会返回true,否则返回false,当加锁失败的时候,我们千万不要去循环调用这个函数再去尝试加锁,循环调用os_unfair_lock_trylock()其实和os_unfair_lock_lock()函数的效果是一样的,而os_unfair_lock_lock()是系统帮我们实现的更高效的获取锁的函数,如果我们手动循环trylock更会错过系统自己实现的解决优先级反转的方案。所以如果trylock失败了,要么就去做其它可以做的事,要么就调用os_unfair_lock_lock()函数去获取锁吧。

3. pthread_mutex

这也是一种自旋锁,等待锁的线程同样也会处于休眠状态
下面直接看

#import 
@property (nonatomic, assign) pthread_mutex_t lock_pthread_mutex;

    // 初始化属性
    pthread_mutexattr_t attr;
    pthread_mutexattr_init(&attr);
    pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_NORMAL);
    
    
    // 如果想在递归中使用的话,就要设置成递归锁,第二个参数设置成 PTHREAD_MUTEX_RECURSIVE 就行了.
//    pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
    
    // 初始化锁
    pthread_mutex_init(&_lock_pthread_mutext, &attr);
    // 销毁属性
    pthread_mutexattr_destroy(&attr);

    // 尝试加锁
    // pthread_mutex_trylock() 返回值为 0 表示加锁成功
    if (pthread_mutex_trylock(&_lock_pthread_mutext) != 0) {  // 这里和os_unfair_lock_trylock()一样,加锁失败的话直接调用加锁方法
        pthread_mutex_lock(&_lock_pthread_mutext);
    }
    
    //  访问共享资源的操作
    ......
    
    // 解锁
    pthread_mutex_unlock(&_lock_pthread_mutext);

pthread_mutex的使用和os_unfair_lock差不多,唯一不同就是需要提供属性设置,这里attr有几种type可以设置,一般就使用PTHREAD_MUTEX_NORMAL就可以了;上面的注释中提到了递归锁,递归锁其实也是普普通通的锁,它的特性就是保证同一个线程可以多次进入同一块加锁区域,后面还有专门用于递归的锁,到时候再详细解释,只需要知道这里将type设置为PTHREAD_MUTEX_RECURSIVE就可以在递归中使用了.

4. NSLock

NSLock是非常简单的一种锁,也是对mutex普通锁的封装,这是我们在代码中使用频率很高的一种锁,一些优秀的第三方框架中也会频繁出现它的身影。它的使用也非常简单:

NSLock  *lock  =  [[NSLock alloc] init];
[lock  lock];

// 访问共享资源的操作
......
[lock  unlock];

5. NSRecursiveLock

上面提到过递归锁的概念,NSRecursiveLock就是专门用于递归的锁,也是对上面提到的pthread_mutex递归锁的封装。下面看下一个简单的递归,如果使用普通的锁会怎么样:

// 假如这里self.lock 是 NSLock
-(void)recursiveAction  {
    [self.lock  lock];
    if (...) {
        [self  recursiveAction];
    }
    [self.lock  unlock];
}

由于递归的特性,递归方法会一直执行加锁操作,直到最后if条件不满足了才会执行一串的解锁操作,简单点就是这样的:
加锁,加锁,加锁....解锁,解锁,解锁
如果用普通锁的话,第一次加锁后,第二次再去加锁就没法获取锁了,只能等待锁释放后才能加锁成功。所以普通锁在递归中是不适用的。而递归锁则允许同一个线程多次对同一块共享区域加锁:

@property (nonatomic, strong) NSRecursiveLock *lock_nsrecursivelock;

self.lock_nsrecursivelock = [[NSRecursiveLock alloc] init]; // 初始化

-(void)test_nsrecursivelock {
    [self.lock_nsrecursivelock lock];
    NSLog(@"%s", __func__);
    
    static int count = 0;
    if (count < 10) {
        count ++;
        [self test_nsrecursivelock];
    }
    
    [self.lock_nsrecursivelock unlock];
}

6. NSCondition

NSCondition是比较适合生产者-消费者模式使用的锁,生产者线程产生数据并负责唤醒消费者线程,消费者线程负责消费数据,有数据就加锁消费,没数据就睡眠。下面是一个简单的小

@property (nonatomic, strong) NSCondition *lock_condition;
@property (nonatomic, strong) NSMutableArray *conditionData;  // 数据

self.lock_condition = [[NSCondition alloc] init];  // 初始化
self.conditionData = [NSMutableArray array];

    for (int i=0; i< 20; i++) {
        [[[NSThread alloc] initWithTarget:self selector:@selector(consume) object:nil] start];   // 20个消费者线程
    }
    [[[NSThread alloc] initWithTarget:self selector:@selector(product) object:nil] start];  // 生产者线程

//  生产数据
-(void)product {
    [self.lock_condition lock];

    // 生产数据
    for (int i=0; i<20; i++) {
        [self.conditionData addObject:[NSString stringWithFormat:@"Test%d", i]];
    }
    
//    [self.lock_condition signal]; // 发出信号,唤醒一个正在等待的线程
    
    // 广播
    [self.lock_condition broadcast]; // 发出信号,唤醒所有正在等待的线程
    
    [self.lock_condition unlock];
}

// 消费数据
-(void)consume {
    [self.lock_condition lock];
    
    if (self.conditionData.count == 0) {
        [self.lock_condition wait];  // 没有资源了,开始等待
    }
    
    [self.conditionData removeLastObject];  // 消费资源
    
    [self.lock_condition unlock];
}

这里需要注意的几个地方:

  1. signal和broadcast函数的区别:
    signal发出信号并唤醒其中一个正在等待的线程,而broadcast会唤醒所有正在等待的线程
  2. 关于wait
    这里wait不仅仅需要接收到signal才能执行,而且必须是加过锁之后,才会继续往下执行

7. NSConditionLock

NSConditionLock是对NSCondition的进一步封装,它可以设置更加具体的值。

@property (nonatomic, strong) NSConditionLock *lock_nsconditionlock;

self.lock_nsconditionlock = [[NSConditionLock alloc] initWithCondition:1];  // 这里将condition设为1

[[[NSThread alloc] initWithTarget:self selector:@selector(threadAction1) object:nil] start];
[[[NSThread alloc] initWithTarget:self selector:@selector(threadAction2) object:nil] start];
[[[NSThread alloc] initWithTarget:self selector:@selector(threadAction3) object:nil] start];

-(void)threadAction1 {
    [self.lock_nsconditionlock lockWhenCondition:1]; // 这里只有当condition为1的时候才会获取到锁并访问共享资源
    NSLog(@"in thread 1");  
    // 操作共享资源
    ......
    [self.lock_nsconditionlock unlockWithCondition:2];  // 释放锁,并将NSConditionLock的condition设为2
}

-(void)threadAction2 {
    [self.lock_nsconditionlock lockWhenCondition:2];  // 这里只有当condition为2的时候才会获取到锁
    NSLog(@"in thread 2");
    // 操作共享资源
    ......
    [self.lock_nsconditionlock unlockWithCondition:3];  // 放弃锁,并将NSConditionLock的condition设为3
}

-(void)threadAction3 {
    [self.lock_nsconditionlock lockWhenCondition:3];    
    NSLog(@"in thread 3");
    // 操作共享资源
    ......
    [self.lock_nsconditionlock unlock];  // 只是放弃当前获取到的锁
}

8. dispatch_queue

dispatch_queue和锁并没有什么关系,它能用来保证线程安全的原因在于串行队列中任务只能按顺序一个一个执行的特点。只需要声明一个串行队列就可以了

dispatch_queue_t  queue  =  dispatch_queue_create("lock_queue",  DISPATCH_QUEUE_SERIAL);
dispatch_sync(queue, ^{
    // 访问共享资源
});

9. dispatch_semaphore

信号量也可以用来保证线程安全,只需要设置信号量的初始值为1就可以了。当信号量大于0的时候就可以执行,并将信号量减1,当信号量等于0时就会阻塞线程并等待。小:

@property (nonatomic, strong) dispatch_semaphore_t   lock_dispatch_semaphore;

self.lock_dispatch_semaphore = dispatch_semaphore_create(1);

-(void)dispatch_semaphore_test {
    
    // 如果信号量大于0,就将信号量减1,并执行访问共享资源的操作
    dispatch_semaphore_wait(self.lock_dispatch_semaphore, DISPATCH_TIME_FOREVER);
    
    // 访问并操作共享资源
    ......
    
    // 共享资源操作完毕,发送signal并将信号量加1
    dispatch_semaphore_signal(self.lock_dispatch_semaphore);
}

10. @synchronized

@synchronized是使用更加频繁的一种保护线程安全的方法,尤其在java开发中,@synchronized出现的频率会更高。@synchronized也属于是一种递归锁,也可以用在递归中,@synchronized(obj)里的obj参数可以是一个实例对象,类对象或者静态变量:

@synchronized (self) {
    // 访问共享资源
}

@synchronized ([self class]) {
    // 访问共享资源
}

static NSObject *lock;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
    lock = [[NSObject alloc] init];
});
@synchronized (lock) {
    // 访问共享资源
}

你可能感兴趣的:(备忘录之-iOS保证线程安全的锁和方法)