iOS 多线程
进程与线程区别
A8处理器是一款双核处理器,A12处理器是6核CPU
进程是cpu资源分配的最小单位,线程是cpu调度的最小单位
一个程序至少有一个进程,一个进程至少有一个线程。线程依赖于进程才能运行
多线程实现主要是靠硬件CPU(中央处理器)件来实现的,CPU有一个很重要的特性时间片
- 缺点
多线程方案.png 队列和同步、异步的组合结果.png更多的线程意味着更多的内存开销。创建线程也是需要CPU开销的
如果线程比核的数量多,则同一时间只能执行与核数量相等的线程数,线程过多会导致频繁的切换,消耗过多的CPU时间,降低了程序性能
多线程就可能出现线程安全问题,为了解决线程安全需要使用锁,进而可能会出现死锁问题。过多的线程会增加程序设计的复杂性,浪费更多精力去处理多线程通信和数据共享
总结:只要是同步或者是主队列都不会创建子线程!!!
NSThread
使用NSThread需要自己管理线程生命周期
类方法:自动开启
[NSThread detachNewThreadSelector:@selector(timerThread) toTarget:self withObject:nil];
对象方法:需要手动开启
_eocThread = [[NSThread alloc] initWithTarget:self selector:@selector(timerThreadTwo) object:nil];
[_eocThread start];
self.block = ^{
dispatch_async(dispatch_get_global_queue(0, 0), ^{
#//线程内部调用这个代码 会使线程睡眠 暂停1秒
[NSThread sleepForTimeInterval:1];
NSLog(@"%@",weakSelf);
});
};
GCD
优点
- 多核并行运算
- 自动管理生命周期
- 简单高效
创建队列
dispatch_queue_create(@"标识", DISPATCH_QUEUE_SERIAL)//串行
dispatch_queue_create(@"标识", DISPATCH_QUEUE_CONCURRENT)//并行
dispatch_queue_t queue = dispatch_get_global_queue(0, 0)//获取全局并发队列
dispatch_queue_t queue = dispatch_get_main_queue()//主队列
注意:queue4和queue5并不是同一个对象,最好不要这么写
NSOperation
基于GCD,更加面向对象,但是处理简单任务会比GCD代码更多 ;
需要配合 NSOperationQueue 来实现多线程。因为默认情况下,NSOperation 单独使用时系统同步执行操作
NSOperation中所有的任务执行完finished == YES,并且任务的执行不是按照先进先出执行的,而是并发无序的执行!!!
优点
- 添加操作之间的依赖关系,方便的控制执行顺序
- 设定操作执行的优先级
- 可以很方便的取消一个操作的执行
- 使用 KVO 观察对操作执行状态的更改:isExecuteing、isFinished、isCancelled
NSOperationQueue
- 主队列: [NSOperationQueue mainQueue] 和GCD中的主队列一样,串行队列
- [[NSOperationQueue alloc] init] 非常特殊(同时具备并发和串行的功能) 默认情况下,非主队列是并发队列
- 操作队列通过设置 最大并发操作数(maxConcurrentOperationCount) 来控制并发、串行
- 不同于 GCD 中的调度队列 FIFO(先进先出)的原则。由操作之间相对的优先级决定
1. NSBlockOperation
operationQueue = [NSOperationQueue new];
blockOperation = [NSBlockOperation new];
[blockOperation addExecutionBlock:^{
NSLog(@"%@", [NSThread currentThread]);
sleep(1);
NSLog(@"one finish");
}];
[blockOperation addExecutionBlock:^{
NSLog(@"%@", [NSThread currentThread]);
sleep(1);
NSLog(@"two finish");
}];
operationQueue.maxConcurrentOperationCount = 4;//设置线程最大并发数
[operationQueue addOperation:blockOperation];
任务执行完毕后再次调用start方法,operation并不会再次执行,因为内部进行判断,当finish == YES 时不再执行
[blockOperation start];
自定义封装NSOperation
NSOperation只需要重写其中的main或start方法,在多线程执行任务的过程中需要注意线程安全问题,
我们还可以通过KVO监听isCancelled、isExecuting、isFinished等属性,确切的回调当前任务的状态
- 我们同时重写start和main方法时,start方法优先执行,main方法不会被执行;如果只重写main方法,则main方法会被执行。
- 因为isFinished是readonly属性,因此我们通过自定义变量taskFinished来重写isFinished的set、get方法,实现方式详见代码。
- NSOperationQueue的maxConcurrentOperationCount一般设置在5个以内,数量过多可能会有性能问题。maxConcurrentOperationCount为1时,队列中的任务串行执行,maxConcurrentOperationCount大于1时,队列中的任务并发执行;
- 不同的NSOperation实例之间可以设置依赖关系,不同queue的NSOperation之间也可以创建依赖关系 ,但是要注意不要“循环依赖”;
- NSOperation实例之间设置依赖关系应该在加入队列之前;
- 在没有使用 NSOperationQueue时,在主线程中单独使用 NSBlockOperation 执行(start)一个操作的情况下,操作是在当前线程执行的,并没有开启新线程,在其他线程中也一样;
- NSOperationQueue可以直接获取mainQueue,更新界面UI应该在mainQueue中进行;
区别自定义封装NSOperation时,重写main或start方法的不同;
自定义封装NSOperation时需要我们完全重载start,在start方法里面,我们还要查看isCanceled属性,确保start一个operation前,task是没有被取消的。如果我们自定义了dependency,我们还需要发送isReady的KVO通知。
死锁产生的条件
- sync
-
往当前串行队列添加任务
死锁.png
子线程保活.png
线程同步
锁的对象必须是唯一的同一个,原理:会判断这个操作有没有被别的线程加锁,如果有就一直等待,否则就加锁
-
OSSpinLock自旋锁
自旋锁.png
优先级反转就是低优先级的线程加锁之后,高优先级的线程一直处于忙等状态,系统把所有的时间都分给了高优先级的线程,所以没有时间处理低优先级的任务,造成低优先级的一直无法完成,高优先级的线程一直忙等
自旋锁的效率还是很高的,不过iOS10之后Apple不推荐使用,而是推荐os_unfair_lock
-
os_unfair_lock 也是一种互斥锁
os_unfair_lock.png -
pthread_mutex(互斥锁)
pthread_mutex.png - 递归锁 允许 同一个线程对一把锁重复加锁
递归锁.png - 条件锁 条件锁.png
-
信号量
信号量.png
@interface SemaphoreDemo()
@property (strong, nonatomic) dispatch_semaphore_t semaphore;
@end
@implementation SemaphoreDemo
//初始化
self.moneySemaphore = dispatch_semaphore_create(1);
//forever一直等待 信号量--1
dispatch_semaphore_wait(self.moneySemaphore, DISPATCH_TIME_FOREVER);
//信号量++1
dispatch_semaphore_signal(self.moneySemaphore);
@end
dispatch_semaphore_wait的声明为:
long dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout);
这个函数会使传入的信号量dsema的值减1;
这个函数的作用是这样的,如果dsema信号量的值大于0,该函数所处线程就继续执行下面的语句,并且将信号量的值减1;
如果desema的值为0,那么这个函数就阻塞当前线程等待timeout(注意timeout的类型为dispatch_time_t,
不能直接传入整形或float型数),如果等待的期间desema的值被dispatch_semaphore_signal函数加1了,且该函数(即dispatch_semaphore_wait)所处线程获得了信号量,那么就继续向下执行并将信号量减1。
如果等待期间没有获取到信号量或者信号量的值一直为0,那么等到timeout时,其所处线程自动执行其后语句。
//配置域名节点,因为项目的域名地址都要依赖这个,所以加一个信号量确保配置完成再往下走
dispatch_semaphore_t semap = dispatch_semaphore_create(0);
[[SFIMLocationDomainNodeManager sharedInstance] locationDomainNodeFinish:^(BOOL locationSuccess) {
if (!locationSuccess) {
DDLogInfo(@"连接所有域名节点都失败了,默认选择内地节点");
}
dispatch_semaphore_signal(semap);
}];
dispatch_time_t t = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC));
dispatch_semaphore_wait(semap, t);
NSLock、NSRecursiveLock、NSCondition、NSConditionLock、串行队列
参考
多线程之NSOperation
https://juejin.im/post/5c14bf1af265da613f2f5e90
https://juejin.im/post/5bf21d935188251d9e0c2937(多线程锁)
https://juejin.im/post/5a9e57af6fb9a028df222555 这个详细
https://www.cnblogs.com/yajunLi/p/6274282.html 信号量的使用
dispatch_group