在多线程开发中数据的安全是非常重要的,在开辟分线程执行耗时操作以保证主线程不阻塞的同时,数据的安全也要保证才行。如果同一个资源(一个变量或者一段代码或者存储的数据等)同时被多个线程访问修改,可能会造成数据的错乱,得到的也就不是自己想要的结果,所以多线程开发中线程间的同步有时显得尤为重要。

实现多线程间的同步方式:锁(互斥锁、条件锁、递归锁)和GCD(队列、信号量、栅栏)。了解更多类型的锁可以查看线程中常见的几种锁。

一、使用锁实现多线程同步

Cocoa中有NSLock锁类,其内部是封装pthread_mutex,类型是PTHREAD_MUTEX_ERRORCHECK错误检测类型。NSLock通过其对象方法lockunlock对资源进行上锁/解锁操作,当锁处于被持有状态时,其他线程则不能再持有该锁,直到持有者释放锁,其他线程才能够获得锁,这就是其互斥性,以此来达到线程的同步执行:

NSLock *lock = [[NSLock alloc] init]; // 创建锁   
    static void (^YZBlock)(NSString *);
    YZBlock = ^(NSString *str) {
        [lock lock];//上锁操作
        // 需要保护的资源(代码)
        sleep(2);
        NSLog(@"%@  %@",str,[NSThread currentThread]);
        //YZBlock(@"sync 1 ....");//递归造成线程死锁
        [lock unlock];//释放锁操作
    };
    dispatch_queue_t concurrentQueue = dispatch_queue_create("com.gcd.concurrent", DISPATCH_QUEUE_CONCURRENT);    
    dispatch_async(concurrentQueue, ^{// 线程1
        YZBlock(@"sync 1 ....");
    });    
    dispatch_async(concurrentQueue, ^{// 线程2
        YZBlock(@"sync 2 ....");
    });

注意:lockunlock必须成对出现,同一个线程不能连续持有两次锁(即连续执行两个lock方法),而不执行unlock方法,否则会造成线程死锁。以上例来说,我们在耗时操作结束后再调用YZBlock,就会因为递归而造成线程1死锁,死锁后导致不能执行unlock释放锁操作,导致锁一直被线程1持有,其他线程就再也无法访问该资源了,以至于资源也被锁住了。

如何解锁递归造成的线程死锁呢?我们可以使用递归锁(NSRecursiveLock)实现,NSRecursiveLock也是封装了pthread_mutex,但是其类型是PTHREAD_MUTEX_RECURSIVE递归类型。递归锁可以使同一个线程多次进行lock,并且记录lock的次数,当递归结束时,会释放相同次数的锁(即执行相同次数的unlock),例如:

__block int i = 0;
    NSRecursiveLock *lock = [[NSRecursiveLock alloc] init];
    static void (^YZBlock)(NSString *);
    YZBlock = ^(NSString *str) {
        [lock lock];
        sleep(2);
        i += 1;
        NSLog(@"%@  %@",str,[NSThread currentThread]);
        if (i < 5) {// 给定一个结束递归的条件
            YZBlock(@"sync 1 ....");
        }
        NSLog(@"释放锁");
        [lock unlock];
    };

现在我们知道当NSLock可以控制多线程同步执行,原理就是当锁被一个线程持有时,其他线程想要获得锁的线程只能进入休眠状态,等到锁被释放时,再唤醒其他线程抢占锁。使用NSLock需要注意线程死锁的情况,如果遇到同一个线程多次获得锁的的情况,为了避免线程死锁可以使用递归锁来实现。现在有个问题,如果我想要锁在释放时指定的某些线程来获得锁,该如何做呢?这时候我们引入条件锁,使用我们制定的条件来确保唤醒哪些线程,例如生产者-消费者模式。

二、使用GCD实现多线程同步

1、串行队列(serial queue)

// 创建串行队列
    dispatch_queue_t serialQueue = dispatch_queue_create("com.gcd.serial", DISPATCH_QUEUE_SERIAL);
    dispatch_async(serialQueue, ^{
        sleep(2);
        NSLog(@"async1...%@",[NSThread currentThread]);
    });
    dispatch_async(serialQueue, ^{
        sleep(2);
        NSLog(@"async2...%@",[NSThread currentThread]);
    });
    dispatch_async(serialQueue, ^{
        sleep(2);
        NSLog(@"async3...%@",[NSThread currentThread]);
    });

上例中使用串行队列,然后把任务加入到串行队列中,串行队列中的任务是先进先出,一个接着一个执行,然后使用异步函数执行,将三个耗时线程操作放到开辟的分线程中执行,能够保证三个任务的同步执行。

注意:不能向同一个串行队列(serial queue)添加同步任务,因为会造成线程死锁。同步函数dispatch_sync没有开辟分线程的能力,但是它会阻塞当前线程,当向同一个串行队列添加同步任务的时候,本来想要去执行block中的任务,结果该线程被阻塞了,以至于block中的任务不能够被执行,最终导致该线程死锁。

2、信号量(Semaphore)

// 创建并发队列
    dispatch_queue_t concurrentQueue = dispatch_queue_create("com.gcd.concurrent", DISPATCH_QUEUE_CONCURRENT);    dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
    dispatch_async(concurrentQueue, ^{
        sleep(2);
        NSLog(@"async1...%@",[NSThread currentThread]);
        dispatch_semaphore_signal(semaphore);
    });
    
    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
    
    dispatch_async(concurrentQueue, ^{
        sleep(2);
        NSLog(@"async2...%@",[NSThread currentThread]);
        dispatch_semaphore_signal(semaphore);
    });
    
    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
    
    dispatch_async(concurrentQueue, ^{
        sleep(2);
        NSLog(@"async3...%@",[NSThread currentThread]);
    });

通过信号量来控制线程同步执行,wait函数使信号量减1,signal使信号量加1,当信号量小于0时会阻塞当前线程。还可以通过信号量控制线程的并发量。

3、栅栏(Barrier)

dispatch_queue_t concurrentQueue = dispatch_queue_create("com.gcd.concurrent", DISPATCH_QUEUE_CONCURRENT);
    dispatch_async(concurrentQueue, ^{
        sleep(2);
        NSLog(@"async1...%@",[NSThread currentThread]);
    });
    dispatch_async(concurrentQueue, ^{
        sleep(2);
        NSLog(@"async1...%@",[NSThread currentThread]);
    });
    dispatch_barrier_async(concurrentQueue, ^{
        sleep(2);
        NSLog(@"async2...%@",[NSThread currentThread]);
    });
    dspatch_async(concurrentQueue, ^{
        sleep(2);
        NSLog(@"async3...%@",[NSThread currentThread]);
    });
    dispatch_async(concurrentQueue, ^{
        sleep(2);
        NSLog(@"async3...%@",[NSThread currentThread]);
    });

GCD中的栅栏函数作用就是总是让队列中栅栏任务之前的任务先执行,然后执行栅栏任务,接着再执行栅栏之后的任务,以此来达到多线程同步。栅栏函数的功能类似于pthread_rwlock_wrlock读/写锁。

注意:1.栅栏函数有同步栅栏(dispatch_barrier_sync)和异步栅栏(dispatch_barrier_async),区别在于同步栅栏函数会阻塞当前线程,类似于dispatch_sync,异步栅栏不会阻塞当前线程。
2.创建并发队列应该使用dispatch_queue_create函数手动创建的并发队列,如果传递给此函数的队列是串行队列全局并发队列,则此函数的行为与dispatch_async函数类似。