[iOS]多线程和GCD

时间:2021-03-18 05:15:57

新博客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

使用的比较多的应该就是GCDNSOperation了,对于这两者的讨论可以看看这个
NSOperation vs Grand Central Dispatch

这里主要介绍GCD

GCD

GCD全称是Grand Central Dispatch,纯c语言提供。
GCD是苹果公司为多核的并行运算提出的解决方案,会自动利用更多的CPU内核(比如双核、四核),会自动管理线程的生命周期(创建线程、调度任务、销毁线程)。程序员只需要告诉GCD想要执行什么任务,不需要编写任何线程管理代码。

GCD中有两个核心概念:

  1. 任务:执行什么操作
  2. 队列:用来存放任务

将任务添加到队列中,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

关于不同队列的执行任务的效果:
[iOS]多线程和GCD

示例代码

一. 异步函数往并发队列添加任务

  //获得全局并发队列 执行顺序每次都不一样
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的相关问题了。

Reference
整理多线程篇
GCD中在主线程中用同步函数分派任务到串行队列中会产生死锁是什么原因?