多线程技术你们都很了解,并且在项目中也比较经常使用。好比开启一个子线程来处理一些耗时的计算,而后返回主线程刷新UI等。首先咱们先简单的梳理一下经常使用到的多线程方案。具体的用法这里我就不说了,每一种方案你们能够去查一下,网上教程不少。面试
咱们比较经常使用的是GCD和NSOperation,固然还有NSThread,pthread。他们的具体区别咱们不详细说,给出下面这一个表格,你们自行对比一下。数组
提到多线程,有一个术语是常常能听到的,同步,异步,串行,并发。安全
同步和异步的区别,就是是否有开启新的线程的能力。异步具有开启线程的能力,同步不具有开启线程的能力。注意,异步只是具有开始新线程的能力,具体开启与否还要跟队列的属性有关系。多线程
串行和并发,是指的任务的执行方式。并发是任务能够多个同时执行,串行之能是一个执行完成后在执行下一个。并发
在面试的过程当中可能被问到什么网状况下会出现死锁的问题,总结一下就是使用sync函数(同步)往当前的串行对列中添加任务时,会出现死锁。异步
多线程和安全问题是分不开的,由于在使用多个线程访问同一块数据的时候,若是同时有读写操做,就可能产生数据安全问题。async
因此这时候咱们就用到了锁这个东西。函数
其实使用锁也是为了在使用多线程的过程当中保障数据安全,除了锁,而后一些其余的实现线程同步来保证数据安全的方案,咱们一块儿来了解一下。atom
下面这些是咱们经常使用来实现线程同步方案的。spa
OSSpinLock os_unfair_lock pthread_mutex NSLock NSRecursiveLock NSCondition NSConditinLock dispatch_semaphore dispatch_queue(DISPATCH_QUEUE_SERIAL) @synchronized
能够看出来,实现线程同步的方案包括各类锁,还有信号量,串行队列。
咱们只挑其中不经常使用的来讲一下使用方法。
下面是咱们模拟了存钱取钱的场景,下面是加锁以前的代码,运行以后确定是有数据问题的。
/** 存钱、取钱演示 */ - (void)moneyTest { self.money = 100; dispatch_queue_t queue = dispatch_get_global_queue(0, 0); dispatch_async(queue, ^{ for (int i = 0; i < 10; i++) { [self __saveMoney]; } }); dispatch_async(queue, ^{ for (int i = 0; i < 10; i++) { [self __drawMoney]; } }); } /** 存钱 */ - (void)__saveMoney { int oldMoney = self.money; sleep(.2); oldMoney += 50; self.money = oldMoney; NSLog(@"存50,还剩%d元 - %@", oldMoney, [NSThread currentThread]); } /** 取钱 */ - (void)__drawMoney { int oldMoney = self.money; sleep(.2); oldMoney -= 20; self.money = oldMoney; NSLog(@"取20,还剩%d元 - %@", oldMoney, [NSThread currentThread]); }
加锁的代码,涉及到锁的初始化、加锁、解锁这么三部分。咱们从OSSpinLock
开始说。
OSSpinLock
叫作自旋锁。那什么叫自旋锁呢?其实咱们能够从大类上面把锁分为两类,一类是自旋锁,一类是互斥锁。咱们经过一个例子来区分这两类锁。
若是线程A率先到达加锁的部分,并成功加锁,线程B到达的时候会由于已经被A加锁而等待。若是是自旋锁,线程B会经过执行一个循环来实现等待,咱们不用管它循环执行了什么,只要知道他在那"转圈圈"等着就行。若是是互斥锁,那线程B在等待的时候会休眠。
使用OSSpinLock
须要导入头文件#import <libkern/OSAtomic.h>
//声明一个锁 @property (nonatomic, assign) OSSpinLock lock; // 锁的初始化 self.lock = OS_SPINLOCK_INIT;
在咱们这个例子中,存钱取钱都是访问了money,因此咱们要在存和取的操做中使用同一个锁。
/** 存钱 */ - (void)__saveMoney { OSSpinLockLock(&_lock); //....省去中间的逻辑代码 OSSpinLockUnlock(&_lock); } /** 取钱 */ - (void)__drawMoney { OSSpinLockLock(&_lock); //....省去中间的逻辑代码 OSSpinLockUnlock(&_lock); }
这就是简单的自旋锁的使用,咱们发如今使用的过程当中,Xcode一直提醒咱们这个OSSpinLock
被废弃了,让咱们使用os_unfair_lock
代替。OSSpinLock
之因此会被废弃是由于它可能会产生一个优先级反转的问题。
具体来讲,若是一个低优先级的线程得到了锁并访问共享资源,那高优先级的线程只能忙等,从而占用大量的CPU。低优先级的线程没法和高优先级的线程竞争(CPU会给高优先级的线程分配更多的时间片),因此会致使低优先级的线程的任务一直完不成,从而没法释放锁。
os_unfair_lock
的用法跟OSSpinLock
很像,就不单独说了。
一看到这个pthread咱们应该就能知道这是一种跨平台的方案了。首先仍是来看用法。
//声明一个锁 @property (nonatomic, assign) pthread_mutex_t lock; //初始化 pthread_mutex_init(pthread_mutex_t *restrict _Nonnull, const pthread_mutexattr_t *restrict _Nullable)
咱们能够看到在初始化锁的时候,第一个参数是锁的地址,第二个参数是一个pthread_mutexattr_t
类型的地址,若是咱们不传pthread_mutexattr_t
,直接传一个NULL,至关于建立一个默认的互斥锁。
//方式一 pthread_mutex_init(mutex, NULL); //方式二 // - 建立attr pthread_mutexattr_t attr; // - 初始化attr pthread_mutexattr_init(&attr); // - 设置attr类型 pthread_mutexattr_settype(&attr,PTHREAD_MUTEX_DEFAULT); // - 使用attr初始化锁 pthread_mutex_init(&_lock, &attr); // - 销毁attr pthread_mutexattr_destroy(&attr);
上面两个方式是一个效果,那为何使用attr,那就说明除了default类型的还有其余类型,咱们后面再说。
在使用的时候用pthread_mutex_lock(&_lock);
和 pthread_mutex_unlock(&_lock);
加锁解锁。
NSLock就是对这种普通互斥锁的OC层面的封装。
调用pthread_mutexattr_settype的时候若是类型传入PTHREAD_MUTEX_RECURSIVE
,会建立一个递归锁。举个例子吧。
// 伪代码 -(void)test { lock; [self test]; unlock; }
若是是普通的锁,当咱们在test方法中,递归调用test,应该会出现死锁,由于被lock,在递归调用时没法调用,一直等待。可是若是锁是递归锁,他会容许同一个线程屡次加锁和解锁,就能够解决这个问题了。
NSRecursiveLock是对递归锁的封装。
咱们直接上这种锁的使用方法,
- (void)otherTest { [[[NSThread alloc] initWithTarget:self selector:@selector(__remove) object:nil] start]; [[[NSThread alloc] initWithTarget:self selector:@selector(__add) object:nil] start]; } // 线程1 // 删除数组中的元素 - (void)__remove { pthread_mutex_lock(&_mutex); NSLog(@"__remove - begin"); if (self.data.count == 0) { // 等待 pthread_cond_wait(&_cond, &_mutex); } [self.data removeLastObject]; NSLog(@"删除了元素"); pthread_mutex_unlock(&_mutex); } // 线程2 // 往数组中添加元素 - (void)__add { pthread_mutex_lock(&_mutex); sleep(1); [self.data addObject:@"Test"]; NSLog(@"添加了元素"); // 信号 pthread_cond_signal(&_cond); // 广播 // pthread_cond_broadcast(&_cond); pthread_mutex_unlock(&_mutex); }
咱们建立了两个线程,一个往数组中添加数据,一个删除数据,咱们经过这个条件锁实现的效果就是在数组中尚未数据的时候等待,数组中添加了一个数据以后在进行删除。
条件锁就是互斥锁+条件。咱们声明一个条件并初始化。
@property (assign, nonatomic) pthread_cond_t cond; //使用完后也要pthread_cond_destroy(&_cond); pthread_cond_init(&_cond, NULL);
在__remove
方法中
if (self.data.count == 0) { // 等待 pthread_cond_wait(&_cond, &_mutex); }
若是线程1率先拿到所并加锁,执行到上面代码这里发现数组中尚未数据,就执行pthread_cond_wait,此时线程1会暂时放开_mutex这个锁,并在这休眠等待。
线程2在__add
方法中最开始由于拿不到锁,因此等待,在线程1休眠放开锁以后拿到锁,加锁,并执行为数组添加数据的代码。添加完了以后会发个信号通知等待条件的线程,并解锁。
pthread_cond_signal(&_cond); pthread_mutex_unlock(&_mutex);
线程2执行了pthread_cond_signal
以后,线程1就收到了通知,退出休眠状态,继续执行下面的代码。
这个地方可能有人会有疑问,是否是线程2应该先unlock再cond_dingnal,其实这个地方顺序没有太大差异,由于线程2执行了pthread_cond_signal
以后,会继续执行unlock代码,线程1收到signal通知后会推出休眠状态,同时线程1须要再一次持有这个锁,就算此时线程2尚未unlock,线程1等到线程2 unlock 的时间间隔很短,等到线程2 unlock 后线程1会再去持有这个锁,并加锁。
NSCondition就是OC层面的条件锁,内部把mutex互斥锁和条件封装到了一块儿。NSConditionLock其实也差很少,NSConditionLock能够指定具体的条件,这两个OC层面的类的用法你们能够自行上网搜索。
@property (strong, nonatomic) dispatch_semaphore_t semaphore; //初始化 self.semaphore = dispatch_semaphore_create(5);
在初始化一个信号的的过程当中传入dispatch_semaphore_create
的值,其实就表明了容许几个线程同时访问。
再回到以前咱们存钱取钱这个例子。
self.moneySemaphore = dispatch_semaphore_create(1);
咱们一次只容许一个线程访问,因此在初始化的时候传1。下面就是使用方法。
- (void)__drawMoney { dispatch_semaphore_wait(self.moneySemaphore, DISPATCH_TIME_FOREVER); // ... 省略代码 dispatch_semaphore_signal(self.moneySemaphore); } - (void)__saveMoney { dispatch_semaphore_wait(self.moneySemaphore, DISPATCH_TIME_FOREVER); // ... 省略代码 dispatch_semaphore_signal(self.moneySemaphore); }
dispatch_semaphore_wait
是怎么上锁的呢?
若是信号量>0的时候,让信号量-1,并继续往下执行。
若是信号量<=0的时候,休眠等待。
就这么简单。
dispatch_semaphore_signal
让信号量+1。
小提示
在咱们平时使用这种方法的时候,能够把信号量的代码提取出来定义一个宏。
#define SemaphoreBegin \ static dispatch_semaphore_t semaphore; \ static dispatch_once_t onceToken; \ dispatch_once(&onceToken, ^{ \ semaphore = dispatch_semaphore_create(1); \ }); \ dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER); #define SemaphoreEnd \ dispatch_semaphore_signal(semaphore);
上面咱们讲到的线程同步方案都是每次只容许一个线程访问,在实际的状况中,读写的同步方案应该下面这样:
这就是多读单写,用于文件读写的操做。在咱们的iOS中能够用下面这两种解决方案。
这个读写锁的用法很简单,跟以前的普通互斥锁都差很少,你们随便搜一下应该就能搜到,我就不拿出来写了,这里主要是提一下这种锁,你们之后有须要的时候能够用。
首先在使用这个函数的时候,咱们要用本身建立的并发队列。
若是传入的是一个串行队列或者全局的并发队列,那dispatch_barrier_async
等同于dispatch_async
的效果。
self.queue = dispatch_queue_create("rw_queue", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(self.queue, ^{ [self read]; }); dispatch_barrier_async(self.queue, ^{ [self write]; });
在读取数据的时候,使用dispatch_async
往对列中添加任务,在写数据时,用dispatch_barrier_async
添加任务。
dispatch_barrier_async
添加的任务会等前面全部的任务都执行完,他再执行,并且他执行的时候,不容许有别的任务同时执行。
咱们都知道这个atomic是原子性的意思。他保证了属性setter和getter的原子性操做,至关于在set和get方法内部加锁。
atomic修饰的属性是读/写安全的,但不是线程安全。
假设有一个 atomic 的属性 "name",若是线程 A 调用 [self setName:@"A"],线程 B 调用 [self setName:@"B"],线程 C 调用 [self name],那么全部这些不一样线程上的操做都将依次顺序执行——也就是说,若是一个线程正在执行 getter/setter,其余线程就得等待。所以,属性 name 是读/写安全的。
可是,若是有另外一个线程 D 同时在调[name release],那可能就会crash,由于 release 不受 getter/setter 操做的限制。也就是说,这个属性只能说是读/写安全的,但并非线程安全的,由于别的线程还能进行读写以外的其余操做。线程安全须要开发者本身来保证。