新博客wossoneri.com
进程和线程
进程
是指在系统中正在运行的一个应用程序。
每个进程之间是独立的,每个进程均运行在其专用且受保护的内存空间内。
比如同时打开QQ、Xcode,系统就会分别启动两个进程。
线程
一个进程要想执行任务,必须得有线程(每一个进程至少要有一条线程)
线程是进程的基本执行单元,一个进程(程序)的所有任务都在线程中执行
比如使用酷狗播放音乐、使用迅雷下载电影,都需要在线程中执行
线程的串行
一个线程中任务的执行是串行的
如果要在一个线程中执行多个任务,那么只能一个一个地按顺序执行这些任务
也就是说,在同一时间内,一个线程只能执行一个任务
比如在一个线程中下载三个文件(分别是文件A、文件B、文件C),下载顺序就是ABC
多线程
一个进程中可以开启多条线程,每条线程可以并行(同时)执行不同的任务
如果把进程比作车间,那么线程就相当于车间工人
多线程技术可以提高程序的执行效率,比如同时开启3条线程分别下载3个文件(分别是文件A、文件B、文件C)
同一时间,CPU只能处理一条线程,只有一条线程在工作(执行)
多线程并发(同时)执行,其实是CPU快速地在多条线程之间调度(切换),如果CPU调度线程的时间足够快,就造成了多线程并发执行的假象。
所以如果线程非常非常多,CPU会在N多线程之间调度,CPU会累死,消耗大量的CPU资源,每条线程被调度执行的频次会降低(线程的执行效率降低)。
优点:
- 能适当提高程序的执行效率
- 能适当提高资源利用率(CPU、内存利用率)
缺点:
- 开启线程需要占用一定的内存空间,如果开启大量的线程,会占用大量的内存空间,降低程序的性能
- 线程越多,CPU在调度线程上的开销就越大
- 程序设计更加复杂:比如线程之间的通信、多线程的数据共享
移动APP经常使用多线程,因为对APP来说,界面要保持响应用户操作并给以反馈,也就是要保持流畅。所以很多比较耗时的运算就应该放在其他线程中,保证主线程能够及时处理用户操作。
对于iOS程序,使用多线程有几类:
- c语言的
pthread_t
- NSThread
- GCD
- NSOperation
使用的比较多的应该就是GCD
和NSOperation
了,对于这两者的讨论可以看看这个
NSOperation vs Grand Central Dispatch
这里主要介绍GCD
GCD
GCD
全称是Grand Central Dispatch
,纯c语言提供。GCD
是苹果公司为多核的并行运算提出的解决方案,会自动利用更多的CPU内核(比如双核、四核),会自动管理线程的生命周期(创建线程、调度任务、销毁线程)。程序员只需要告诉GCD
想要执行什么任务,不需要编写任何线程管理代码。
GCD
中有两个核心概念:
- 任务:执行什么操作
- 队列:用来存放任务
将任务添加到队列中,GCD
会自动将队列中的任务取出,放到对应的线程中执行。任务的取出遵循队列的FIFO原则:First in first out
GCD
路径iOS usr/include/dispatch/下查看头文件说明
GCD常用方法
执行任务
-
dispatch_sync(dispatch_queue_t queue, dispatch_block_t block);
用同步的方式执行任务(当前线程中执行)
-
dispatch_async(dispatch_queue_t queue, dispatch_block_t block);
用异步的方式执行任务(另起一条线程中执行)
队列
从上面方法第一个参数dispatch_queue_t
就是GCD
的队列类型。一般分为两大类型:并发队列和串行队列。并发功能只有在异步函数下才有用。
- 同步:在当前线程中执行任务,不具备开启新线程的能力
- 异步:在新的线程中执行任务,具备开启新线程的能力
- 并发:多个任务并发(同时)执行
- 串行:一个任务执行完毕后,再执行下一个任务
获取串行队列:
-
dispatch_queue_t dispatch_queue_create(const char *label, dispatch_queue_attr_t attr);
参数为队列名和属性,属性一般用
NULL
-
dispatch_get_main_queue()
获得主队列,主队列是
GCD
自带的一种特殊的串行队列,放在主队列中的任务,都会放到主线程中执行
获取并发队列:GCD
默认已经提供了全局的并发队列,供整个应用使用,不需要手动创建
-
dispatch_queue_t dispatch_get_global_queue(dispatch_queue_priority_t priority,unsigned long flags);
第一个参数是优先级,第二个暂用0即可
#define DISPATCH_QUEUE_PRIORITY_HIGH 2
#define DISPATCH_QUEUE_PRIORITY_DEFAULT 0
#define DISPATCH_QUEUE_PRIORITY_LOW (-2)
#define DISPATCH_QUEUE_PRIORITY_BACKGROUND INT16_MIN
关于不同队列的执行任务的效果:
示例代码
一. 异步函数往并发队列添加任务
//获得全局并发队列 执行顺序每次都不一样
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
//添加任务到队列执行任务
//异步函数 具备开启新线程能力
dispatch_async(queue, ^{
NSLog(@"下载图片1---%@", [NSThread currentThread]);
});
dispatch_async(queue, ^{
NSLog(@"下载图片2---%@", [NSThread currentThread]);
});
dispatch_async(queue, ^{
NSLog(@"下载图片3---%@", [NSThread currentThread]);
});
NSLog(@"Main Thread: %@", [NSThread mainThread]);
//输出
Main Thread: <NSThread: 0x12de0c090>{number = 1, name = main}
下载图片1---<NSThread: 0x12ddc1140>{number = 6, name = (null)}
下载图片2---<NSThread: 0x12dd80ea0>{number = 7, name = (null)}
下载图片3---<NSThread: 0x12dee45f0>{number = 8, name = (null)}
看到开启了三个子线程执行任务
二. 异步函数往串行队列中添加任务
//创建串行队列 按顺序执行 而且只开启一个线程
dispatch_queue_t queue = dispatch_queue_create("wossoneri", NULL);
//添加任务到队列执行任务
//异步函数 具备开启新线程能力
dispatch_async(queue, ^{
NSLog(@"下载图片1---%@", [NSThread currentThread]);
});
dispatch_async(queue, ^{
NSLog(@"下载图片2---%@", [NSThread currentThread]);
});
dispatch_async(queue, ^{
NSLog(@"下载图片3---%@", [NSThread currentThread]);
});
NSLog(@"Main Thread: %@", [NSThread mainThread]);
//输出
Main Thread: <NSThread: 0x12c60c0d0>{number = 1, name = main}
下载图片1---<NSThread: 0x12c6574f0>{number = 6, name = (null)}
下载图片2---<NSThread: 0x12c6574f0>{number = 6, name = (null)}
下载图片3---<NSThread: 0x12c6574f0>{number = 6, name = (null)}
看到只开启了一条线程,串行执行任务
三. 用同步函数往并发队列中添加任务
//获得全局并发队列 执行顺序每次都不一样
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
NSLog(@"Main Thread: %@", [NSThread mainThread]);
//同步
dispatch_sync(queue, ^{
NSLog(@"下载图片1---%@", [NSThread currentThread]);
});
dispatch_sync(queue, ^{
NSLog(@"下载图片2---%@", [NSThread currentThread]);
});
dispatch_sync(queue, ^{
NSLog(@"下载图片3---%@", [NSThread currentThread]);
});
//输出
Main Thread: <NSThread: 0x136d04ae0>{number = 1, name = main}
下载图片1---<NSThread: 0x136d04ae0>{number = 1, name = main}
下载图片2---<NSThread: 0x136d04ae0>{number = 1, name = main}
下载图片3---<NSThread: 0x136d04ae0>{number = 1, name = main}
发现根本没有开启新线程,直接在主线程顺序执行,并发队列失去了并发功能。
四. 用同步函数往串行队列中添加任务
//创建串行队列 按顺序执行 而且只开启一个线程
dispatch_queue_t queue = dispatch_queue_create("wossoneri", NULL);
NSLog(@"Main Thread: %@", [NSThread mainThread]);
//同步
dispatch_sync(queue, ^{
NSLog(@"下载图片1---%@", [NSThread currentThread]);
});
dispatch_sync(queue, ^{
NSLog(@"下载图片2---%@", [NSThread currentThread]);
});
dispatch_sync(queue, ^{
NSLog(@"下载图片3---%@", [NSThread currentThread]);
});
//输出
Main Thread: <NSThread: 0x135e0c0b0>{number = 1, name = main}
下载图片1---<NSThread: 0x135e0c0b0>{number = 1, name = main}
下载图片2---<NSThread: 0x135e0c0b0>{number = 1, name = main}
下载图片3---<NSThread: 0x135e0c0b0>{number = 1, name = main}
没有开启新线程,依旧在主线程顺序执行任务。
小结:
- 同步函数(永远)不会开启新线程,不具备开线程的能力
- 异步函数具备开启新线程的能力(但不一定总会开线程?)
- 在串行队列只开启一条线程
- 在并发队列开启多条线程
主队列
主队列是和主线程相关联的队列,主队列是GCD
自带的一种特殊的串行队列,放在主队列中的任务,都会放到主线程中执行。
dispatch_queue_t queue = dispatch_get_main_queue();
使用异步函数执行主队列任务:
//获取主队列
dispatch_queue_t queue = dispatch_get_main_queue();
//把任务添加到主队列中执行
dispatch_async(queue, ^{
NSLog(@"使用异步函数执行主队列中的任务1--%@",[NSThread currentThread]);
});
dispatch_async(queue, ^{
NSLog(@"使用异步函数执行主队列中的任务2--%@",[NSThread currentThread]);
});
dispatch_async(queue, ^{
NSLog(@"使用异步函数执行主队列中的任务3--%@",[NSThread currentThread]);
});
//输出
Main Thread: <NSThread: 0x13de0c030>{number = 1, name = main}
使用异步函数执行主队列中的任务1--<NSThread: 0x13de0c030>{number = 1, name = main}
使用异步函数执行主队列中的任务2--<NSThread: 0x13de0c030>{number = 1, name = main}
使用异步函数执行主队列中的任务3--<NSThread: 0x13de0c030>{number = 1, name = main}
看到任务都在主线程中执行。
使用同步函数执行主队列任务:
此时会发生死锁。
死锁的原因是:主线程本身是串行队列,串行队列的任务是顺序执行的。
比如下面代码段
NSLog(@"1");
dispatch_sync(dispatch_get_main_queue(), ^{
NSLog(@"2");
});
NSLog(@"3");
在主线程中,任务执行的顺序是:
NSLog(@"1");
dispatch_sync()
NSLog(@"3");
NSLog(@"2");
其中
NSLog(@"2");
是作为Block
的内容放在队列最后执行。
但dispatch_sync()
方法必须返回才能往下执行,其返回的条件是Block
的内容执行完毕才行。
也就是说死锁的条件是因为dispatch_sync()
方法在等待Block
执行完毕,而Block
在等待dispatch_sync()
方法往下执行才能轮到它。
所以,如果把任务放到主队列中进行处理,那么不论处理函数是异步的还是同步的都不会开启新的线程。
延时执行
可以使用NSObject
方法,该方法通常在哪个线程调用,就在哪个线程执行,一般是主线程
[self performSelector:@selector(run) withObject:nil afterDelay:2.0];
示例
- (void)viewDidLoad {
NSLog(@"打印线程----%@",[NSThread currentThread]);
//延迟执行
[self performSelector:@selector(runA) withObject:nil afterDelay:2.0];
}
- (void)onBtnClicked {
//在异步函数中执行
dispatch_queue_t queue = dispatch_queue_create("wOw", 0);
dispatch_async(queue, ^{
[self performSelector:@selector(runB) withObject:nil afterDelay:1.0];
});
NSLog(@"异步函数");
}
- (void)runA {
NSLog(@"延迟执行----%@", [NSThread currentThread]);
}
- (void)runB {
NSLog(@"异步函数中延迟执行----%@", [NSThread currentThread]);
}
//输出
2016-05-29 23:31:49.194 FunctionTest[4199:1337673] 打印线程----<NSThread: 0x154e04b60>{number = 1, name = main}
2016-05-29 23:31:51.197 FunctionTest[4199:1337673] 延迟执行----<NSThread: 0x154e04b60>{number = 1, name = main}
2016-05-29 23:31:58.198 FunctionTest[4199:1337673] 异步函数
这里发现,异步下的runB方法似乎并没有执行。换成同步则会执行。
出现这个问题的原因是async开的新线程中的runLoop
没有启动,在后面加上
[[NSRunLoop currentRunLoop] run];
即可。好吧..后面再研究一下RunLoop
原理...
dispatch_async(queue, ^{
[self performSelector:@selector(runB) withObject:nil afterDelay:1.0];
[[NSRunLoop currentRunLoop] run];
});
//再看输出
2016-05-29 23:37:10.877 FunctionTest[4214:1339152] 打印线程----<NSThread: 0x12660a2a0>{number = 1, name = main}
2016-05-29 23:37:12.879 FunctionTest[4214:1339152] 延迟执行----<NSThread: 0x12660a2a0>{number = 1, name = main}
2016-05-29 23:37:15.150 FunctionTest[4214:1339152] 异步函数
2016-05-29 23:37:16.157 FunctionTest[4214:1339199] 异步函数中延迟执行----<NSThread: 0x1266b3f40>{number = 6, name = (null)}
延时都是正确的。使用异步函数后执行的线程也变为的新线程。
使用GCD
方法
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
});
示例:
NSLog(@"打印线程----%@",[NSThread currentThread]);
//GCD delay
//1 主队列
dispatch_queue_t queue = dispatch_get_main_queue();
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5.0 * NSEC_PER_SEC)), queue, ^{
NSLog(@"主队列--延迟执行------%@",[NSThread currentThread]);
});
//2 并发队列
dispatch_queue_t queue1 = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_time_t when = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5.0 * NSEC_PER_SEC));
dispatch_after(when, queue1, ^{
NSLog(@"并发队列-延迟执行------%@",[NSThread currentThread]);
});
//输出
2016-05-30 22:14:43.734 FunctionTest[4507:1470176] 打印线程----<NSThread: 0x15ee0bd40>{number = 1, name = main}
2016-05-30 22:14:49.234 FunctionTest[4507:1470176] 主队列--延迟执行------<NSThread: 0x15ee0bd40>{number = 1, name = main}
2016-05-30 22:14:49.235 FunctionTest[4507:1470304] 并发队列-延迟执行------<NSThread: 0x15ed90390>{number = 6, name = (null)}
看到并发队列开启一个新线程,在新线程执行。主队列直接在主线程执行。
线程切换
之前说过,程序中遇到耗时操作就要把操作放在另外一个线程中执行。当执行过之后,就需要把耗时操作得到的数据带回到主线程对UI进行刷新操作,这时就可以用如下代码。
dispatch_async( dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// 执⾏耗时的异步操作
dispatch_async(dispatch_get_main_queue(), ^{
// 回到主线程,执⾏UI刷新操作
});
});
一次性代码
这个概念在之前的单例模式中提到过,就是保证一段代码段在程序执行过程中只执行一次。使用的方法就是dispatch_once
。
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
// 只执行1次的代码(这里面默认是线程安全的)
});
Block
中的代码在整个程序运行期间只执行一次!
队列组
队列组的使用情形是这样:
现在有多个耗时的操作要做,我当然要考虑开启异步的线程去,把任务放到并发队列去做。但此时我需要这几个操作都完成的时候回到主线程来。
此时有一个办法,就是把操作统一放在一个线程里做,这样我能知道线程执行结束的时间,但缺点是这些耗时的操作是串行的。
如果让这些操作并行执行,那效率就更高了,但我该怎么知道全部操作都执行完的时间呢?
这时就用上队列组了。
创建一个队列组,把所有异步操作都放在队列组中,这样队列组执行完会发出一个通知回来。
//创建队列组
dispatch_group_t group = dispatch_group_create();
//异步方法
dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// 执行一个耗时的异步操作
});
dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// 执行另一个耗时的异步操作
});
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
// 等前面的异步操作都执行完毕后,回到主线程
});
暂时整理这么多,掌握这些可以应对大多数使用到GCD
的相关问题了。