iOS开发之多线程
本文章博主和大家一块学习多线程,很自然就涉及到线程和进程,然后涵括NSThread、GCD、NSOperation!然后就是最牛叉的RunLoop和Runtime。
进程的介绍:
NSProcessInfo
进程是系统中正在运行的应用程序,系统分配资源的最小单元,例如手机上面运行的APP就是一个进程。每个进程都是独立,专有空间受保护的。
进程有五种状态:
- 新建状态:系统为进程分配资源
- 就绪状态:进程创建完成,等待CPU调度
- 运行状态:得到CPU调度运行
- 阻塞状态:等待某事件执行
- 终止状态:进程终止
在iOS中可以通过系统提供的进程信息类NSProcessInfo类获取当前进程的信息。
NSProcessInfo *processInfo = [NSProcessInfo processInfo];
NSLog(@"activeProcessorCount=%ld",processInfo.activeProcessorCount);
//当前的进程的环境
NSLog(@"environment:%@",processInfo.environment);
//获取进程开启的时候,传入的命令行参数信息,是一个参数数组
NSLog(@"arguments:%@",processInfo.arguments);
NSLog(@"hostName:%@",processInfo.hostName);
processInfo.processName = @"custom_process_name";
//进程名称 进程名称默认与项目的名称相同,可以更改
NSLog(@"processName:%@",processInfo.processName);
//进程ID
NSLog(@"processIdentifier:%d",processInfo.processIdentifier);
//全球唯一码
NSLog(@"globallyUniqueString:%@",processInfo.globallyUniqueString);
//操作系统版本号
NSLog(@"operatingSystemVersionString:%@",processInfo.operatingSystemVersionString);
NSLog(@"operatingSystemVersion.majorVersion:%ld",processInfo.operatingSystemVersion.majorVersion);
NSLog(@"operatingSystemVersion.minorVersion:%ld",processInfo.operatingSystemVersion.minorVersion);
NSLog(@"operatingSystemVersion.patchVersion:%ld",processInfo.operatingSystemVersion.patchVersion);
//系统启动的时间
NSLog(@"systemUptime:%f",processInfo.systemUptime);
//获取iOS设备是否开启了低电量模式
NSLog(@"lowPowerModeEnabled:%d",processInfo.lowPowerModeEnabled);
一、线程与进程
进程是对运行时程序的封装,是系统进行资源调度和分配的基本单位,实现了操作系统的并发。
线程是进程的子任务,是CPU调度和分配的基本单位,用于保障程序执行的实时性,实现进程内部的并发。
区别:
- 一个线程只能属于一个进程,而一个进程可以有多个线程,但至少有一个线程。线程依赖于进程而存在。
- 进程是资源分配的最小单位,线程是CPU调度的最小单位
- 进程在执行过程中拥有独立的内存单元,而多个线程共享进程的内存。
- 进程间不会相互影响 ;线程:一个线程挂掉将导致整个进程挂掉
- 线程之间的通信更加方便,同一个进程下线程共享全局变量,静态变量等数据。
二、多线程
多线程的优点和缺点
- 优点
1.能适当的提高程序的执行效率
2.能适当提高资源利用率(CPU内存利用率) - 缺点
1.开启线程需要占用一定的内存空间,如果开启大量的线程,则会占用大量的内存空间,降低程序的性能。
2.线程越多,CUP在调度线程的开销就越大。
3.程序设计更加复杂,比如线程之间的通信,多线程的数据共享。
多线程的应用
1.使用单利模式时,可以使用GCD
2.耗时操作放入子线程处理,完成后回主线程显示。
3.从数据库读取大量数据,可开辟子线程操作。
4.处理音频、视频数据时,在子线程处理。
5.数据同步操作。
三、容易混淆的4个术语
同步和异步主要影响:能不能开启新的线程。
1.同步:只是在当前线程中执行任务,不具备开启新线程的能力。
2.异步:可以在新的线程中执行,具备开启新线程的能力。
并发和串行主要影响:任务的执行方式
并发:多个任务并发(同时)执行。
串行:一个任务执行完毕后,再执行下一个任务。
四、NSThread、NSOperation、GCD
1.NSThread
1)通过NSThread的对象方法(动态创建)
2)通过NSThread的类方法(静态创建)
NSThread的线程管理
- 线程创建
- 线程开启
- 线程取消
- 线程关闭
- 线程暂停
- 设置线程优先级
NSThread的线程通信
- 指定当前线程执行操作
- (在其他线程中)指定主线程执行操作
- 在主线程中)指定其他线程执行操作
2.NSOperation
2.1 NSOperation 基本使用
在iOS开发中,为了提升用户体验,我们通常会将操作耗时的操作放在主线程之外的线程进行处理。对于正常的简单操作,我们更多的选择代码更少的GCD,让我们专注于自己的业务逻辑开发。NSOperation在iOS4后也基于GCD实现,但是对于GCD来说可控性更强,并且可以加入操作依赖。
(1) 相关概念
NSOperation是对GCD的封装,其中有两个核心概念【队列+操作】
1.NSOperation本身是抽象类,只能有它的子类。
2.两大子类分别是:NSBlockOperation、NSInvocationOperation,当然你也可以自定义继承自NSOperation的类。
(2)基本使用
- 实例化NSOperation的子类,绑定执行的操作。
- 创建NSOperationQueue队列,将NSOperation实例添加进来。
- 系统会自动将NSOperationQueue队列中检测取出和执行NSOperation的操作。
NSInvocationOperation类
// 01 NSInvocationOperation创建线程
/*
第一个参数:目标对象
第二个参数:该操作要调用的方法,最多接受一个参数
第三个参数:调用方法传递的参数,如果方法不接受参数,那么该值传nil
*/
- (void) method1 {
NSString * imageUrl = @"https://ss1.bdstatic.com/70cFvXSh_Q1YnxGkpoWK1HF6hhy/it/u=482494819,3150422682&fm=200&gp=0.jpg";
NSInvocationOperation * invocationOperation = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(loadImageSource:) object:imageUrl];
// [invocationOperation start];
//一旦执行操作,就会调用target的sel方法, 默认情况下调用了start方法后并不会开一条新线程去执行操作,而是在当前线程同步执行操作,只有将NSOperation放到一个NSOperationQueue中,才会异步执行操作
NSOperationQueue *queue = [[NSOperationQueue alloc]init];
[queue addOperation:invocationOperation];
}
//更新imageView
- (void)updateImage:(NSData *)data{
//在主线程中更新UI
//将二进制数据转换为图片
UIImage *image=[UIImage imageWithData:data];
//设置image
self.imageView.image=image;
}
NSBlockOperation类
- 创建NSBlockOperation对象
- 通过addExecutionBlock:方法添加更多的操作
NSOperationQueue 控制串行执行、并发执行
使用NSOperation Queue创建的自定义队列同时具有串行、并发执行的能力,这里的关键就是maxConcurrentOperationCount这个关键属性,叫做最大并发操作数,用来控制一个特定队列中可以有多少个操作同时参与并发执行。 - maxConcurrentOperationCount 默认情况下为-1,表示不进行限制,可进行并发执行。
- maxConcurrentOperationCount 为1时,队列为串行队列。只能串行执行。
- maxConcurrentOperationCount 大于1时,队列为并发队列。
队列的取消、暂停、和恢复
// 恢复队列,继续执行
// self.queue.suspended = NO;
// 暂停(挂起)队列,暂停执行
// self.queue.suspended = YES;
// 取消队列的所有操作
[self.queue cancelAllOperations];
操作依赖
NSOperation之间可以设置依赖来保证执行顺序
不添加依赖之前op1、op2、op3的顺序是随机的
在不同queue的NSOperation之间创建依赖关系
在不加依赖之前,op1和op2的执行顺序是随机的,添加依赖后op2会在op1之后执行。
线程间的通讯
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
[self updateImage:data];
}];
3.GCD
3.1GCD介绍
GCD(Grand Central Dispatch)即大中枢调度器,是苹果公司为 iOS 和 macOS 等系统开发的一套用于并发编程的技术框架。纯C语言开发。
两大核心概念
- 任务(Task):指需要执行的具体工作单元,可以是一个代码块,比如一段进行数据计算、网络请求或者 UI 更新的代码等。
- 队列(Queue):是 GCD 中用于组织和管理任务的结构,任务会按照一定规则被添加进队列,然后由 GCD 安排线程去执行队列中的任务。
GCD的优势
- 简化并发编程:开发者无需手动创建和管理线程,只需要把任务添加到合适的队列中,GCD 就会自动根据系统情况安排线程去执行任务,大大降低了并发编程的复杂性。
- 高效利用系统资源:能够根据系统的硬件资源(如多核 CPU)情况,动态地分配线程来执行任务,充分发挥多核优势,提高任务执行效率,无论是并行执行多个任务还是合理安排串行任务执行顺序都能很好地适配系统。
- 线程安全保障:通过各种机制(如主队列保证 UI 操作线程安全、调度组协调多任务同步等),帮助开发者在多线程环境下更容易实现线程安全的代码编写,减少因多线程并发访问共享资源等导致的问题。
3.2GCD如何自动管理线程
- 线程池机制
GCD 维护了一个线程池,线程池中的线程数量会根据系统负载等情况动态调整。
1.系统自动创建和回收线程:当有任务需要执行时,GCD会从线程池中挑选合适的空闲线程来执行任务。如果当前没有空闲线程且系统判定需要增加线程数量来满足任务执行需求,他会自动创建新的线程。而当线程完成任务处于空闲状态一段时间后,GCD会根据情况回收这些线程,将其释放回系统资源中,避免线程资源的浪费,整个过程无需开发者手动干预。
2.适配不同硬件资源:在多核处理器环境下,GCD 能够充分利用多核优势,合理分配任务到多个核心对应的线程上,尽可能并行地执行任务以提高效率;在单核处理器场景中,它会通过合理的任务调度,让任务按顺序高效执行,模拟出多任务并发执行的效果
- 任务队列管理
GCD 基于队列来组织任务,主要有串行队列和并行队列这两种类型的队列,不同队列特性决定了任务的执行顺序和并行情况,以此实现对线程的自动管理。
1.串行队列:
任务顺序执行:放入串行队列的任务会按照添加的先后顺序依次执行,一个任务执行完毕后,才会开始执行下一个任务。GCD 会自动为串行队列关联线程,同一时间只会有一个任务在对应的线程上执行,比如常见的主线程上的任务执行就是通过主线程对应的串行队列来管理的,确保 UI 相关操作按顺序依次进行,避免出现界面显示混乱等问题。
线程复用:虽然任务顺序执行,但不同任务执行时可能复用同一个线程,即前一个任务执行完,线程空出来后,下一个任务直接使用该线程继续执行,减少了线程频繁创建和销毁的开销。
2.并行队列
任务并行执行(受限于系统资源):并行队列允许其中的多个任务同时执行,只要系统资源(如 CPU 核心数量、内存等)允许,GCD 会自动分配多个线程来并行处理这些任务,加快任务的整体完成速度,像进行多个数据文件的同时读取、多个网络请求的并发发起等场景可以使用并行队列来提高效率。
动态分配线程资源:GCD 根据当前系统的繁忙程度、可用资源以及队列中任务的数量等因素动态地决定为并行队列分配多少线程来执行任务。例如在系统资源紧张时,可能会限制并行执行的任务数量,减少线程的分配;而在资源充足时,充分利用多核并行执行更多任务。
- 基于优先级调度
GCD 中的任务队列可以设置不同的优先级,从而实现对线程使用的精细化管理。
优先级区分:有高、中(默认)、低等不同的优先级级别可供选择。高优先级的任务队列中的任务通常会优先获得线程资源去执行,比如一些和用户交互紧密相关、对实时性要求高的任务(如实时更新 UI 响应操作)可以设置为高优先级;而低优先级的任务可能会在系统资源相对空闲时才会被分配线程执行,像一些后台的数据统计、日志记录等非紧急任务可以设置为低优先级。
- 自动处理线程间的同步和通信
GCD 提供了多种机制来确保线程间的正确协作,实现对线程的有效管理。
1.调度组(Dispatch Group):可以将多个任务添加到一个调度组中,用于同步这些任务的执行情况。例如,当你有多个异步任务,并且需要在它们全部完成后再执行后续的操作时,通过调度组可以方便地实现这种等待和同步。GCD 会自动跟踪这些任务在不同线程上的执行进度,当所有任务都完成后,再调度后续相关线程执行对应的任务,避免了复杂的线程间手动同步代码编写。
2.栅栏函数(Barrier Block):在并行队列中,栅栏函数可以起到一个分隔作用。在它之前提交到队列的任务可以并行执行,而它之后提交的任务则要等栅栏函数执行完后才开始执行,常用于需要在某个关键节点对并行任务执行顺序进行控制的场景,GCD 借助栅栏函数自动协调线程间关于这部分任务的执行先后顺序。
3.主队列(Main Queue)和异步执行到主线程:为了方便线程间通信,尤其是和 UI 线程(主线程)的交互,GCD 提供了主队列,开发者可以通过异步方式将任务派发到主队列,GCD 会自动在合适的时机将任务切换到主线程对应的线程上执行,确保 UI 更新等操作在主线程安全进行,实现了从其他线程到主线程的自动线程切换和同步。
通过以上这些机制的协同作用,GCD 实现了对线程的自动高效管理,让开发者可以更专注于业务逻辑的实现,而无需过多操心底层线程的具体创建、调度和销毁等复杂操作。
3.3GCD的使用步骤:
-
指定任务:确定想要做的事
-
将任务添加到队列中:GCD会自动将队列中的任务取出,放到对应的线程中执行,任务的取出遵循队列的FIFO原则:先进先出,后进后出。
3.4GCD的队列
3.4.1 GCD的队列可以分为2大类型:
1.并发队列(Current Dispatch Queue)
2.串行队列(Serial Dispatch Queue)
3.4.2 使用dispatch_queue_create函数创建队列
dispatch_queue_create(const char *label, dispatch_queue_attr_t attr);
其中:const char *label 列队列的名称
dispatch_queue_attr_t attr 队列的类型
创建一个并发队列
dispatch_queue_t queue1 = dispatch_queue_create("myQueue1", DISPATCH_QUEUE_CONCURRENT);
创建一个串行队列
dispatch_queue_t queue2 = dispatch_queue_create("myQueue2", DISPATCH_QUEUE_SERIAL);
GCD默认意境提供了全局的并发队列,供整个应用使用,可以无需手动创建
使用dispatch_get_global_queue 函数获得全局并发队列
//获得全局的并发队列
dispatch_queue_t queue3 = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 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 --》 后台
第二个参数标记:是为了未来使用保留的!所以这个参数应该永远指定为0
GCD中获得串行有2种途径
1.使用dispatch_queue_create 创建串行队列,创建串行队列(队列类型传递NULL或者DISPATCH_QUEUE_SERIAL)
dispatch_queue_t queue4 = dispatch_queue_create("com.520.queue", NULL);
2.使用主队列 (跟主线程相关联的队列),主队列时GCD自带的一种特殊的串行队列,放在主队列中的任务,都会放在主线程中执行,dispatch_get_main_queue()获得主队列
dispatch_queue_t queue5 = dispatch_get_main_queue();
3.4.3 应用
3.4.3.1 串行队里
3.4.3.2.并发队列(Concurrent Dispatch Queue)
//
// ViewController.m
// TestThread
//
// Created by denggaoqiang on 2022/9/10.
//
#import "ViewController.h"
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
[self example14];
}
- (void)example1 {
dispatch_queue_t queue = dispatch_queue_create("queueName", DISPATCH_QUEUE_SERIAL);
dispatch_sync(queue, ^{
NSLog(@"当前同步任务1一-------%@",[NSThread currentThread]);
sleep(3);
});
NSLog(@"1111");
dispatch_sync(queue, ^{
NSLog(@"当前同步任务2一-------%@",[NSThread currentThread]);
sleep(2);
});
NSLog(@"2222");
dispatch_sync(queue, ^{
NSLog(@"当前同步任务3一-------%@",[NSThread currentThread]);
sleep(1);
});
NSLog(@"走到这里了");
/** 打印结果
当前同步任务1
1111
当前同步任务2
2222
当前同步任务3
走到这里了
*/
}
- (void)example2 {
dispatch_queue_t queue = dispatch_queue_create("queueName", DISPATCH_QUEUE_SERIAL);
dispatch_async(queue, ^{
sleep(5);
NSLog(@"当前异步任务1一-------%@",[NSThread currentThread]);
});
NSLog(@"1111");
dispatch_async(queue, ^{
sleep(2);
NSLog(@"当前异步任务2一-------%@",[NSThread currentThread]);
});
NSLog(@"2222");
dispatch_async(queue, ^{
sleep(1);
NSLog(@"当前异步任务3一-------%@",[NSThread currentThread]);
});
NSLog(@"走到这里了");
/** 打印结果
1111
2222
走到这里了
当前异步任务1
当前异步任务2
当前异步任务3
*/
}
- (void)example3 {
dispatch_queue_t queue = dispatch_queue_create("queueName", DISPATCH_QUEUE_SERIAL);
dispatch_sync(queue, ^{
sleep(3);
NSLog(@"当前同步任务1一-------%@",[NSThread currentThread]);
});
NSLog(@"1111");
dispatch_async(queue, ^{
NSLog(@"当前异步任务2开始一-------%@",[NSThread currentThread]);
sleep(10);
NSLog(@"当前异步任务2结束一-------%@",[NSThread currentThread]);
});
NSLog(@"2222");
dispatch_sync(queue, ^{
sleep(2);
NSLog(<