多线程的原理
CPU在不同的线程之间以一个非常小的时间片不断的进行调度。
多线程的实现原理:事实上,同一时间内单核的CPU只能执行一个线程,多线程是CPU快速的在多个线程之间进行切换(调度),造成了多个线程同时执行的假象。
如果是多核CPU就真的可以同时处理多个线程了。
多线程的目的是为了同步完成多项任务,通过提高系统的资源利用率来提高系统的效率。
那么如果线程非常非常多,会发生什么情况?
CPU会在N多线程之间调度,CPU会累死,消耗大量的CPU资源,同时每条线程被调度执行的频次也会会降低(线程的执行效率降低)。
因此我们一般只开3-5条线程。
多线程技术方案
1、pthread
- 一套通用的多线程API
- 适用于Unix/Linux/Windows等系统
- 跨平台/可移植
- 使用难度大
- C语言,需要手动管理线程生命周期
2、NSThread
- 使用更加面向对象
- 简单易用,可直接操作线程对象
- OC语言,需要手动管理线程对象
3、GCD
- 苹果为多核并行运算开发的解决方案
- 会自动利用更多的CPU内核(比如双核、四核)
- 程序员只需要将任务添加到队列中,不需要编写任何线程管理代码
- OC语言,自动管理生命周期
4、NSOperation
- 基于GCD,但是比GCD多了一些简单实用的功能
- 使用更加面向对象
- 可添加完成的代码块,在操作完成之后执行
- 添加操作之间的依赖关系,方便的控制执行顺序
- 可以设定操作执行的优先级
- 可以很方便的取消一个操作的执行
- 使用KVO观察对操作执行状态的更改:isExecuteing、isFinished、isCancelled
- OC语言,手动管理生命周期
三种多线程技术比较
1、NSThread 优点:NSThread 比其他两个轻量级,使用简单
缺点:需要自己管理线程的生命周期、线程同步、加锁、睡眠以及唤醒等。线程同步对数据的加锁会有一定的系统开销
2、GCD: 是iOS 4.0以后才出现的并发技术。使用方式:将任务添加到队列(串行/并行(全局)),指定执行任务的方法(同步(阻塞)/异步 )拿到主队列:dispatch_get_main_queu()
NSOperation无法做到的:1.一次性执行,2.延迟执行,3.调度组(op实现要复杂的多 )
3、NSOperation: iOS2.0的时候就出现了(当时不好用,后来苹果对其进行改造)。
使用方式:将操作(异步执行)添加到队列(并发/全局)
拿到主队列:[NSOperationQueue mainQueue] 主队列,任务添加到主队列就会在主线程执行
提供了GCD不好实现的:1.最大并发数,2.暂停和继续,3.取消所有任务,4.依赖关系
GCD是比较底层的封装,我们知道较低层的代码一般性能都是比较高的,相对于NSOperationQueue。
所以追求性能,而功能够用的话就可以考虑使用GCD。
如果任务之间没有什么依赖关系,而是需要更高的并发能力,GCD则更有优势。
如果异步操作的过程需要更多的用户交互和被UI显示出来,NSOperationQueue会是一个好选择。
多程优缺点
多线程的优点:
能适当提高程序的执行效率
能适当提高资源利用率(CPU、内存利用率)
多线程的缺点:
创建线程是有开销的,iOS下主要成本包括:内核数据结构(大约1KB)、栈空间(子线程512KB、主线程1MB,也可以使用-setStackSize:设置,但必须是4K的倍数,而且最小是16K),创建线程大约需要90毫秒的创建时间
如果开启大量的线程,会降低程序的性能,线程越多,CPU在调度线程上的开销就越大。
程序设计更加复杂:比如线程之间的通信、多线程的数据共享等问题。
多线程的应用
主线程的主要作用
显示\刷新UI界面
处理UI事件(比如点击事件、滚动事件、拖拽事件等)
主线程的使用注意
别将比较耗时的操作放到主线程中
耗时操作会卡住主线程,严重影响UI的流畅度,给用户一种“卡”的坏体验
将耗时操作放在子线程中执行,提高程序的执行效率
线程生命周期
线程的生命周期分为5个部分,新建、就绪、运行、阻塞、死亡。
- 新建:实例化线程对象
- 就绪:向线程对象发送start消息,线程对象被加入可调度线程池等待CPU调度
- 运行:CPU负责调度可调度线程池中线程执行。当前线程任务执行完毕后,进入就绪状态,即有一个短暂停留期,如果在这期间有新的任务,则继续执行这个任务,如果没有新的任务,则线程销毁,等有下一个任务的时候,则开启新的线程执行。
- 阻塞:当满足某个预定条件时,可以使用休眠或锁,阻塞线程执行。sleepForTimeInterval(休眠指定时长),
sleepUntilDate(休眠到指定日期),
@synchronized(self):(互斥锁)。 - 死亡:正常死亡,线程执行完毕。
非正常死亡,在线程内部中止执行(如[NSThread exit] ),
在线程外死亡,当满足某个条件后,在线程内部中止执行或者在主线程中止线程对象。
线程的exit、cancel和suspend操作区别。
- exit:一旦强行终止线程,后续的所有代码都不会被执行
- cancel:并不会直接取消线程,只是给线程对象添加isCancelled标记
- suspend:挂起状态,通过resume,可以恢复线程的任务执行
线程池的实现原理
多线程的锁
1.为什么要线程安全
2.多线程安全隐患分析
3.多线程安全隐患的解决方案
4.锁的分类-13种锁
4.1.1OSSpinLock
4.1.2os_unfair_lock
4.1.3pthread_mutex
4.1.4NSLock
4.1.5NSRecursiveLock
4.1.6NSCondition
4.1.7NSConditionLock
4.1.8dispatch_semaphore
4.1.9dispatch_queue
4.1.10@synchronized
4.1.11atomic
4.1.12pthread_rwlock
4.1.13dispatch_barrier_async
5.性能对比
6.常见的死锁
1.为什么要线程安全
多个线程访问同一块资源的时候,很容易引发数据混乱问题。
2.多线程安全隐患分析
3.多线程安全隐患的解决方案
4.13种锁
1 OSSpinLock - 自旋锁
它是为实现保护共享资源而提出一种锁机制。对于互斥锁,如果资源已经被占用,资源申请者只能进入睡眠状态。但是自旋锁不会引起调用者睡眠,如果自旋锁已经被别的执行单元保持,调用者就一直循环在那里看是否该自旋锁的保持者已经释放了锁iOS10.0以后就被弃用了,可以使用os_unfair_lock_lock替代
2 os_unfair_lock
从iOS10开始才支持 , os_unfair_lock锁的线程会处于休眠状态,并非忙等
3 pthread_mutex
pthread除了创建互斥锁,还可以创建递归锁、读写锁、once等锁。
- mutex叫做”互斥锁”,等待锁的线程会处于休眠状态
4 NSLock
- NSLock是对mutex普通锁的封装。pthread_mutex_init(mutex, NULL);
- NSLock 遵循 NSLocking 协议。Lock 方法是加锁,unlock 是解锁,tryLock 是尝试加锁,如果失败的话返回 NO,lockBeforeDate: 是在指定Date之前尝试加锁,如果在指定时间之前都不能加锁,则返回NO
5 NSRecursiveLock(递归锁)
NSRecursiveLock是对mutex递归锁的封装,API跟NSLock基本一致
6 NSCondition(条件锁)
NSCondition 的对象实际上作为一个锁和一个线程检查器:锁主要为了当检测条件时保护数据源,执行条件引发的任务;线程检查器主要是根据条件决定是否继续运行线程,即线程是否被阻塞。
NSCondition是对mutex和cond的封装,更加面向对象,我们使用起来也更加的方便简洁
7 NSConditionLock
- lock不分条件,如果锁没被申请,直接执行代码
- unlock不会清空条件,之后满足条件的锁还会执行
- unlockWithCondition:我的理解就是设置解锁条件(同一时刻只有一个条件,如果已经设置条件,相当于修改条件)
- lockWhenCondition:满足特定条件,执行相应代码
- NSConditionLock同样实现了NSLocking协议,试验过程中发现性能很低。
- NSConditionLock也可以像NSCondition一样做多线程之间的任务等待调用,而且是线程安全的。
NSConditionLock是对NSCondition的进一步封装,可以设置具体的条件值
8 dispatch_semaphore
- semaphore叫做”信号量”
- 信号量的初始值,可以用来控制线程并发访问的最大数量
- 信号量的初始值为1,代表同时只允许1条线程访问资源,保证线程同步
9 dispatch_queue
使用GCD的串行队列也可以实现线程同步的
10 @synchronized(互斥锁)
在编程中,引入对象互斥锁的概念,来保证共享数据操作的完整性。每个对象都对应于一个可称为“互斥锁”的标记,这个标记用来保证在任一时刻,只能有一个线程访问对象。
@synchronized是对mutex递归锁的封装, @synchronized(obj)内部会生成obj对应的递归锁,然后进行加锁、解锁操作
11 atomic
- atomic用于保证属性setter、getter的原子性操作,相当于在getter和setter内部加了线程同步的锁
- 它并不能保证使用属性的过程是线程安全的
12 pthread_rwlock
pthread_rwlock经常用于文件等数据的读写操作,需要导入头文件#import <pthread.h>
13 dispatch_barrier_async
这个函数传入的并发队列必须是自己通过dispatch_queue_cretate创建的 如果传入的是一个串行或是一个全局的并发队列,那这个函数便等同于dispatch_async函数的效果
6.常见的死锁
案例一:
NSLog(@"1"); // 任务1
dispatch_sync(dispatch_get_main_queue(), ^{
NSLog(@"2"); // 任务2
});
NSLog(@"3"); // 任务3
结果,控制台输出:
1
分析
- dispatch_sync表示一个同步线程;
- dispatch_get_main_queue表示运行在主线程中的主队列;
- 任务2是同步线程的任务。
首先执行任务1,这是肯定没问题的,只是接下来,程序遇到了同步线程,那么它会进入等待,等待任务2执行完,然后执行任务3。但这是队列,有任务来,当然会将任务加到队尾,然后遵循FIFO原则执行任务。那么,现在任务2就会被加到最后,任务3排在了任务2前面,问题来了:
任务3要等任务2执行完才能执行,任务2由排在任务3后面,意味着任务2要在任务3执行完才能执行,所以他们进入了互相等待的局面。【既然这样,那干脆就卡在这里吧】这就是死锁。
案例二
NSLog(@"1"); // 任务1
dispatch_sync(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
NSLog(@"2"); // 任务2
});
NSLog(@"3"); // 任务3
结果,控制台输出:
1
2
3
分析
首先执行任务1,接下来会遇到一个同步线程,程序会进入等待。等待任务2执行完成以后,才能继续执行任务3。从dispatch_get_global_queue可以看出,任务2被加入到了全局的并行队列中,当并行队列执行完任务2以后,返回到主队列,继续执行任务3。
案例三
dispatch_queue_t queue = dispatch_queue_create("com.demo.serialQueue", DISPATCH_QUEUE_SERIAL);
NSLog(@"1"); // 任务1
dispatch_async(queue, ^{
NSLog(@"2"); // 任务2
dispatch_sync(queue, ^{
NSLog(@"3"); // 任务3
});
NSLog(@"4"); // 任务4
});
NSLog(@"5"); // 任务5
结果,控制台输出:
1
5
2
// 5和2的顺序不一定
分析
这个案例没有使用系统提供的串行或并行队列,而是自己通过dispatch_queue_create函数创建了一个DISPATCH_QUEUE_SERIAL的串行队列。
- 执行任务1;
- 遇到异步线程,将【任务2、同步线程、任务4】加入串行队列中。因为是异步线程,所以在主线程中的任务5不必等待异步线程中的所有任务完成;
- 因为任务5不必等待,所以2和5的输出顺序不能确定;
- 任务2执行完以后,遇到同步线程,这时,将任务3加入串行队列;
- 又因为任务4比任务3早加入串行队列,所以,任务3要等待任务4完成以后,才能执行。但是任务3所在的同步线程会阻塞,所以任务4必须等任务3执行完以后再执行。这就又陷入了无限的等待中,造成死锁。
案例四
NSLog(@"1"); // 任务1
dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSLog(@"2"); // 任务2
dispatch_sync(dispatch_get_main_queue(), ^{
NSLog(@"3"); // 任务3
});
NSLog(@"4"); // 任务4
});
NSLog(@"5"); // 任务5
结果,控制台输出:
1
2
5
3
4
// 5和2的顺序不一定复制代码
分析
- 首先,将【任务1、异步线程、任务5】加入Main Queue中,异步线程中的任务是:【任务2、同步线程、任务4】。
- 所以,先执行任务1,然后将异步线程中的任务加入到Global Queue中,因为异步线程,所以任务5不用等待,结果就是2和5的输出顺序不一定。
- 然后再看异步线程中的任务执行顺序。任务2执行完以后,遇到同步线程。将同步线程中的任务加入到Main Queue中,这时加入的任务3在任务5的后面。
- 当任务3执行完以后,没有了阻塞,程序继续执行任务4。
- 从以上的分析来看,得到的几个结果:1最先执行;2和5顺序不一定;4一定在3后面。
案例五
dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSLog(@"1"); // 任务1
dispatch_sync(dispatch_get_main_queue(), ^{
NSLog(@"2"); // 任务2
});
NSLog(@"3"); // 任务3
});
NSLog(@"4"); // 任务4
while (1) {
}
NSLog(@"5"); // 任务5
结果,控制台输出:
1
4
// 1和4的顺序不一定复制代码
分析
- 和上面几个案例的分析类似,先来看看都有哪些任务加入了Main Queue:【异步线程、任务4、死循环、任务5】。
- 在加入到Global Queue异步线程中的任务有:【任务1、同步线程、任务3】。
第一个就是异步线程,任务4不用等待,所以结果任务1和任务4顺序不一定。 - 任务4完成后,程序进入死循环,Main Queue阻塞。但是加入到Global Queue的异步线程不受影响,继续执行任务1后面的同步线程。
- 同步线程中,将任务2加入到了主线程,并且,任务3等待任务2完成以后才能执行。这时的主线程,已经被死循环阻塞了。所以任务2无法执行,当然任务3也无法执行,在死循环后的任务5也不会执行。
- 最终,只能得到1和4顺序不定的结果。
总结
- 总的来看,推荐pthread_mutex作为实际项目的首选方案;
- 对于耗时较大又易冲突的读操作,可以使用读写锁代替pthread_mutex;
- 如果确认仅有set/get的访问操作,可以选用原子操作属性;
- 对于性能要求苛刻,可以考虑使用OSSpinLock,需要确保加锁片段的耗时足够小;
- 条件锁基本上使用面向对象的NSCondition和NSConditionLock即可;
- @synchronized则适用于低频场景如初始化或者紧急修复使用;
苹果为多线程、共享内存提供了多种同步解决方案(锁),对于这些方案的比较,大都讨论了锁的用法以及锁操作的开销。个人认为最优秀的选用还是看应用场景,高频接口VS低频接口、有限冲突VS激烈竞争、代码片段耗时的长短,都是选择的重要依据,选择适用于当前应用场景的方案才是王道。