在多线程开发中数据的安全是非常重要的,在开辟分线程执行耗时操作以保证主线程不阻塞的同时,数据的安全也要保证才行。如果同一个资源(一个变量或者一段代码或者存储的数据等)同时被多个线程访问修改,可能会造成数据的错乱,得到的也就不是自己想要的结果,所以多线程开发中线程间的同步有时显得尤为重要。
实现多线程间的同步方式:锁(互斥锁、条件锁、递归锁)和GCD(队列、信号量、栅栏)。了解更多类型的锁可以查看线程中常见的几种锁。
一、使用锁实现多线程同步
Cocoa中有NSLock锁类,其内部是封装pthread_mutex
,类型是PTHREAD_MUTEX_ERRORCHECK
错误检测类型。NSLock
通过其对象方法lock
和unlock
对资源进行上锁/解锁操作,当锁处于被持有状态时,其他线程则不能再持有该锁,直到持有者释放锁,其他线程才能够获得锁,这就是其互斥性,以此来达到线程的同步执行:
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 ....");
});
注意:lock
和unlock
必须成对出现,同一个线程不能连续持有两次锁(即连续执行两个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函数类似。