【转载+补充】iOS开发中的各种锁

  • 转载自:http://www.cocoachina.com/ios/20161129/18216.html
  • 递归锁参考: http://www.cocoachina.com/ios/20150513/11808.html

在日常开发过程中,为了提升程序运行效率,以及用户体验,我们经常使用多线程。在使用多线程的过程中,难免会遇到资源竞争问题。我们采用锁的机制来确保线程安全。在盘点锁之前,我们先来介绍几个概念

-线程安全

当一个线程访问数据的时候,其他的线程不能对其进行访问,直到该线程访问完毕。即,同一时刻,对同一个数据操作的线程只有一个。只有确保了这样,才能使数据不会被其他线程污染。而线程不安全,则是在同一时刻可以有多个线程对该数据进行访问,从而得不到预期的结果。

比如写文件和读文件,当一个线程在写文件的时候,理论上来说,如果这个时候另一个线程来直接读取的话,那么得到将是不可预期的结果。

-信号量

在多线程环境下用来确保代码不会被并发调用。在进入一段代码前,必须获得一个信号量,在结束代码前,必须释放该信号量,其他想要想要执行该代码的线程必须等待直到前者释放了该信号量。

以一个停车场的运作为例。简单起见,假设停车场只有三个车位,一开始三个车位都是空的。这时如果同时来了五辆车,看门人允许其中三辆直接进入,然后放下车拦,剩下的车则必须在入口等待,此后来的车也都不得不在入口处等待。这时,有一辆车离开停车场,看门人得知后,打开车拦,放入外面的一辆进去,如果又离开两辆,则又可以放入两辆,如此往复。

在这个停车场系统中,车位是公共资源,每辆车好比一个线程,看门人起的就是信号量的作用。

- 互斥锁

一种用来防止多个线程同一时刻对共享资源进行访问的信号量,它的原子性确保了如果一个线程锁定了一个互斥量,将没有其他线程在同一时间可以锁定这个互斥量。它的唯一性确保了只有它解锁了这个互斥量,其他线程才可以对其进行锁定。当一个线程锁定一个资源的时候,其他对该资源进行访问的线程将会被挂起,直到该线程解锁了互斥量,其他线程才会被唤醒,进一步才能锁定该资源进行操作。

所以,为了线程安全,我们使用锁的机制来确保,同一时刻只有同一个线程来对同一个数据源进行访问。在iOS开发过程中我们通常使用以下几种锁(互斥锁、递归锁、读写锁、自旋锁、条件锁、信号量机制实现锁):

  1. NSLock --- 互斥锁
  2. NSRecursiveLock --- 递归锁
  3. NSCondition --- 特殊类型的锁
  4. NSConditionLock --- 互斥锁
  5. pthread_mutex --- 互斥锁
  6. pthread_rwlock --- 读写锁
  7. POSIX Conditions --- 条件锁(需要互斥锁和条件两项来实现)
  8. OSSpinLock --- 自旋锁
  9. os_unfair_lock --- 因为自旋锁已经不再安全,然后苹果又整出这个锁
  10. dispatch_semaphore --- 信号量机制实现锁
  11. @synchronized --- 互斥锁

下面就一一盘点所有锁的用法:

1.NSLock_互斥锁

  • 官方api解释
    NSLock 对象是在应用中用于协调多个线程操作。NSLock对象,可用于间接访问应用程序的全局数据或者保护关键部分代码,允许他运行的更自然(atomically)。
  • 用法
    NSLock实现了最基本的互斥锁,遵循了 NSLocking 协议,通过 lock 和 unlock 来进行锁定和解锁:
- (void)doSomething {

   [self.lock lock];

   //TODO: do your stuff

   [self.lock unlock];

}
  • 使用场景举例(模拟窗口卖票)
#import "ViewController.h"

@interface ViewController (){
    
    int _ticketCount ;  // 总票数
    int _soldCount;     // 已经售出票数
    NSLock *_lock;   // 数据锁
}

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
   
    _ticketCount = 100;
    _soldCount = 0;
    

    _lock = [[NSLock alloc] init];
    
    NSThread *thread_1 = [[NSThread alloc] initWithTarget:self selector:@selector(sellTicket) object:nil];
    thread_1.name = @"1号售票窗口";
    [thread_1 start];
    
    NSThread *thread_2 = [[NSThread alloc] initWithTarget:self selector:@selector(sellTicket) object:nil];
    thread_2.name = @"2号售票窗口";
    [thread_2 start];
    
    NSThread *thread_3 = [[NSThread alloc] initWithTarget:self selector:@selector(sellTicket) object:nil];
    thread_3.name = @"3号售票窗口";
    [thread_3 start];
    
}

-(void)sellTicket{
    
    // 加锁
    [_lock lock];
    
    if (_ticketCount == 0) {  // 票卖完了
        NSLog(@"===========%@ :剩余票数%d",[NSThread currentThread].name, _ticketCount);
        NSLog(@"卖的总票数:%d",_soldCount);
        [_lock unlock];
        return;
    }

    // 延时卖票  (加上这一句,可以看到票一张张被卖掉的过程)
    [NSThread sleepForTimeInterval:0.3];

    _ticketCount --;
    _soldCount ++;
    
   NSLog(@"%@ :剩余票数%d",[NSThread currentThread].name, _ticketCount);
   
    
    // 解锁
    [_lock unlock];
    
    // 一直售票
    [self sellTicket];

}

我们先来看下,不加锁的结果(把 lock 和 unlock 的代码全部注掉):

不加锁_线程不安全.png

很明显,不加锁的话,会大概率造成不同的售票窗口卖出同一张票(不同的线程同时访问同一个数据),这就是线程不安全的。
然后,我们再来看,加锁的结果,即运行以上示例代码:

【转载+补充】iOS开发中的各种锁_第1张图片
加锁_线程安全.png

100张票太长,只截图了一段。但是很明显每张票都是被依次卖出的,这就是加锁之后的线程安全。

  • 需要特别注意的是:
    由于NSLock是互斥锁,当一个线程进行访问的时候,该线程获得锁,其他线程进行访问的时候,将被操作系统挂起,直到该线程释放锁,其他线程才能对其进行访问,从而却确保了线程安全。但是如果连续锁定两次,则会造成死锁问题。那如果想在递归中使用锁,那要怎么办呢,这就用到了 NSRecursiveLock 递归锁。

2. NSRecursiveLock_递归锁

NSRecursiveLock实际上定义的是一个递归锁,这个锁可以被同一线程多次请求,而不会引起死锁。这主要是用在循环或递归操作中。我们先来看一个示例:

    NSLock *lock = [[NSLock alloc] init];
  
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        
        // block声明
        static void (^RecursiveMethod)(int);
        
        // block定义
        RecursiveMethod = ^(int value) {
            
            // 加锁
            [lock lock];
            
            if (value > 0) {
                
                NSLog(@"value = %d", value);
                sleep(2);
                RecursiveMethod(value - 1);
            }
            
            // 解锁
            [lock unlock];
        };
        
        // 执行block
        RecursiveMethod(5);
        
    });

上面这段代码是一个典型的死锁情况。在我们的线程中,RecursiveMethod是递归调用的。所以每次进入这个block时,都会去加一次锁,而从第二次开始,由于锁已经被使用了且没有解锁,所以它需要等待锁被解除,这样就导致了死锁,线程被阻塞住了。调试器中会输出如下信息(代码块只走了一次):

死锁.png

在这种情况下,我们就可以使用NSRecursiveLock。它可以允许同一线程多次加锁,而不会造成死锁。递归锁会跟踪它被lock的次数。每次成功的lock都必须平衡调用unlock操作。只有所有达到这种平衡,锁最后才能被释放,以供其它线程使用。
所以,对上面的代码进行一下改造,

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

这样,程序就能正常运行了,输出如下所示:

递归锁.png

3. NSCondition

NSCondition 是一种特殊类型的锁,通过它可以实现不同线程的调度。一个线程被某一个条件所阻塞,直到另一个线程满足该条件从而发送信号给该线程使得该线程可以正确的执行。比如说,你可以开启一个线程下载图片,一个线程处理图片。这样的话,需要处理图片的线程由于没有图片会阻塞,当下载线程下载完成之后,则满足了需要处理图片的线程的需求,这样可以给定一个信号,让处理图片的线程恢复运行。

  • 用法
- (void)download {
    [self.condition lock];
    //TODO: 下载文件代码
    if (donloadFinish) { // 下载结束后,给另一个线程发送信号,唤起另一个处理程序
        [self.condition signal];
        [self.condition unlock];
    }
}
- (void)doStuffWithDownloadPicture {
    [self.condition lock];
    while (!donloadFinish) {
        [self.condition wait];
    }
    //TODO: 处理图片代码
    [self.condition unlock];
}

4. NSConditionLock_互斥锁(条件锁)

NSConditionLock 对象所定义的互斥锁可以在使得在某个条件下进行锁定和解锁。它和 NSCondition 很像,但实现方式是不同的。

当两个线程需要特定顺序执行的时候,例如生产者消费者模型,则可以使用 NSConditionLock 。当生产者执行的时候,消费者可以通过特定的条件获得锁,当生产者完成执行的时候,它将解锁该锁,然后把锁的条件设置成唤醒消费者线程的条件。锁定和解锁的调用可以随意组合,lock 和 unlockWithCondition: 配合使用 lockWhenCondition: 和 unlock 配合使用。

  • 用法
- (void)producer {
    while (YES) {
        [self.conditionLock lock];
        NSLog(@"have something");
        self.count++;
        [self.conditionLock unlockWithCondition:1];  // 当生产者释放锁的时候,把条件设置成了1
    }
}
- (void)consumer {
    while (YES) {
        [self.conditionLock lockWhenCondition:1]; //  消费者可以获得该锁,进而执行程序
        NSLog(@"use something");
        self.count--;
        [self.conditionLock unlockWithCondition:0];
    }
}

如果消费者获得锁的条件和生产者释放锁时给定的条件不一致,则消费者永远无法获得锁,也不能执行程序。同样,如果消费者释放锁给定的条件和生产者获得锁给定的条件不一致的话,则生产者也无法获得锁,程序也不能执行。

5. pthread_mutex_互斥锁

POSIX 互斥锁是一种超级易用的互斥锁,使用的时候,只需要初始化一个 pthread_mutex_t 用 pthread_mutex_lock 来锁定 pthread_mutex_unlock 来解锁,当使用完成后,记得调用 pthread_mutex_destroy 来销毁锁。

  • 用法
 pthread_mutex_init(&lock,NULL);
  pthread_mutex_lock(&lock);
  //do your stuff
  pthread_mutex_unlock(&lock);
  pthread_mutex_destroy(&lock);

6. pthread_rwlock_读写锁

读写锁,在对文件进行操作的时候,写操作是排他的,一旦有多个线程对同一个文件进行写操作,后果不可估量,但读是可以的,多个线程读取时没有问题的

  • API
// 初始化
pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER
// 读模式
pthread_rwlock_wrlock(&lock);
// 写模式
pthread_rwlock_rdlock(&lock);
// 读模式或者写模式的解锁
pthread_rwlock_unlock(&lock);
  • 使用场景
dispatch_async(dispatch_get_global_queue(0, 0), ^{
        [self readBookWithTag:1];
    });
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        [self readBookWithTag:2];
    });
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        [self writeBook:3];
    });
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        [self writeBook:4];
    });
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        [self readBookWithTag:5];
    });

- (void)readBookWithTag:(NSInteger )tag {
    pthread_rwlock_rdlock(&rwLock);   //  读模式加锁
    NSLog(@"start read ---- %ld",tag);
    self.path = [[NSBundle mainBundle] pathForResource:@"1" ofType:@".doc"];
    self.contentString = [NSString stringWithContentsOfFile:self.path encoding:NSUTF8StringEncoding error:nil];
    NSLog(@"end   read ---- %ld",tag);
    pthread_rwlock_unlock(&rwLock);  // 读模式解锁
}

- (void)writeBook:(NSInteger)tag {
    pthread_rwlock_wrlock(&rwLock);   // 写模式加锁
    NSLog(@"start wirte ---- %ld",tag);
    [self.contentString writeToFile:self.path atomically:YES encoding:NSUTF8StringEncoding error:nil];
    NSLog(@"end   wirte ---- %ld",tag);   
    pthread_rwlock_unlock(&rwLock);    // 写模式解锁
}
  • 以上代码打印结果
start   read ---- 1
start   read ---- 2
end    read ---- 1
end    read ---- 2
start   wirte ---- 3
end    wirte ---- 3
start   wirte ---- 4
end    wirte ---- 4
start   read ---- 5
end    read ---- 5
  • 当读写锁被一个线程以读模式占用的时候,写操作的其他线程会被阻塞,读操作的其他线程还可以继续进行。
  • 当读写锁被一个线程以写模式占用的时候,写操作的其他线程会被阻塞,读操作的其他线程也被阻塞。

6. POSIX Conditions_条件锁

POSIX 条件锁需要互斥锁和条件两项来实现,虽然看起来没什么关系,但在运行时中,互斥锁将会与条件结合起来。线程将被一个互斥和条件结合的信号来唤醒。

首先初始化条件和互斥锁,当 ready_to_go 为 flase 的时候,进入循环,然后线程将会被挂起,直到另一个线程将 ready_to_go 设置为 true 的时候,并且发送信号的时候,该线程会才被唤醒。

  • 用法
pthread_mutex_t mutex;   
pthread_cond_t condition;  
Boolean     ready_to_go = true;  
void MyCondInitFunction()
{
    pthread_mutex_init(&mutex);       // 初始化互斥锁
    pthread_cond_init(&condition, NULL);     // 初始化条件
}
void MyWaitOnConditionFunction()
{
   
    pthread_mutex_lock(&mutex);           // 加 互斥锁
    // If the predicate is already set, then the while loop is bypassed;
    // otherwise, the thread sleeps until the predicate is set.
    while(ready_to_go == false)    
    {
        pthread_cond_wait(&condition, &mutex);      // 线程被挂起
    }
    // Do work. (The mutex should stay locked.)
    // Reset the predicate and release the mutex.
    ready_to_go = false;
    pthread_mutex_unlock(&mutex);
}
void SignalThreadUsingCondition()
{
    // At this point, there should be work for the other thread to do.
    pthread_mutex_lock(&mutex);
    ready_to_go = true;
    // Signal the other thread to begin work.
    pthread_cond_signal(&condition);
    pthread_mutex_unlock(&mutex);
}

7. OSSpinLock_自旋锁

自旋锁,和互斥锁类似,都是为了保证线程安全的锁。但二者的区别是不一样的,对于互斥锁,当一个线程获得这个锁之后,其他想要获得此锁的线程将会被阻塞,直到该锁被释放。但自旋锁不一样,当一个线程获得锁之后,其他线程将会一直循环在哪里查看是否该锁被释放。所以,此锁比较适用于锁的持有者保存时间较短的情况下。

  • 用法
// 初始化
spinLock = OS_SPINLOCK_INIT;
// 加锁
OSSpinLockLock(&spinLock);
// 解锁
OSSpinLockUnlock(&spinLock);

然而,YYKit 作者 @ibireme 的文章也有说这个自旋锁存在优先级反转问题,具体文章可以戳 不再安全的 OSSpinLock。

未完待续。。。

你可能感兴趣的:(【转载+补充】iOS开发中的各种锁)