目录
一、进程、线程、多线程
二、iOS中的多线程实现方案
1、pthread
2、NSThread
3、GCD
4、NSOperation
一、进程、线程、多线程
进程就是指操作系统上正在运行的应用程序。
线程就是指一段代码从头到尾的执行路径,具体地说我们编写的OC代码最终都会被编译成二进制代码供CPU执行,那么CPU在执行这些二进制代码时是从上往下一行一行串行执行的,当遇见if
语句或for
语句等控制语句时,CPU会偏离当前地址处的二进制代码去执行其它地址处的二进制代码,但执行完后又会返回来执行当前地址处的二进制代码,直到二进制代码执行完毕,这样一段代码从头到尾的执行路径就被称为一个线程。
而开辟一个新线程就是指把某段代码的执行路径和另一段代码的执行路径给完全独立开来,成为CPU的一个单独调度单位,所以当一个进程中有多个这样独立的代码的执行路径时,就是多线程。使用多线程的好处是可以提高程序的执行效率,使用多线程的坏处是开辟过多的线程会占用大量内存和CPU资源,而且还会存在数据竞争问题,因此通常开三到五个线程就差不多了。我们都知道一个CPU一次只能调度一个线程,那苹果是怎么实现多线程并发的呢?原来苹果会让CPU一会儿执行线程1,一会执行线程2,当线程之间的切换时间足够短时,就让我们感觉CPU是在一次执行多个线程,而现在设备都是多核的了,就不仅仅是“感觉”了,而是真得可以利用多个CPU来实现多线程并发。主线程主要用来显示/刷新UI界面和处理UI事件(如点击事件、滚动事件等),子线程主要用来做耗时操作。
二、iOS中的多线程实现方案
实现方案 | 简介 | 语言 | 线程生命周期 | 使用频率 |
---|---|---|---|---|
pthread | 是一套跨平台的多线程API,但使用难度较大 | C | 程序员管理 | 几乎不用 |
NSThread | 是对pthread的OC封装,使用更加面向对象,轻量级、灵活 | OC | 程序员管理 | 偶尔使用 |
GCD | 充分利用设备的多核,旨在替代NSThread等实现方案 | C | 自动管理 | 经常使用 |
NSOperation | 是对GCD的OC封装,使用更加面向对象,同时它提供了更加简单的API来实现线程同步和任务同步 | OC | 自动管理 | 经常使用 |
1、pthread
#import "PthreadViewController.h"
#import <pthread.h>
@interface PthreadViewController ()
@end
@implementation PthreadViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.title = @"pthread";
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
// // ❌主线程做耗时操作...
// for (int i = 0; i <= 10000; i++) {
// NSLog(@"%d---%@", i, [NSThread currentThread]);
// }
// 1、子线程对象
pthread_t pthread;
// 2、创建子线程
//
// 第一个参数:子线程对象的地址
// 第二个参数:子线程的属性,可以传个nil
// 第三个参数:子线程对应函数的指针
// 第四个参数:子线程对应函数的参数,可以传个nil
pthread_create(&pthread, nil, subThreadAction, nil);
}
void *subThreadAction(void *params) {
// ✅子线程做耗时操作...
for (int i = 0; i <= 10000; i++) {
NSLog(@"%d---%@", i, [NSThread currentThread]);
}
return nil;
}
@end
2、NSThread
#import "NSThreadViewController.h"
@interface NSThreadViewController ()
@end
@implementation NSThreadViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.title = @"NSThread";
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
// // ❌主线程做耗时操作...
// for (int i = 0; i <= 10000; i++) {
// NSLog(@"%d---%@", i, [NSThread currentThread]);
// }
[self createSubThread1];
// [self createSubThread2];
// [self createSubThread3];
}
/// 创建子线程的方式1
- (void)createSubThread1 {
// 1、创建子线程(此时线程处于新建状态)
//
// NSThread的生命周期是由我们程序员管理的,但我们也只需要负责创建线程就可以了
// 至于线程的销毁时机,它有点特别,你别看这里subThreadA是个局部变量,出了createSubThread1的作用域就会销毁,但是它指向的线程对象却不会销毁,线程的销毁时机是它内部的任务执行完后(此时线程处于销毁状态),也就是subThreadAction这个方法执行完后,系统会做这件事,我们不用管
// 当然我们也可以通过[NSThread exit]手动退出这个线程,也就是把这个线程销毁掉
NSThread *subThreadA = [[NSThread alloc] initWithTarget:self selector:@selector(subThreadAction) object:nil];
// 设置子线程的名字
subThreadA.name = @"我是线程A";
// 设置子线程的优先级(0~1,默认值0.5),可以改变CPU调度该子线程的概率
subThreadA.threadPriority = 1;
// 2、启动子线程(此时线程会被放入线程池,处于运行状态)
[subThreadA start];
NSThread *subThreadB = [[NSThread alloc] initWithTarget:self selector:@selector(subThreadAction) object:nil];
subThreadB.name = @"我是线程B";
subThreadB.threadPriority = 0.5;
[subThreadB start];
NSThread *subThreadC = [[NSThread alloc] initWithTarget:self selector:@selector(subThreadAction) object:nil];
subThreadC.name = @"我是线程C";
subThreadC.threadPriority = 0.25;
[subThreadC start];
}
/// 创建子线程的方式2
- (void)createSubThread2 {
// 创建子线程,会自动启动
[NSThread detachNewThreadSelector:@selector(subThreadAction) toTarget:self withObject:nil];
}
/// 创建子线程的方式3
- (void)createSubThread3 {
// 创建子线程,会自动启动
[self performSelectorInBackground:@selector(subThreadAction) withObject:nil];
}
- (void)subThreadAction {
// ✅子线程做耗时操作...
for (int i = 0; i <= 10000; i++) {
NSLog(@"%d---%@", i, [NSThread currentThread]);
}
}
@end
3、GCD
4、NSOperation
GCD里有两对儿重要的概念是同步追加任务/异步追加任务和队列,NSOperation是对GCD的OC封装,所以它也有两对儿重要的概念是同步追加任务/异步追加任务和队列。
4.1 异步追加
GCD里有dispatch_sync
和dispatch_async
之分,要想实现多线程开发就必须得用dispatch_async
,dispatch_sync
使用不当还会造成死锁。而NSOperation里没有同步追加,只有异步追加,只要我们创建一个任务,把任务追加到队列里时就是异步追加任务。
- 通过
selector
创建一个任务
NSInvocationOperation *operation = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(threadAction) object:nil];
- 通过
block
创建一个任务
NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
// 任务...
}];
- 把任务追加到队列中
[queue addOperation:operation];
4.2 并发队列和主队列
GCD里有串行队列和并发队列之分,如果想让多个任务并发执行,就必须得用并发队列,而如果想让多个任务串行执行,就必须得用串行队列。而NSOperation
里没有串行队列(当然主队列是个串行队列),只有并发队列(当然我们把并发队列的线程最大并发数设置为1时也能实现串行队列的效果),只要我们创建一个队列,它就是并发队列。
- 并发队列
// 创建队列
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
- 主队列
// 获取主队列
NSOperationQueue *mainQueue = [NSOperationQueue mainQueue];
4.3 简单使用
#import "ViewController.h"
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// 创建队列
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
for (int i = 0; i < 10; i++) {
// 创建任务
NSInvocationOperation *operation = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(threadAction) object:nil];
// 把任务追加到队列中
[queue addOperation:operation];
}
}
- (void)threadAction {
NSLog(@"执行任务,%@", [NSThread currentThread]);
}
@end
// 控制台打印:
执行任务,<NSThread: 0x600001909a00>{number = 6, name = (null)}
执行任务,<NSThread: 0x600001916080>{number = 7, name = (null)}
执行任务,<NSThread: 0x600001927000>{number = 5, name = (null)}
执行任务,<NSThread: 0x600001909a00>{number = 6, name = (null)}
执行任务,<NSThread: 0x600001927000>{number = 5, name = (null)}
执行任务,<NSThread: 0x600001916080>{number = 7, name = (null)}
执行任务,<NSThread: 0x600001925780>{number = 4, name = (null)}
执行任务,<NSThread: 0x600001909a00>{number = 6, name = (null)}
执行任务,<NSThread: 0x600001927000>{number = 5, name = (null)}
执行任务,<NSThread: 0x600001916080>{number = 7, name = (null)}
4.4 其它常用API
- 设置线程最大并发数,可以用来实现线程同步(等同于GCD里使用信号量设置线程最大并发数)
当我们设置NSOperationQueue的线程最大并发数为1时,这个队列就实现串行队列的效果了,需要注意的是设置线程最大并发数为1 != 只开一条线程,而是指线程同步。
#import "NSOperationViewController.h"
@interface NSOperationViewController ()
@end
@implementation NSOperationViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.title = @"NSOperation";
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
// 设置线程最大并发数
queue.maxConcurrentOperationCount = 1;
NSBlockOperation *op1 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"1---%@", [NSThread currentThread]);
}];
NSBlockOperation *op2 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"2---%@", [NSThread currentThread]);
}];
NSBlockOperation *op3 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"3---%@", [NSThread currentThread]);
}];
NSBlockOperation *op4 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"4---%@", [NSThread currentThread]);
}];
NSBlockOperation *op5 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"5---%@", [NSThread currentThread]);
}];
NSBlockOperation *op6 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"6---%@", [NSThread currentThread]);
}];
[queue addOperation:op1];
[queue addOperation:op2];
[queue addOperation:op3];
[queue addOperation:op4];
[queue addOperation:op5];
[queue addOperation:op6];
}
@end
// 控制台打印(可见开了两条线程,但线程是同步执行的):
1---<NSThread: 0x6000033382c0>{number = 3, name = (null)}
2---<NSThread: 0x60000332f000>{number = 5, name = (null)}
3---<NSThread: 0x6000033382c0>{number = 3, name = (null)}
4---<NSThread: 0x6000033382c0>{number = 3, name = (null)}
5---<NSThread: 0x6000033382c0>{number = 3, name = (null)}
6---<NSThread: 0x6000033382c0>{number = 3, name = (null)}
- 设置任务之间的依赖关系,可以用来实现任务同步(等同于GCD里使用队列组来控制任务的执行顺序,但要比GCD队列组强大得多得多)
NSOperation设置任务之间的依赖关系功能真得很强大,即便是不同队列里的任务也可以设置依赖关系,需要注意的是不要设置循环依赖就可以了。
#import "NSOperationViewController.h"
@interface NSOperationViewController ()
@end
@implementation NSOperationViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.title = @"NSOperation";
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
NSBlockOperation *op1 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"1---%@", [NSThread currentThread]);
}];
NSBlockOperation *op2 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"2---%@", [NSThread currentThread]);
}];
NSBlockOperation *op3 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"3---%@", [NSThread currentThread]);
}];
NSBlockOperation *op4 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"4---%@", [NSThread currentThread]);
}];
NSBlockOperation *op5 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"5---%@", [NSThread currentThread]);
}];
NSBlockOperation *op6 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"6---%@", [NSThread currentThread]);
}];
// 比如我们希望任务的执行顺序为:6、3、5、2、4、1,那么可以设置任务之间的依赖关系
[op3 addDependency:op6];
[op5 addDependency:op3];
[op2 addDependency:op5];
[op4 addDependency:op2];
[op1 addDependency:op4];
[queue addOperation:op1];
[queue addOperation:op2];
[queue addOperation:op3];
[queue addOperation:op4];
[queue addOperation:op5];
[queue addOperation:op6];
}
@end
// 控制台打印(可见任务是按我们指定的顺序执行的):
6---<NSThread: 0x6000011dd5c0>{number = 3, name = (null)}
3---<NSThread: 0x6000011de240>{number = 2, name = (null)}
5---<NSThread: 0x6000011dd5c0>{number = 3, name = (null)}
2---<NSThread: 0x6000011dd5c0>{number = 3, name = (null)}
4---<NSThread: 0x6000011de240>{number = 2, name = (null)}
1---<NSThread: 0x6000011dd5c0>{number = 3, name = (null)}
这里我们再解释一下线程同步和任务同步,举个例子:假设火车站出站口有100个人在等着打车回家,有10个站台,每个站台上等了10个人,出租车公司一共就安排了4辆出租车来拉这批人。那么我们可以把一辆出租车看成是一个线程,一个站台看成是一个队列,一个人看成是一个任务,也就是说我们开辟了4条线程来做事情,有10个队列,每个队列里塞了10个任务。
开始拉人了,出租车1从站台1把排第1位的人拉走,与此同时,出租车2从站台2把排第1位的人拉走,出租车3从站台3把排第1位的人拉走,出租车4从站台4把排第1位的人拉走,也就是说4辆车同时发射,几分钟后,出租车2先回来了,它又去站台5把排第1位的人拉走,又隔了几分钟,出租车4也回来了,它又去站台6把排第1位的人拉走,又隔了几分钟,出租车1也回来了,它又去站台7把排第1位的人拉走,又隔了几分钟,出租车3也回来了,它又去站台8把排第1位的人拉走,……,这就是多线程并发。
而我们所说的线程同步主要是指开辟了多个线程的情况下、同一时间只能有一个线程在执行任务,也就是说虽然安排了4辆车来拉人,但同一时间只能有一辆车真正在路上穿梭,下一辆车想拉人出发就必须得等上一辆车返回来之后才行,这也很简单,只要我们制定规则一次只能发出一辆车就好了,也就是设置线程最大并发数为1。这个用GCD和NSOperation都可以很简单地实现。
而任务同步则主要是指任务是按顺序执行的,当然这些任务可以是在同一个队列里也可以是在多个队列里,也就是说100个人你得按他们先出站的顺序一个一个地送到家。方案一:你可以把他们都安排在一个站台上排队,也就是只开一个队列,而且这个队列得是个串行队列,然后你可以只开一个线程,也就是只安排一辆车来拉他们,这样必然就是一个一个地按顺序送到家,当然你也可以开多个线程 + 线程同步来拉他们,这样也能完成任务同步,这个用GCD和NSOperation都可以很简单地实现。方案二:你可以把他们安排在10个站台上等,但是你得有一种机制知道第二个站台上的第一个人应该在第一个站台最后一个人后面(即依赖关系),这样才能保证车不优先去二站台拉人,然后同样你可以只开一个线程,也就是只安排一辆车来拉他们,这样必然就是一个一个地按顺序送到家,当然你也可以开多个线程 + 线程同步来拉他们,这样也能完成任务同步,这个就得用NSOperation来实现了,只有它才可以设置跨队列之间任务的依赖关系。
可见线程同步的实现很简单,就是在开辟多个线程的情况下,设置一下线程的最大并发数为1就行了(当然还有加锁、把多个线程放到串行队列里等其它方案),用GCD和NSOperation都行。
而任务同步的实现则比较复杂,你得首先把任务放在一个或多个都可以串行队里,然后决定开辟一个线程或开辟多个线程 + 线程同步来实现,当遇到需要做任务同步的场景时推荐使用NSOperation。