使用多线程是每个程序员必须要掌握的。然而使用多线程的时候,如果不加注意就会产生很多比较难排查的bug。所以要对多线程有深入的理解才行。比如可变数组和可变字典是线程安全的吗?本身就是异步执行的任务,如何等待真正的结果返回之后才继续后面的事情?在线程中睡眠,睡醒之后当前类已经释放,self会为nil吗?等等,在多线程的实际使用过程中会有很多出现,因此需要把基础打牢,也要有更深的理解。
首先从最基本的定义说起
什么是进程?
进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体。
狭义定义:进程是正在运行的程序的实例(an instance of a computer program that is being executed)。
上面是百科的部分定义,简单来说,每个应用启动之后,都对应着一个进程。打开活动监视器即可看到,第一列就是进程名称,每一个程序对应一个进程,每个进程都有一个唯一标示PID,即进程ID。
进程概念主要有两点:
- 进程是一个实体。每个进程都有自己的地址空间,一般情况下包括 文本区域text region、数据区域data region 和堆栈stack region。
- 进程是一个执行中的程序。程序是一个没有生命的实体,只有程序执行的时候,它才能成为一个活动的实体。我们称它为进程。
除去进程的新建和终止,运行中的进程具有三种基本状态:
运行中的进程状态- 就绪:进程已经获得除处理器外的所需资源,等待分配处理器资源。只要分配了处理器进程就可以执行。
- 运行:进行占用处理器资源,出于此状态的运行数小于等于处理器数
- 阻塞:由于进程等待某种条件,比如I/O操作或者进程同步,在条件满足之前无法继续执行
什么是线程?
线程是程序执行的最小单元。一个标准的线程由线程ID,当前指令指针,寄存器集合和堆栈组成。线程是进程中的一个实体。线程也具有就绪,阻塞和运行三种基本状态。
通常一个进行中可以包含若干个线程,它们可以利用进程所拥有的全部资源。进程是分配资源的基本单位。而线程则是独立运行和调度的基本单位。由于线程比进程更小,基本上不拥有系统资源,只拥有一点儿在运行中必不可少的资源,但它可与同属一个进程的其他线程共享进程所拥有的全部资源。
线程是进程的基本执行单元,进程的所有任务都是在线程中执行的。
多线程的实现原理
先说下任务执行有两种方式,串行和并行。
串行:任务一个一个执行,所需时间是所有任务执行完成之和。
并行:任务并发执行,所需时间是耗时最久的任务完成时间。
任务的串行于行与线程并没有必然的联系。串行并非就只有一个线程,也可以有多个线程。并行必然有多个线程。
对于单核操作系统,同一时间只有一个线程在执行。每个线程都分配有时间片进行执行,然后切换到其他线程。从宏观来看是并行的,从微观上来看,是串行的。
对于多核操作系统,就真正实现了并行执行任务。在同一时间可以有多个线程在执行任务。
多线程的场景使用场景:
在iOS系统中,UIKit中的所有操作都是在主线程中执行的,包括用户的触摸事件等。如果在主线程执行耗时操作就会造成卡顿等现象,影响用户体验。常见的耗时操作有:
-
网络请求
-
图片加载
-
文件处理
-
数据存储
-
多任务
常见实现方式
- pThread --c 语言
- NSThread -- oc对象
- GCD -- c语言
- NSOpreation -- oc对象
pThread 用的是一套c语言库,在iOS开发中使用的不多。
使用时需要先导入头文件#import <pthread.h>
使用时创建线程,并指定执行的方法即可
- (IBAction)pThreadClicked:(UIButton *)sender {
NSLog(@"主线程事件");
pthread_t pthread;
pthread_create(&pthread, NULL, run, NULL);
}
void *run(){
NSLog(@"run方法执行");
sleep(1);
NSLog(@"执行结束");
return NULL;
}
// 打印结果4540是进程id,后面的是线程id,打印结果可以看出run在子线程中执行。
2019-01-03 17:22:26.260893+0800 ThreadTest[4540:1088694] 主线程事件
2019-01-03 17:22:26.262011+0800 ThreadTest[4540:1089164] run方法执行
2019-01-03 17:22:27.263254+0800 ThreadTest[4540:1089164] 执行结束
因为phread不常用,所以也不做多做介绍,使用的时候时候看一下官方的api就可以了。
NSThread 的三种创建方式
// 1. 对象方式,可以获取到线程对象,需要手动执行start方法,当然也方便设置其他属性,比如优先级,比如线程名字等。
- (instancetype)initWithTarget:(id)target selector:(SEL)selector object:(nullable id)argument API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
- (instancetype)initWithBlock:(void (^)(void))block API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));
- (IBAction)NSTreadClick:(UIButton *)sender {
NSLog(@"主线程");
NSThread *thread = [[NSThread alloc]initWithBlock:^{
sleep(1);
NSLog(@"子线程");
}];
thread.name = @"thread1";
thread.threadPriority = 1.0;
[thread start];
}
// 打印结果
2019-01-03 17:44:14.421668+0800 ThreadTest[4668:1122856] 主线程
2019-01-03 17:44:15.423002+0800 ThreadTest[4668:1123018] 子线程
// 2. 类方法 不能直接获取线程对象 不过可以在线程执行过程中使用类方法currentThread获取当前线程
+ (void)detachNewThreadWithBlock:(void (^)(void))block API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));
+ (void)detachNewThreadSelector:(SEL)selector toTarget:(id)target withObject:(nullable id)argument;
// 3. NSObject的分类方法,不能直接获取线程对象
- (void)performSelectorInBackground:(SEL)aSelector withObject:(nullable id)arg API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
GCD
以下面的代码为例
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
...
});
// async 异步
// get_global_queue 并发
// ^{ ... } 执行的任务
GCD的使用需要告诉gcd三个东西
- 同步 还是 异步:同步会阻塞当前线程,异步不会阻塞。
- 在哪个队列:队列分为串行和并行,即需要一个一个执行还是需要并发执行。
- 任务是什么: block中的东西即是要执行的任务。
主队列:dispatch_get_main_queue() 串行队列
全局队列: dispatch_get_global_queue(<#long identifier#>, <#unsigned long flags#>) 并发队列
举个异步执行任务回到主线程刷新的例子:
- (IBAction)gcdClick:(UIButton *)sender {
NSLog(@"用户点击事件在主线程");
// 在自线程执行任务
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"start...");
[NSThread sleepForTimeInterval:3];
NSLog(@"end...");
// 回到主线程 刷新UI
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@"回到主线程刷新UI");
});
});
}
// 打印 可以通过线程id区分是不是主线程,也可以打印当前线程
2019-01-03 19:14:14.841561+0800 ThreadTest[4789:1195593] 用户点击事件在主线程
2019-01-03 19:14:14.841752+0800 ThreadTest[4789:1195941] start...
2019-01-03 19:14:17.845246+0800 ThreadTest[4789:1195941] end...
2019-01-03 19:14:17.845676+0800 ThreadTest[4789:1195593] 回到主线程刷新UI
获取全局队列时,第一个参数是设置优先级的,第二个是预留参数,暂时没有用。优先级越高,先执行的概率越大。
进行串行执行,串行还是并发和同步异步没有关系,也就是和线程没有必然的联系,我们举个例子来说明这一点,同步因为没有开启线程必然是串行的,但异步如果指定了串行队列,也会一个一个执行。
同步的串行:
// 同步 -- 串行
NSLog(@"当前主线程");
dispatch_queue_t serial = dispatch_queue_create("com.test.gcd", DISPATCH_QUEUE_SERIAL);
dispatch_sync(serial, ^{
NSLog(@"sync-task1");
});
dispatch_sync(serial, ^{
NSLog(@"sync-task2");
});
dispatch_sync(serial, ^{
NSLog(@"sync-task3");
});
异步的串行:
//异步 -- 串行
dispatch_async(serial, ^{
NSLog(@"async-task1");
});
dispatch_async(serial, ^{
NSLog(@"async-task2");
});
dispatch_async(serial, ^{
NSLog(@"async-task3");
});
上述两者的打印结果如下:
2019-01-03 19:28:09.275753+0800 ThreadTest[4836:1220108] 当前主线程
2019-01-03 19:28:09.276001+0800 ThreadTest[4836:1220108] sync-task1
2019-01-03 19:28:09.276151+0800 ThreadTest[4836:1220108] sync-task2
2019-01-03 19:28:09.276287+0800 ThreadTest[4836:1220108] sync-task3
2019-01-03 19:28:09.276452+0800 ThreadTest[4836:1220151] async-task1
2019-01-03 19:28:09.276656+0800 ThreadTest[4836:1220151] async-task2
2019-01-03 19:28:09.277657+0800 ThreadTest[4836:1220151] async-task3
上述结果说明:
- 同步时,没有开启线程。
- 异步时,开启了线程,因为是串行,所以只开了一个线程。
- 串行是为了保证任务的顺序执行,并行是为了保证任务的并发执行,串行没有创建新线程,并行会根据需要至少创建一个线程。
关于同步&异步。串行&并行 可以列个象限图
有四种组合:
-
同步-串行:同步会阻塞当前线程,因此不会创建线程,也可以保证任务同步执行
-
同步-并发:同步会阻塞当前线程,所以不会创建线程,所以无法实现并发,实际上还是串行。
-
异步-串行:异步不会阻塞当前线程,会创建新线程,为保证串行,一般创建一个线程就够了。
-
异步-并发:异步不会阻塞当前线程,会创建新线程,为保证并发,一般会创建多个线程。
这里重点说明一下同步-并发 其实因为同步会阻塞线程所以不能并发
// 同步-并发,实际无法实现并发,依然是串行执行
NSLog(@"当前主线程");
dispatch_queue_t async = dispatch_queue_create("com.test.async", DISPATCH_QUEUE_CONCURRENT);
for (int i = 0; i < 8; i++) {
dispatch_sync(async, ^{
NSLog(@"sync_concurrent_task%d",i);
});
}
// 打印结果 并没有创建新的线程,所以并不是并发执行的。
2019-01-03 19:44:25.405840+0800 ThreadTest[4888:1241863] 当前主线程
2019-01-03 19:44:25.406140+0800 ThreadTest[4888:1241863] sync_concurrent_task0
2019-01-03 19:44:25.406288+0800 ThreadTest[4888:1241863] sync_concurrent_task1
2019-01-03 19:44:25.406429+0800 ThreadTest[4888:1241863] sync_concurrent_task2
2019-01-03 19:44:25.406534+0800 ThreadTest[4888:1241863] sync_concurrent_task3
2019-01-03 19:44:25.406634+0800 ThreadTest[4888:1241863] sync_concurrent_task4
2019-01-03 19:44:25.406756+0800 ThreadTest[4888:1241863] sync_concurrent_task5
2019-01-03 19:44:25.407134+0800 ThreadTest[4888:1241863] sync_concurrent_task6
2019-01-03 19:44:25.407561+0800 ThreadTest[4888:1241863] sync_concurrent_task7
// 任务组 — 用来一组任务结束之后,再执行其他操作,组任务结束之后会调用notify方法
NSLog(@"当前主线程");
dispatch_group_t group = dispatch_group_create();
dispatch_queue_t group_queue = dispatch_queue_create("com.test.group", DISPATCH_QUEUE_CONCURRENT);
dispatch_group_async(group, group_queue, ^{
[NSThread sleepForTimeInterval:2];
NSLog(@"group task 1");
});
dispatch_group_async(group, group_queue, ^{
[NSThread sleepForTimeInterval:2];
NSLog(@"group task 2");
});
dispatch_group_async(group, group_queue, ^{
[NSThread sleepForTimeInterval:2];
NSLog(@"group task 3");
});
dispatch_group_notify(group, group_queue, ^{
NSLog(@"all task done");
});
// 打印 启用了三个线程 任务等待完成之后执行。
2019-01-03 19:55:20.669903+0800 ThreadTest[4924:1260901] 当前主线程
2019-01-03 19:55:22.674533+0800 ThreadTest[4924:1260946] group task 2
2019-01-03 19:55:22.674533+0800 ThreadTest[4924:1260947] group task 1
2019-01-03 19:55:22.674533+0800 ThreadTest[4924:1260945] group task 3
2019-01-03 19:55:22.674753+0800 ThreadTest[4924:1260946] all task done
需要注意的是:以上每一个任务本身就是同步的,如果任务本身就是异步的,每个任务很快就会执行,可能获取不到我们想要的结果。比如我们模拟一个异步请求。
// 任务组
dispatch_group_t group = dispatch_group_create();
dispatch_queue_t group_queue = dispatch_queue_create("com.test.group", DISPATCH_QUEUE_CONCURRENT);
dispatch_group_async(group, group_queue, ^{
[self requestOneInfo:^{
NSLog(@"one info done");
}];
});
dispatch_group_async(group, group_queue, ^{
[self requestOtherInfo:^{
NSLog(@"other info done");
}];
});
dispatch_group_notify(group, group_queue, ^{
NSLog(@"all task done");
});
// 打印结果 很明显不是我们想要的
2019-01-03 20:07:51.231103+0800 ThreadTest[4950:1281306] 当前主线程
2019-01-03 20:07:51.231406+0800 ThreadTest[4950:1281362] get OneInfo start
2019-01-03 20:07:51.231426+0800 ThreadTest[4950:1281360] all task done
2019-01-03 20:07:51.231414+0800 ThreadTest[4950:1281359] get OtherInfo start
2019-01-03 20:07:53.234123+0800 ThreadTest[4950:1281359] get OtherInfo end
2019-01-03 20:07:53.234130+0800 ThreadTest[4950:1281362] get OneInfo end
2019-01-03 20:07:53.234489+0800 ThreadTest[4950:1281359] other info done
2019-01-03 20:07:53.234491+0800 ThreadTest[4950:1281362] one info done
以上结果很明显不是我们想要的,就是因为任务本身是异步执行的,任务组的任务很快就结束了,真正的任务并没有结束。这个时候,我们需要使用 enter 和 leave , enter 和 leave要成对出现。
我们修改一下代码,让任务组可以执行预期的异步操作
// 任务组
dispatch_group_t group = dispatch_group_create();
dispatch_queue_t group_queue = dispatch_queue_create("com.test.group", DISPATCH_QUEUE_CONCURRENT);
dispatch_group_enter(group);
dispatch_group_async(group, group_queue, ^{
[self requestOneInfo:^{
NSLog(@"one info done");
dispatch_group_leave(group);
}];
});
dispatch_group_enter(group);
dispatch_group_async(group, group_queue, ^{
[self requestOtherInfo:^{
NSLog(@"other info done");
dispatch_group_leave(group);
}];
});
dispatch_group_notify(group, group_queue, ^{
NSLog(@"all task done");
});
// 打印,符合预期
2019-01-03 20:13:05.609618+0800 ThreadTest[4963:1290616] 当前主线程
2019-01-03 20:13:05.609897+0800 ThreadTest[4963:1290656] get OneInfo start
2019-01-03 20:13:05.609923+0800 ThreadTest[4963:1290654] get OtherInfo start
2019-01-03 20:13:07.615273+0800 ThreadTest[4963:1290656] get OneInfo end
2019-01-03 20:13:07.615284+0800 ThreadTest[4963:1290654] get OtherInfo end
2019-01-03 20:13:07.615583+0800 ThreadTest[4963:1290654] other info done
2019-01-03 20:13:07.615583+0800 ThreadTest[4963:1290656] one info done
2019-01-03 20:13:07.615914+0800 ThreadTest[4963:1290654] all task done
NSOpreation
是GCD的一种封装,需要使用子类。
任务队列:NSOpreationQueue 相当于一个线程池的概念,可以添加任务,设置最大并发数。
任务有几种状态:ready,canceld,executing,finished,asynchronous
任务可以很方便的添加依赖
我们可以使用系统提供了两个子类创建NSOpreaion通过调用任务的start方法启动任务,会在当前的线程同步执行。
如果我们需要异步执行,通过创建队列,把任务添加到队列即可。
NSBlockOperation *op = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"任务执行了");
}];
[op start];// 在当前线程同步执行
NSOperationQueue *queue = [[NSOperationQueue alloc]init];
[queue addOperation:op];// 异步执行
如果任务需要设置依赖关系,调用任务的方法
-(void*)addDependency:(NSOperation *)op;
如果我们需要等待所有任务完成,调用队列的方法
-(void*)waitUntilAllOperationsAreFinished;
注意事项:
线程之间共用进程所有资源,当多线程操作同一个变量的时候,可能会使得结果不正确。
因此要特别注意线程安全的问题。
通常保证线程安全有很多种方式
- 使用线程锁
- 使用串行队列
- 使用线程安全的类
- 使用信号量或runloop使异步看起来像同步在执行
- 注意任务可能本身就是异步的
网友评论