多线程在iOS开发中的应用

时间:2021-10-26 21:27:31

1. 多线程基本概念


1.1 进程

进程是指在系统中正在运行的一个应用程序。每个进程之间是独立的,每个进程均运行在其专用且受保护的内存空间内。

1.2 线程

基本概念:一个进程要想执行任务,必须得有线程(每一个进程至少要有一条线程),线程是进程的基本执行单元,一个进程(程序)的所有任务都在线程中执行。

线程的串行:一条线程中任务的执行是串行的,如果要在一条线程中执行多个任务,那么只能一个一个地按顺序执行这些任务。也就是说,在同一时间内,一条线程只能执行一个任务。

1.3 多线程

基本概念:即一个进程中可以开启多个线程,每条线程可以并行(同时)执行不同的任务。

线程的并行:并行即同时执行。比如同时开启3条线程分别下载3个文件(分别是文件A、文件B、文件C)。

多线程并发执行的原理:在同一时间里,CPU只能处理1条线程,只有1条线程在工作(执行)。多线程并发(同时)执行,其实是CPU快速地在多条线程之间调度(切换),如果CPU调度线程的时间足够快,就造成了多线程并发执行的假象。

多线程优缺点

优点:能适当提高程序的执行效率、能适当提高资源利用率(CPU、内存利用率)

缺点:

1)开启线程需要占用一定的内存空间(默认情况下,主线程占用1M,子线程占用512KB),如果开启大量的线程,会占用大量的内存空间,降低程序的性能。

2)线程越多,CPU在调度线程上的开销就越大。

3)程序设计更加复杂:比如线程之间的通信、多线程的数据共享

1.4 多线程在iOS开发中的应用

1.4.1 主线程

​ 1)一个iOS程序运行后,默认会开启1条线程,称为“主线程”或“UI线程”。

​ 2)作用。刷新显示UI,处理UI事件。

1.4.2 使用注意

​ 1)不要将耗时操作放到主线程中去处理,会卡住线程。

​ 2)和UI相关的刷新操作必须放到主线程中进行处理。

2. 多线程实现方案

技术方案 简介 语言 线程生命周期管理 使用频率
pthread —> 一套通用的多线程API
—> 适用于Unix\Linux\ Windows等系统
—> 跨平台\可植入
—> 使用难度比较大
C 手动 极低
NSThread —> 使用更加面向对象
—> 简单易用,可以直接操作线程对象
Objective-C 手动
GCD —> 旨在替代NSThread等线程技术
—> 充分利用设备的多核
C 自动
NSOperation —> 基于GCD (底层是GCD)
—> 比GCD多了一些更简单实用的功能
—> 使用更加面向对象
Objective-C 自动

2.1 pthread

//pthread需要包含头文件
//1.创建线程对象
pthread_t thread;
NSString *name = @"线程";
//2.使用pthread创建线程
//第一个参数:线程对象地址
//第二个参数:线程属性
//第三个参数:指向函数的指针
//第四个参数:传递给该函数的参数
pthread_create(&thread, NULL, run, (__bridge void *)(name));

2.2 NSThread

2.2.1 基本使用

// 第一种:alloc init,需要手动开启线程,可以拿到线程对象进行详细设置
NSThread *thread = [[NSThread alloc]initWithTarget:self selector:@selector(run:) object:@"线程"];
[thread start]; // 第二种:分离(detach)出一条子线程,自动启动线程,无法对线程进行更详细的设置
[NSThread detachNewThreadSelector:@selector(run:) toTarget:self withObject:@"分离出的子线程"]; // 第三种:后台线程,自启动,不能详细设置
[self performSelectorInBackground:@selector(run:) withObject:@"后台线程"];

2.2.2 设置线程属性

thread.name = @"线程A"; // 线程名称
thread.threadPriority = 1.0; // 线程的优先级,取值范围0.0~1.0,1.0的优先级最高,默认0.5

2.2.3 线程的状态

// 线程的各种状态:新建-就绪-运行-阻塞-死亡
// 常用的控制线程状态的方法
[NSThread exit]; // 退出当前线程
[NSThread sleepForTimeInterval:2.0]; // 阻塞线程
[NSThread sleepUntilDate:[NSDate dateWithTimeIntervalSinceNow:2.0]];//阻塞线程
// 注意:线程死了不能复生

2.2.4 线程安全

01 前提:多个线程访问同一块资源会发生数据安全问题
02 解决方案:加互斥锁
03 相关代码:@synchronized(self){}
04 专业术语-线程同步
05 原子和非原子属性(是否对setter方法加锁)

2.2.5线程间通信

-(void)touchesBegan:(nonnull NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event {
// 开启子线程下载图片
[NSThread detachNewThreadSelector:@selector(downloadImage) toTarget:self withObject:nil];
} -(void)downloadImage { NSURL *url = [NSURL URLWithString:@"http://p6.qhimg.com/t01d2954e2799c461ab.jpg"];
UIImage *image = [UIImage imageWithData:[NSData dataWithContentsOfURL:url]]; // 回到主线程刷新UI,以下三种任选其一,第二种方面些
[self performSelectorOnMainThread:@selector(showImage:) withObject:image waitUntilDone:YES];
[self.imageView performSelectorOnMainThread:@selector(setImage:) withObject:image waitUntilDone:YES];
[self.imageView performSelector:@selector(setImage:) onThread:[NSThread mainThread] withObject:image waitUntilDone:YES];
}

2.2.6 如何计算代码段的执行时间

//第一种方法
NSDate *start = [NSDate date];
NSData *data = [NSData dataWithContentsOfURL:url];
NSDate *end = [NSDate date];
NSLog(@"耗时%f",[end timeIntervalSinceDate:start]); //第二种方法
CFTimeInterval start = CFAbsoluteTimeGetCurrent();
NSData *data = [NSData dataWithContentsOfURL:url];
CFTimeInterval end = CFAbsoluteTimeGetCurrent();
NSLog(@"耗时%f",end - start);

2.3 GCD

2.3.1 GCD基本知识

  • 两个核心概念 队列和任务
  • 同步函数和异步函数

2.3.2 GCD基本使用

主队列:dispatch_get_main_queue()

并发队列:dispatch_get_global_queue(0, 0),dispatch_queue_create("com.test", DISPATCH_QUEUE_CONCURRENT)

手动创建串行队列:dispatch_queue_create("com.test", DISPATCH_QUEUE_SERIAL)

并发队列 手动创建的串行队列 主队列
dispatch_sync 没有开启新的线程,串行执行 同← 同←
dispatch_async 开启新的线程,并发执行 开启新的线程,串行执行 没有开启新的线程,串行执行

2.3.3 GCD线程间通信

// 获取一个全局的队列
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
// 先开启一个线程,把下载图片的操作放在子线程中处理
dispatch_async(queue, ^{
// 下载图片
NSString *urlString = @"http://h.hiphotos.baidu.com/zhidao/pic/item/6a63f6246b600c3320b14bb3184c510fd8f9a185.jpg";
NSURL *url = [NSURL URLWithString:urlString];
UIImage *image = [UIImage imageWithData:[NSData dataWithContentsOfURL:url]];
// 回到主线程刷新UI
dispatch_async(dispatch_get_main_queue(), ^{
self.imageView.image = image;
});
});

2.3.4 GCD其它常用函数

—> 栅栏函数(控制任务的执行顺序)

以下实例,会保证Task One和Task Two全部完成后执行之后的Task Three和Task Four

dispatch_queue_t queue = dispatch_queue_create("com.example.myqueue", DISPATCH_QUEUE_CONCURRENT);

dispatch_async(queue, ^{
for (int i = 0; i < 10; i++) {
NSLog(@"Task One");
}
});
dispatch_async(queue, ^{
for (int i = 0; i < 10; i++) {
NSLog(@"Task Two");
}
}); // The queue you specify should be a concurrent queue that you create yourself using the dispatch_queue_create function. If the queue you pass to this function is a serial queue or one of the global concurrent queues, this function behaves like the dispatch_async function.
// 使用 dispatch_queue_create 函数创建并发队列(使用DISPATCH_QUEUE_CONCURRENT, 而不是DISPATCH_QUEUE_SERIAL), 不要使用dispatch_get_global_queue, 否则和dispatch_async一样效果
dispatch_barrier_async(queue, ^{
NSLog(@"++++++++++");
}); dispatch_async(queue, ^{
for (int i = 0; i < 10; i++) {
NSLog(@"Task Three");
}
}); dispatch_async(queue, ^{
for (int i = 0; i < 10; i++) {
NSLog(@"Task Four");
}
});

—> 延迟执行(延迟并且可以控制在哪个线程执行)

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
// code to be executed after a specified delay
});

—> 一次性代码

-(void)once {
// 整个程序运行过程中只会执行一次(注意区分一次性代码和懒加载)
// onceToken用来记录该部分的代码是否被执行过
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
});
}

—> 快速迭代(开多个线程并发完成迭代操作)

dispatch_apply(12, dispatch_get_global_queue(0, 0), ^(size_t index) {
NSLog(@"SuperLog------ %zd",index);
});

—> 队列组(同栅栏函数)

dispatch_queue_t queue = dispatch_queue_create("com.example", DISPATCH_QUEUE_CONCURRENT);
dispatch_group_t group = dispatch_group_create();
dispatch_group_async(group, queue, ^{ });
// 队列组中的任务执行完毕之后,执行该函数
dispatch_group_notify(group, queue, ^{ });

2.4.NSOperation

2.4.1 概念

  • NSOperation是对GCD的包装
  • 两个核心概念【队列+操作】

2.4.2 使用

  • NSOperation本身是抽象类,只能使用它的子类
  • 三个子类分别是:NSBlockOperation、NSInvocationOperation以及自定义继承自NSOperation的类
  • NSOperation和NSOperationQueue结合使用实现多线程并发

2.4.3 代码

—> NSInvocationOperation

NSInvocationOperation *operation = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(test) object:nil];
[operation start];

—> NSBlockOperation

NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
// 在主线程中执行
NSLog(@"downloadOne--%@",[NSThread currentThread]);
}]; // 增加操作,增加的操作在子线程中执行
[operation addExecutionBlock:^{
NSLog(@"downloadTwo--%@",[NSThread currentThread]);
}]; [operation addExecutionBlock:^{
NSLog(@"downloadThree--%@",[NSThread currentThread]);
}]; [operation start];

—> 自定义NSOperation

// 自定义的NSOperation, 通过重写内部的main方法实现封装操作
-(void)main {
NSLog(@"--main--%@",[NSThread currentThread]);
}
// 使用
SPOperation *operation = [[SPOperation alloc] init];
[operation start];

2.5 NSOperationQueue

2.5.1 NSOperation中的两种队列

  • 主队列 通过mainQueue获得,凡是放到主队列中的任务都将在主线程执行
  • 非主队列 直接alloc init出来的队列。非主队列同时具备了并发和串行的功能,通过设置最大并发数属性来控制任务是并发执行还是串行执行

2.5.2 代码

GCD中的队列

串行队列:dispatch_queue_create("com.test", DISPATCH_QUEUE_SERIAL),dispatch_get_main_queue()

并发队列:dispatch_queue_create("com.test", DISPATCH_QUEUE_CONCURRENT),dispatch_get_global_queue(0, 0)

NSOperationQueue

主队列:[NSOperationQueue mainqueue] 放在主队列中的操作都在主线程中执行

非主队列:[[NSOperationQueue alloc] init] 并发和串行,默认是并发执行的

// 自定义NSOperation
-(void)customOperation {
// 创建队列
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
// 创建操作
SPOperation *operationOne = [[SPOperation alloc] init];
SPOperation *operationTwo = [[SPOperation alloc] init];
// 添加操作到队列中
[queue addOperation:operationOne];
[queue addOperation:operationTwo]; // You can call this method explicitly if you want to execute your operations manually. However, it is a programmer error to call this method on an operation object that is already in an operation queue or to queue the operation after calling this method. Once you add an operation object to a queue, the queue assumes all responsibility for it.
// 有队列管理,不要手动调用 start
// [operationOne start]
} //NSBlockOperation
- (void)block {
// 创建队列
NSOperationQueue *queue = [[NSOperationQueue alloc] init]; // 封装操作
NSBlockOperation *operationOne = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"operationOne--%@",[NSThread currentThread]);
}];
NSBlockOperation *operationTwo = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"operationTwo--%@",[NSThread currentThread]); }];
[operationTwo addExecutionBlock:^{
NSLog(@"operationThree--%@",[NSThread currentThread]);
}]; // 添加操作到队列中
[queue addOperation:operationOne];
[queue addOperation:operationTwo]; // 开发直接使用该方法即可,简单方便
[queue addOperationWithBlock:^{
NSLog(@"operationFour--%@",[NSThread currentThread]);
}];
} // NSInvocationOperation
- (void)invocation {
// 创建队列
NSOperationQueue *queue = [[NSOperationQueue alloc] init]; // 封装操作
NSInvocationOperation *operationOne = [[NSInvocationOperation alloc]initWithTarget:self selector:@selector(downloadOne) object:nil];
NSInvocationOperation *operationTwo = [[NSInvocationOperation alloc]initWithTarget:self selector:@selector(downloadTwo) object:nil]; // 把封装好的操作添加到队列中
[queue addOperation:operationOne];
[queue addOperation:operationTwo];
}

2.5.3 NSOperation其它用法

—> 设置最大并发数

NSOperationQueue *queue = [[NSOperationQueue alloc]init];
// 设置最大并发数, 该属性需要在任务添加到队列中之前进行设置
// 该属性控制队列是串行执行还是并发执行, 1 串行 >1 并行
// 系统的最大并发数有个默认的值,为-1,如果该属性设置为0,那么不会执行任何任务
queue.maxConcurrentOperationCount = 2;

—> 暂停和恢复以及取消

//设置暂停和恢复
//暂停表示不继续执行队列中的下一个任务,暂停操作是可以恢复的
if (self.queue.isSuspended) {
self.queue.suspended = NO;
}else {
self.queue.suspended = YES;
} // 取消队列里面的所有操作
// 取消之后,当前正在执行的操作的下一个操作将不再执行,而且永远都不在执行,就像后面的所有任务都从队列里面移除了一样
// 取消操作是不可以恢复的
[self.queue cancelAllOperations]; // 自定义NSOperation取消操作
-(void)main {
//耗时操作
for (int i = 0; i<1000; i++) {
NSLog(@"任务1-%d--%@",i,[NSThread currentThread]);
}
NSLog(@"+++++++++++++++++++++++++++++++++");
//苹果官方建议,每当执行完一次耗时操作之后,就查看一下当前队列是否为取消状态,如果是,那么就直接退出(为了提高程序的性能)
if (self.isCancelled) {
return;
}
}

2.5.4 NSOperation实现线程间通信

—> 开子线程下载图片

NSOperationQueue *queue = [[NSOperationQueue alloc]init];
[queue addOperationWithBlock:^{
NSURL *url = [NSURL URLWithString:@"http://h.hiphotos.baidu.com/zhidao/pic/item/6a63f6246b600c3320b14bb3184c510fd8f9a185.jpg"];
UIImage *image = [UIImage imageWithData:[NSData dataWithContentsOfURL:url]];
// 主线程刷新UI
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
self.imageView.image = image;
}];
}];

—> 操作依赖(operationThree 依赖operationOne和operationTwo)

NSOperationQueue *queue = [[NSOperationQueue alloc]init];
NSBlockOperation *operationOne = [NSBlockOperation blockOperationWithBlock:^{
// Task One
}]; NSBlockOperation *operationTwo = [NSBlockOperation blockOperationWithBlock:^{
// Task Two
}]; NSBlockOperation *operationThree = [NSBlockOperation blockOperationWithBlock:^{
// Task Three
}]; // 操作依赖
[operationThree addDependency:operationOne];
[operationThree addDependency:operationTwo]; // 添加操作到队列中执行
[queue addOperation:operationOne];
[queue addOperation:operationTwo];
[queue addOperation:operationThree];

补充

使用create函数创建的并发队列和全局并发队列的主要区别:

  1. 全局并发队列在整个应用程序中本身是默认存在的,并且对应有高优先级、默认优先级、低优先级和后台优先级一共四个并发队列,我们只是选择其中的一个直接拿来用。而create函数是从头开始去创建一个队列。
  2. 在iOS6.0之前,在GCD中凡是使用了带create和retain的函数在最后都需要做一次release操作。而主队列和全局并发队列不需要我们手动release。当然了,在iOS6.0之后GCD已经被纳入到了ARC的内存管理范畴中,即便是使用retain或者create函数创建的对象也不再需要开发人员手动释放,我们像对待普通OC对象一样对待GCD就可以。
  3. 在使用栅栏函数的时候,苹果官方明确规定栅栏函数只有在和使用create函数自己的创建的并发队列一起使用的时候才有效(没有给出具体原因)
  4. 其它区别涉及到XNU内核的系统级线程编程,不一一列举。
  5. 给出一些参考资料(可以自行研究):

GCDAPI

Libdispatch版本源码