iOS多线程机制
1、概述
1.1 进程特点
1.2 线程
- 线程是进程的基本执行单元,一个进程的所有任务都在线程中执行
- 进程要想执行任务,必须得有线程,进程至少要有一条线程
- 程序启动会默认开启一条线程,这条线程被称为主线程或 UI 线程
1.3 进程和线程的关系
- 地址空间:同一进程的线程共享本进程的地址空间,而进程之间则是独立的地址空间。
- 资源拥有:同一进程内的线程共享本进程的资源如内存、I/O、cpu等,但是进程之间 的资源是独立的。
- 一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃整个进程 都死掉。所以多进程要比多线程健壮。
- 进程切换时,消耗的资源大,效率高。所以涉及到频繁的切换时,使用线程要好于进程。 同样如果要求同时进行并且又要共享某些变量的并发操作,只能用线程不能用进程
- 执行过程:每个独立的进程有一个程序运行的入口、顺序执行序列和程序入口。但是线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。
- 线程是处理器调度的基本单位,但是进程不是。
1.4 多进程
- 进程是程序在计算机上的一次执行活动。当你运行一个程序,你就启动了一个进程。显然程序是死的(静态的),进程是活动的(动态的)。
-
进程可以分为系统进程和用户进程。
- 系统进程:凡是用于完成操作系统的各种功能的进程就是系统进程,他们就是出于运行状态下的操作系统本身
- 用户进程:运行用户程序时创建的运行在用户态下的进程。
- 进程又被细化为线程,也就是一个进程下有多个能独立运行的更小的单位。在同一个时间里,同一个计算机系统中如果允许两个或两个以上的进程处于运行状态,这便是多进程。
1.5 多线程
-
同一时间,CPU 只能处理1条线程,只有1条线程执行。多线程并发执行,其实是CPU快速地在多条线程之间调度(切换)。如果CPU的调度线程的时间足够快,就造成了多线程并发执行的假象。
-
如果线程非常至多(N条),CPU会在这些(N条)线程之间调度,消耗大量的CPU资源,每条线程被调用执行的频率会降低(线程的执行效率降低),因此是有最佳线程数的计算的。
-
多线程的优点:
- 能适当提高程序的执行效率
- 能适当提高资源的利用率(CPU、内存利用率)
-
多线程的缺点:
-
开启线程需要占用一定的内存空间(默认情况下,主线程占用1M,子线程占用512kb),如若开启大量线程,会占用大量的内存空间,就会降低程序的性能
-
线程越多,CPU在调度线程的开销就越大
-
程序设计更加复杂:如线程之间的通信、多线程之间的数据共享等
-
1.6 任务
就是执行操作的意思,也就是在线程中执行的那段代码。在DCD中是放在block 中的将要执行的代码。
执行任务的方式有两种:同步执行(sync)和异步执行(async)
-
同步(Sync): 同步添加任务到指定的队列中,在添加的任务执行结束之前,会一直等待,直到队列中的任务都完成之后再执行,所以会阻塞线程。 只能在当前线程中执行任务(是当前的线程,不一定是主线程),不具备开启新线程的能力。
//创建同步任务(queue是队列) dispatch_sync(queue, ^{ });
-
异步(Async): 线程会立即返回,无需等待就会继续执行下面的任务,不会阻塞当前线程。可以在线的线程中执行任务,具备开启线程的能力(注:具备这个能力,但并不代表一定开启新的线程)。如果不是添加在主队列上,异步会在子线程中执行任务。
//创建异步任务(queue是队列) dispatch_async(queue, ^{ });
1.7 队列
队列(Dispatch Queue): 这里的队列是指执行任务的等待队列,即用来存放任务的队列。队列是一种特殊的线性表,遵循 FIFO(先进先出) 的原则,即只能在队尾插入,队头删除。 没读取一个任务,则从队列中释放(删除)一个任务。
在GCD中有两种队列:串行队列和并发队列。两者都符合 FIFO 的原则,二者的主要区别是:执行的顺序不同和开启的线程数不同。
-
串行队列(Serial Dispatch Queue):
同一时间内,队列中只能执行一个任务,只有当前的任务执行完成之后,才能执行下一个任务。(只能开启一个线程,一个线程执行完毕后,再执行下一个任务)。
//第一个参数是队列的名字,自定义;第二个参数表示是串行还是并发 //DISPATCH_QUEUE_SERIAL = NULL,因此DISPATCH_QUEUE_SERIAL可以用NULL代替创建串行队列 dispatch_queue_t queue = dispatch_queue_create("shifx", DISPATCH_QUEUE_SERIAL);
-
主队列:是主线程上的一个串行队列,是系统自动为程序创建的。在主队列中调用不能在主线程再次调用同步任务,否则会造成堵塞,但是可以在其他线程中调用
-
并行队列(Concurrent Dispatch Queue):
同时允许多个任务同时执行。(可以开启多个线程,并且同时执行)。并发队列的并发功能只有在异步(dispatch_async) 函数下才有效。
//第一个参数是队列的名字,自定义;第二个参数表示是串行还是并发 dispatch_queue_t queue = dispatch_queue_create("shifx", DISPATCH_QUEUE_CONCURRENT);
-
全局队列:系统提供的并发队列
1.8 任务和队列
- 串行队列+同步执行
- 并发队列+同步执行
- 串行队列+异步执行
- 并发队列+异步执行
- 串行主队列
- 全局并发队列
1.8.1串行队列+同步执行
-(void)serialSysncText { //串行队列 dispatch_queue_t serial = dispatch_queue_create("shifx", DISPATCH_QUEUE_SERIAL); NSLog(@"串行同步任务开始"); dispatch_sync(serial, ^{ sleep(1); for (int i = 0; i<3; i++) { NSLog(@"串行同步任务1 = %d",i); } }); dispatch_sync(serial, ^{ sleep(1); for (int i = 0; i<3; i++) { NSLog(@"串行同步任务2 = %d",i); } }); NSLog(@"串行同步任务结束"); }
从执行结果可以看出,串行同步任务是顺序执行的,任务2只有等任务1完成之后才会开始。
1.8.2 并发队列+同步任务
-(void)concurrentAsysncText { //并发队列 dispatch_queue_t concurrent = dispatch_queue_create("shifx", DISPATCH_QUEUE_CONCURRENT); NSLog(@"并发同步任务开始"); dispatch_sync(concurrent, ^{ sleep(1); for (int i = 0; i<3; i++) { NSLog(@"并发同步任务1 = %d",i); } }); dispatch_sync(concurrent, ^{ sleep(1); for (int i = 0; i<3; i++) { NSLog(@"并发同步任务2 = %d",i); } }); NSLog(@"并发同步任务结束"); }
1.8.3 串行队列+异步执行
-(void)serialAsysncText { //串行队列 dispatch_queue_t serial = dispatch_queue_create("shifx", DISPATCH_QUEUE_SERIAL); NSLog(@"串行异步任务开始"); dispatch_async(serial, ^{ for (int i = 0; i<3; i++) { sleep(1); NSLog(@"串行异步任务1 = %d",i); NSLog(@"当前线程:%@",[NSThread currentThread]); } }); dispatch_async(serial, ^{ for (int i = 0; i<3; i++) { sleep(1); NSLog(@"串行异步任务2 = %d",i); NSLog(@"当前线程:%@",[NSThread currentThread]); } }); NSLog(@"串行异步任务结束"); }
从执行结果来看:
- 所有的异步任务都是在打印串行异步任务结束后才开始执行的;这意味着,异步任务是主线程开启的子线程中进行的。
- 从线程信息可以看到,开启了新的线程,但也只开启了一个线程;
- 在串行队列中的异步任务还是按照先后顺序执行的;
1.8.4 并发队列+异步执行
-(void)concurrentAsysncText { //并发队列 dispatch_queue_t concurrent = dispatch_queue_create("shifx", DISPATCH_QUEUE_CONCURRENT); NSLog(@"并发异步任务开始"); dispatch_async(concurrent, ^{ for (int i = 0; i<3; i++) { sleep(1); NSLog(@"并发异步任务1 = %d",i); NSLog(@"当前线程:%@",[NSThread currentThread]); } }); dispatch_async(concurrent, ^{ for (int i = 0; i<3; i++) { sleep(1); NSLog(@"并发异步任务2 = %d",i); NSLog(@"当前线程:%@",[NSThread currentThread]); } }); NSLog(@"并发异步任务结束"); }
- 所有的异步任务都是在打印并发异步任务结束后开始执行,这还是意味着,主线程开启型线程去执行异步任务。
- 由于有两个异步任务,所以并发队列就开了两个新的线程去执行,这一点从打印的线程编号也可以看出。
- 两个异步任务的执行是没有先后顺序的。
1.8.5 主队列+同步任务
//使用 NSThread 的 detachNewThreadWithBlock 方法会创建线程 [NSThread detachNewThreadWithBlock:^{ [self mainSysncText]; }]; -(void)mainSysncText { NSLog(@"当前所在的线程:%@",[NSThread currentThread]); dispatch_sync(dispatch_get_main_queue(), ^{ for (int i = 0; i<2; i++) { sleep(1); NSLog(@"主队列同步任务1 = %d",i); NSLog(@"当前线程:%@",[NSThread currentThread]); } }); dispatch_sync(dispatch_get_main_queue(), ^{ for (int i = 0; i<2; i++) { sleep(1); NSLog(@"主队列同步任务2 = %d",i); NSLog(@"当前线程:%@",[NSThread currentThread]); } }); }
1.8.6 主队列+异步任务
-(void)mainAsysncText { NSLog(@"当前所在的线程:%@",[NSThread currentThread]); NSLog(@"主队列异步任务开始"); dispatch_async(dispatch_get_main_queue(), ^{ for (int i = 0; i<2; i++) { sleep(1); NSLog(@"主队列异步任务1 = %d",i); NSLog(@"当前线程:%@",[NSThread currentThread]); } }); dispatch_async(dispatch_get_main_queue(), ^{ for (int i = 0; i<2; i++) { sleep(1); NSLog(@"主队列异步任务2 = %d",i); NSLog(@"当前线程:%@",[NSThread currentThread]); } }); NSLog(@"主队列异步任务结束"); }
1.9 多线程生命周期
- 新建:实例化线程对象
- 就绪:向线程对象发送start消息,线程对象被加入可调度线程池等待CPU调度。
- 运行:CPU 负责调度可调度线程池中线程的执行。线程执行完成之前,状态可能会在就绪和运行之间来回切换。就绪和运行之间的状态变化由CPU负责,程序员不能干预。
- 阻塞:当满足某个预定条件时,可以使用休眠或锁,阻塞线程执行。sleepForTimeInterval(休眠指定时长),sleepUntilDate(休眠到指定日期),@synchronized(self):(互斥锁)。
- 死亡:正常死亡,线程执行完毕。非正常死亡,当满足某个条件后,在线程内部中止执行/在主线程中止线程对象
1.10 线程池原理
- 创建线程执行任务
- 先判断线程池工作队列是否已满
- 若没满就将任务push进队列
- 若已满时,且maximumPoolSize>corePoolSize,将创建新的线程来执行任务
- 反之则交给饱和策略去处理
- AbortPolicy直接抛出RejectedExecutionExeception异常来阻止系统正常运行
- CallerRunsPolicy将任务回退到调用者
- DisOldestPolicy丢掉等待最久的任务
- DisCardPolicy直接丢弃任务
参数意义:
2、iOS中的多线程机制
2.1 NSThread
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(testThread:) object:@"参数"];//使用初始化方法出来的线程需要手动启动 [thread start] //启动线程 thread.name = @"NsThread线程"; thread.threadPriority = 1; //设置优先级。范围0~1,表示执行概率,其值越大,表示该线程越容易被执行。由于是概率,并不能完全按照此实现想要的执行顺序 [thread cancel];//取消已经启动的线程 [NSThread detachNewThreadSelector:@selector(testThread:) toTarget:self withObject:@"构造器方法"];//使用构造器方式新建并启动一个线程
2.2.1 相关用法
// 获得主线程
+ (NSThread *)mainThread;
// 判断是否为主线程(对象方法)
- (BOOL)isMainThread;
// 判断是否为主线程(类方法)
+ (BOOL)isMainThread;
// 获得当前线程
NSThread *current = [NSThread currentThread];
//打印当前线程信息: NSLog(@"%@", [NSThread currentThread]); // 线程的名字——setter方法
- (void)setName:(NSString *)n;
// 线程的名字——getter方法
- (NSString *)name;
2.2.2 状态控制
//1.启动线程 - (void)start; // 线程进入就绪状态 -> 运行状态。当线程任务执行完毕,自动进入死亡状态 //2.阻塞线程 + (void)sleepUntilDate:(NSDate *)date; + (void)sleepForTimeInterval:(NSTimeInterval)ti; // 线程进入阻塞状态 //3.强制停止线程,杀死线程 + (void)exit; // 线程进入死亡状态
2.2.3 线程通信
// 在主线程上执行操作 - (void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)arg waitUntilDone:(BOOL)wait; - (void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)arg waitUntilDone:(BOOL)wait modes:(NSArray<NSString *> *)array; // equivalent to the first method with kCFRunLoopCommonModes // 在指定线程上执行操作 - (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(id)arg waitUntilDone:(BOOL)wait modes:(NSArray *)array NS_AVAILABLE(10_5, 2_0); - (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(id)arg waitUntilDone:(BOOL)wait NS_AVAILABLE(10_5, 2_0); // 在当前线程上执行操作,调用 NSObject 的 performSelector:相关方法 - (id)performSelector:(SEL)aSelector; - (id)performSelector:(SEL)aSelector withObject:(id)object; - (id)performSelector:(SEL)aSelector withObject:(id)object1 withObject:(id)object2;
下面通过一个经典的下载图片 DEMO 来展示线程之间的通信。具体步骤如下:
- 开启一个子线程,在子线程中下载图片。
- 回到主线程刷新 UI,将图片展示在 UIImageView 中。
DEMO 代码如下:
/** * 创建一个线程下载图片 */ - (void)downloadImageOnSubThread { // 在创建的子线程中调用downloadImage下载图片 [NSThread detachNewThreadSelector:@selector(downloadImage) toTarget:self withObject:nil]; } /** * 下载图片,下载完之后回到主线程进行 UI 刷新 */ - (void)downloadImage { NSLog(@"current thread -- %@", [NSThread currentThread]); // 1. 获取图片 imageUrl NSURL *imageUrl = [NSURL URLWithString:@"https://ysc-demo-1254961422.file.myqcloud.com/YSC-phread-NSThread-demo-icon.jpg"]; // 2. 从 imageUrl 中读取数据(下载图片) -- 耗时操作 NSData *imageData = [NSData dataWithContentsOfURL:imageUrl]; // 通过二进制 data 创建 image UIImage *image = [UIImage imageWithData:imageData]; // 3. 回到主线程进行图片赋值和界面刷新 [self performSelectorOnMainThread:@selector(refreshOnMainThread:) withObject:image waitUntilDone:YES]; } /** * 回到主线程进行图片赋值和界面刷新 */ - (void)refreshOnMainThread:(UIImage *)image { NSLog(@"current thread -- %@", [NSThread currentThread]); // 赋值图片到imageview self.imageView.image = image; }
2.2.4 线程安全和线程同步
- 线程安全:
若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作(更改变量),一般都需要考虑线程同步,否则的话就可能影响线程安全。
- 线程同步:
可理解为线程 A 和 线程 B 一块配合,A 执行到一定程度时要依靠线程 B 的某个结果,于是停下来,示意 B 运行;B 依言执行,再将结果给 A;A 再继续操作。举个简单例子就是:两个人在一起聊天。两个人不能同时说话,避免听不清(操作冲突)。等一个人说完(一个线程结束操作),另一个再说(另一个线程再开始操作)。
下面,我们模拟火车票售卖的方式,实现 NSThread 线程安全和解决线程同步问题。场景:总共有50张火车票,有两个售卖火车票的窗口,一个是北京火车票售卖窗口,另一个是上海火车票售卖窗口。两个窗口同时售卖火车票,卖完为止。
(1)NSThread非线程安全的情况/** * 初始化火车票数量、卖票窗口(非线程安全)、并开始卖票 */ - (void)initTicketStatusNotSave { // 1. 设置剩余火车票为 50 self.ticketSurplusCount = 50; // 2. 设置北京火车票售卖窗口的线程 self.ticketSaleWindow1 = [[NSThread alloc]initWithTarget:self selector:@selector(saleTicketNotSafe) object:nil]; self.ticketSaleWindow1.name = @"北京火车票售票窗口"; // 3. 设置上海火车票售卖窗口的线程 self.ticketSaleWindow2 = [[NSThread alloc]initWithTarget:self selector:@selector(saleTicketNotSafe) object:nil]; self.ticketSaleWindow2.name = @"上海火车票售票窗口"; // 4. 开始售卖火车票 [self.ticketSaleWindow1 start]; [self.ticketSaleWindow2 start]; } /** * 售卖火车票(非线程安全) */ - (void)saleTicketNotSafe { while (1) { //如果还有票,继续售卖 if (self.ticketSurplusCount > 0) { self.ticketSurplusCount --; NSLog(@"%@", [NSString stringWithFormat:@"剩余票数:%ld 窗口:%@", self.ticketSurplusCount, [NSThread currentThread].name]); [NSThread sleepForTimeInterval:0.2]; } //如果已卖完,关闭售票窗口 else { NSLog(@"所有火车票均已售完"); break; } } }可以看到在不考虑线程安全的情况下,得到票数是错乱的,这样显然不符合我们的需求,所以我们需要考虑线程安全问题。
(2)NSThread考虑线程安全的情况
/** * 初始化火车票数量、卖票窗口(线程安全)、并开始卖票 */ - (void)initTicketStatusSave { // 1. 设置剩余火车票为 50 self.ticketSurplusCount = 50; // 2. 设置北京火车票售卖窗口的线程 self.ticketSaleWindow1 = [[NSThread alloc]initWithTarget:self selector:@selector(saleTicketSafe) object:nil]; self.ticketSaleWindow1.name = @"北京火车票售票窗口"; // 3. 设置上海火车票售卖窗口的线程 self.ticketSaleWindow2 = [[NSThread alloc]initWithTarget:self selector:@selector(saleTicketSafe) object:nil]; self.ticketSaleWindow2.name = @"上海火车票售票窗口"; // 4. 开始售卖火车票 [self.ticketSaleWindow1 start]; [self.ticketSaleWindow2 start]; } /** * 售卖火车票(线程安全) */ - (void)saleTicketSafe { while (1) { // 互斥锁 @synchronized (self) { //如果还有票,继续售卖 if (self.ticketSurplusCount > 0) { self.ticketSurplusCount --; NSLog(@"%@", [NSString stringWithFormat:@"剩余票数:%ld 窗口:%@", self.ticketSurplusCount, [NSThread currentThread].name]); [NSThread sleepForTimeInterval:0.2]; } //如果已卖完,关闭售票窗口 else { NSLog(@"所有火车票均已售完"); break; } } } }可以看出,在考虑了线程安全的情况下,加锁之后,得到的票数是正确的,没有出现混乱的情况。我们也就解决了多个线程同步的问题。实质上就是使用互斥锁来保护临界资源在同一时刻只能被一个线程访问,从而实现线程安全。
2.2.5 线程的状态转换
- 当我们新建一个线程:
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil];
- 在内存中表现为:
- 当调用[thread start]之后,系统把线程对象放入可调度线程池中,线程对象进入了就绪状态:
- 如果CPU现在调度当前线程对象,则当前线程对象进入运行状态,如果CPU调度其他线程对象,则当前线程对象回到就绪状态。
- 如果CPU在运行当前线程对象的时候调用了sleep方法\等待同步锁,则当前线程对象就进入了阻塞状态,等到sleep到时\得到同步锁,则回到就绪状态。
- 如果CPU在运行当前线程对象的时候线程任务执行完毕\异常强制退出,则当前线程对象进入死亡状态。
2.2 GCD
Grand Central Dispatch(GCD)是Apple开发的一个多核编程的技术方案。它是一套纯c语言的api,主要用于优化应用程序以支持多核处理器以及其他对称多处理系统。它是一个在线程池模式的基础上执行的并发任务。- GCD支持多核并行计算
- GCD自动管理线程的生命周期(线程的创建、调起、等待、销毁)
- 使用者只需告知GCD执行任务
2.2.1 并发时的资源竞争
-
原子操作(Atomic Operations):一种简单的同步形式,适用于简单的数据类型。 原子操作的优点是它们不会阻塞竞争线程。 对于简单的操作(例如增加计数器变量),这比使用锁可以带来更好的性能。
-
内存壁垒和易失性变量(Memory Barriers and Volatile Variables):内存屏障是一种非阻塞同步工具,用于确保内存操作以正确的顺序发生。内存屏障的作用类似于围栏,迫使处理器在允许执行位于屏障之后的加载和存储操作之前,完成位于屏障前面的所有加载和存储操作。易失性变量将另一种类型的内存约束应用于单个变量,它具有“易变性”,声明为volatile变量编译器会强制要求读内存,相关语句不会直接使用上一条语句对应的的寄存器内容,而是重新从内存中读取。。由于内存屏障和易失性变量都会减少编译器可执行的优化次数,因此应谨慎使用它们,并且仅在需要确保正确性的地方使用它们。
-
锁(Locks):锁是最常用的同步工具之一,可以使用锁来保护代码的关键部分,锁一次只能允许一个线程访问。例如,关键代码可能操纵特定的数据结构或一次使用最多支持一个客户端的某些资源。通过在此部分周围加锁,可以排除其他线程进行可能影响代码正确性的更改。
-
条件(Conditions):条件是一种信号量,同时也可以看做一种特殊的锁。当某个条件为真时,它允许线程彼此发信号。条件通常用于指示资源的可用性或确保任务以特定顺序执行。当线程测试条件时,除非该条件已经为真,否则它将阻塞。它保持阻塞状态,直到其他线程显式更改并发出条件信号为止。条件和互斥锁之间的区别在于,可以允许多个线程同时访问该条件。条件更多的表现得像一个看门人,它根据某些指定的标准让不同的线程通过这个门。
-
执行选择器事务(Perform Selector Routines):Cocoa框架中可以使用performSelector:onThread:withObject:waitUntilDone:相关的方法来在主线程或者其他线程的Runloop中顺序执行代码。
对于应用开发而言最最最常接触到就是互斥锁、递归锁这两种锁了,它们在API层面上又被封装为NSLock、NSRecursiveLock或者@synchronized语法了。这些锁通过保证线程同步访问的方式保护了资源从而解决了资源竞争的问题,但是它并不能完美解决并发编程中的所有问题,可能还会引发几个问题:
- 死锁(经典场景:使用互斥锁的线程加锁后又再次访问这个锁)
- 资源饥饿
- 线程优先级反转
2.2.2 GCD中任务与调度队列
- 创建/获取一个并发/串行的队列;
- 创建任务:确定要执行的事情,就是写匿名函数block,也就是要执行的代码块;
- 将任务加入到队列中:同时需要指定任务的执行方式,即同步还是异步。
// 1.创建一个队列 dispatch_queue_t queue = dispatch_queue_create("myqueue", DISPATCH_QUEUE_SERIAL); // 2.创建一个任务 dispatch_block_t block = ^{ NSLog(@"%@",[NSThread currentThread]); }; // 3.将任务添加进队列中(同时指定任务的执行方式) dispatch_async(queue, block);
完成这三步之后,就是OS要完成的事情了。在GCD中,GCD会自动将队列中的任务取出,放到对应的线程中去执行。任务的取出遵循队列的FIFO原则,即先进先出,后进后出。GCD中,当要执行队列中的任务的时候,会自动开启一个新线程(不是一定会开启新线程的,比如串行队列,以及同步任务就不具有开启新线程的能力)。如果接下来还要继续执行任务的话就会从线程池中取出线程,而不是再去新建一个线程,这也就节省了创建线程所需要的时间。但是如果在一段时间内没有执行任务的话,该线程就会被销毁,再执行任务就再去创建新的线程。
● dispatch_block_t(常用)
提交给指定队列的 block,无参无返回值。
typedef void (^dispatch_block_t)(void);
● dispatch_function_t
提交给指定队列的 function,void(*)()类型的函数指针。
typedef void (*dispatch_function_t)(void *);
dispatch_queue_t serial = dispatch_queue_create("A", DISPATCH_QUEUE_SERIAL); dispatch_queue_t conque = dispatch_queue_create("B", DISPATCH_QUEUE_CONCURRENT); dispatch_queue_t mainQueue = dispatch_get_main_queue(); dispatch_queue_t globQueue = dispatch_get_global_queue(0, 0); NSLog(@"%@-%@-%@-%@",serial,conque,mainQueue,globQueue);关于任务和队列的组合情况在1.8小节有详细的说明,这里就不重复了。我们接下来先看GCD执行任务的方式的区别,以及一些重点函数的说明。
void dispatch_sync(dispatch_queue_t queue, dispatch_block_t block); void dispatch_sync_f(dispatch_queue_t queue, void *context, dispatch_function_t work);这两个都是将任务添加到队列中,并指定任务的执行方式为同步。dispatch_sync 提交了一个block对象到指定队列以同步执行,并在该block完成之后返回(阻塞),由于阻塞的特性,使用该函数需要注意死锁的问题。dispatch_sync _f则是提交一个function到指定队列以同步执行,并在该function完成后返回(阻塞)。
(4)dispatch_async & dispatch_async_f
void dispatch_async(dispatch_queue_t queue, dispatch_block_t block);
(5)dispatch_get_main_queue
// @return 主队列
dispatch_queue_main_t dispatch_get_main_queue(void);
① 调用 dispatch_main();
② 调用 UIApplicationMain(iOS)或者 NSApplicationMain(macOS);
③ 在主线程使用 CFRunLoopRef。
大多数情况下我们的应用程序会在 main() 函数里使用第 2 种方式
/*! * @param identifier * 队列的服务质量,传0就是默认 * * @param flags * 苹果留着以后用的,传0就行 * * @return dispatch_queue_global_t * 可以指定服务质量的系统定义的全局并发队列 */ dispatch_queue_global_t dispatch_get_global_queue(long identifier, unsigned long flags);一种特殊的并发队列,可以指定服务质量(服务质量有助于确定队列执行任务的优先级)。这里需要注意:对主队列和全局并发队列使用dispatch_suspend、dispatch_resume、dispatch_set_context是无效的。另外,这里再学习下全局并发队列与手动创建的并发队列的区别:
typedef NSObject<OS_dispatch_queue> *dispatch_queue_t;应用程序向其提交block(任务,代码块)以进行后续执行的轻量级对象。首先,队列是一个对象,这也解释了为什么在MRC中要手动管理dispatch_queue_t的内存。这是一个队列,遵循FIFO原则,分为”串行“和”并发“两种队列。
/*! * @param label * 给队列一个字符串标签进行唯一标识,以便在调试时区分队列 * 建议使用反向DNS命名方式(com.example.myqueue) * 该参数可以为空(NULL) * * @param attr * 指定队列类型 * DISPATCH_QUEUE_SERIAL 为串行队列 * DISPATCH_QUEUE_CONCURRENT 为并发队列 * 该参数可以为空(NULL),传空时默认为串行队列(在iOS4.3版本之前该参数只能传空) * * @return dispatch_queue_t * 新创建的队列 */ dispatch_queue_t dispatch_queue_create(const char *label, dispatch_queue_attr_t attr); // 创建一个串行队列 dispatch_queue_t queue = dispatch_queue_create("com.junteng.myqueue", DISPATCH_QUEUE_SERIAL); // 创建一个并发队列 dispatch_queue_t queue = dispatch_queue_create("com.junteng.myqueue", DISPATCH_QUEUE_CONCURRENT); // 获取主队列 dispatch_queue_t queue = dispatch_get_main_queue(); // 获取全局并发队列 dispatch_queue_t queue = dispatch_get_global_queue(0, 0);(9) dispatch_queue_get_label
/*! * @param queue * 需要获取label的队列; * 如果需要获取当前队列的label则使用 DISPATCH_CURRENT_QUEUE_LABEL * * @return * 创建队列时给队列设置的标签 */ const char * dispatch_queue_get_label(dispatch_queue_t queue); dispatch_queue_t queue = dispatch_queue_create("com.junteng.myqueue", NULL); dispatch_sync(queue, ^{ NSLog(@"%s", dispatch_queue_get_label(DISPATCH_CURRENT_QUEUE_LABEL)); }); NSLog(@"%s", dispatch_queue_get_label(DISPATCH_CURRENT_QUEUE_LABEL)); NSLog(@"%s", dispatch_queue_get_label(queue)); /* com.junteng.myqueue com.apple.main-thread com.junteng.myqueue */
2.2.3 死锁
/* 队列的特点:FIFO (First In First Out) 先进先出 以下将 block(任务2)提交到主队列,主队列将来要取出这个任务放到主线程执行。 而主队列此时已经有任务,就是执行(viewDidLoad方法), 所以主队列要想取出 block(任务2),就要等上一个任务(viewDidLoad方法)先执行完,才能取出该任务执行。 而 dispatch_sync 函数必须执行完 block(任务2)才会返回,才能往下执行代码。 所以(任务2)要等待(viewDidLoad方法)执行完,(viewDidLoad方法)要等待(任务2)执行完。互相等待,就产生了死锁。 */ - (void)viewDidLoad { [super viewDidLoad]; NSLog(@"执行任务1"); dispatch_queue_t queue = dispatch_get_main_queue(); dispatch_sync(queue, ^{ NSLog(@"执行任务2"); }); NSLog(@"执行任务3"); } /* 打印: 2020-01-19 00:16:26.980630+0800 多线程[25011:5507937] 执行任务1 (lldb) */ /* 解决方案:打破(使用`dispatch_sync`函数往`当前串行队列`中添加任务)这一条件即可 以下将(任务2)异步执行,打印结果为:132 */ - (void)viewDidLoad { [super viewDidLoad]; NSLog(@"执行任务1"); dispatch_queue_t queue = dispatch_get_main_queue(); dispatch_async(queue, ^{ NSLog(@"执行任务2"); }); NSLog(@"执行任务3"); } /* 打印: 2020-01-19 03:16:47.472682+0800 多线程[25416:5603048] 执行任务1 2020-01-19 03:16:47.472890+0800 多线程[25416:5603048] 执行任务3 2020-01-19 03:16:47.474389+0800 多线程[25416:5603048] 执行任务2 */
2.3 NSOperationQueue
- GCD是纯C语言的API,NSOperationQueue 则是基于GCD的OC版本封装。
- GCD只支持FIFO的队列,NSOperationQueue可以添加任务依赖,方便控制执行顺序
- 可以设定操作执行的优先级
- 任务执行状态控制:isReady,isExecuting,isFinished,isCancelled
- 可以设置最大并发量
- GCD的执行速度比NSOperationQueue 快
2.3.1 基本概念
- 执行操作的意思,换句话说就是你在线程中执行的那段代码。
- 在 GCD 中是放在 block 中的。在 NSOperation 中,我们使用 NSOperation 子类NSInvocationOperation、NSBlockOperation,或者自定义子类来封装操作。
- 这里的队列指操作队列,即用来存放操作的队列。不同于 GCD 中的调度队列 FIFO(先进先出)的原则。NSOperationQueue 对于添加到队列中的操作,首先进入准备就绪的状态(就绪状态取决于操作之间的依赖关系),然后进入就绪状态的操作的开始执行顺序(非结束执行顺序)由操作之间相对的优先级决定(优先级是操作对象自身的属性)。
- 操作队列通过设置最大并发操作数(maxConcurrentOperationCount)来控制并发、串行。
- NSOperationQueue 为我们提供了两种不同类型的队列:主队列和自定义队列。主队列运行在主线程之上,而自定义队列在后台执行。
- NSOperation 需要配合 NSOperationQueue 来实现多线程。因为默认情况下,NSOperation 单独使用时系统同步执行操作,配合 NSOperationQueue 我们能更好的实现异步执行。
-
NSOperation 实现多线程的使用步骤分为三步
- 创建操作:先将需要执行的操作封装到一个 NSOperation 对象中。
- 创建队列:创建 NSOperationQueue 对象。
- 将操作加入到队列中:将 NSOperation 对象添加到 NSOperationQueue 对象中。
- 之后呢,系统就会自动将 NSOperationQueue 中的 NSOperation 取出来,在新线程中执行操作。
2.3.2 创建操作
NSOperation 是个抽象类,不能用来封装操作。我们只有使用它的子类来封装操作。我们有三种方式来封装操作。
- 使用子类 NSInvocationOperation
- 使用子类 NSBlockOperation
- 自定义继承 NSOperation 的子类,通过实现内部相应的方法来封装操作。
/**
NSInvocationOperation : 创建操作 ---> 创建队列 ---> 操作加入队列
*/
- (void)demo{
//1:创建操作
NSInvocationOperation *op = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(handleInvocation:) object:@"123"];
//2:创建队列
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
//3:操作加入队列 --- 操作会在新的线程中
[queue addOperation:op];
}
- (void)handleInvocation:(id)op{
NSLog(@"%@ --- %@",op,[NSThread currentThread]);
} 复制代码
打印:
2020-01-29 15:43:11.012492+0800 001---NSOperation初体验[1628:78932] 123 --- <NSThread: 0x600001ea9080>{number = 3,
NSInvocationOperation也可以手动调起,这样不会开启线程,会在当前队列执行任务
/**
手动调起操作
*/
- (void)demo1{
NSInvocationOperation *op = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(handleInvocation:) object:@"123"];
// 注意 : 如果该任务已经添加到队列,你再手动调回出错
// NSOperationQueue *queue = [[NSOperationQueue alloc] init];
// [queue addOperation:op];
// 手动调起操作,默认在当前线程
[op start];
}
- (void)handleInvocation:(id)op{
NSLog(@"%@ --- %@",op,[NSThread currentThread]);
} 复制代码
打印:
2020-01-29 15:52:55.404726+0800 001---NSOperation初体验[1740:83188] 123 --- <NSThread: 0x60000024e940>{number = 1, name = main} 复制代码
注意:我们尽量不要用这种方式去执行任务,如果该任务已经添加到队列,再手动调起任务程序会崩溃
3、runloop和多线程
3.1 概念
那什么是 run loop?顾名思义,run loop 就是在 “跑圈”,run loop 运行的核心代码是一个有状态的 do while 循环,每循环一次就相当于跑了一圈,线程就会对当前这一圈里面产生的事件进行处理,do while 循环我们可能已经写过无数次,当然我们日常在函数中写的都是会明确结束的循环,并且循环的内容是我们一开始就编写好的,我们并不能动态的改变或者插入循环的内容,而 run loop 则不同,只要不是超时或者故意退出状态下 run loop 就会一直执行 do while 循环,所以可以保证线程不退出,并且可以让我们根据自己需要向线程中添加任务。
那么为什么线程要有 run loop 呢?前面提到了一点,是为了降低系统性能开销,其实还有一点也很重要。我们的 APP 可以理解为是靠 event 驱动的(包括 iOS 和 Android 应用)。我们触摸屏幕、网络回调等都是一个个的 event,也就是事件。这些事件产生之后会分发给我们的 APP,APP 接收到事件之后分发给对应的线程。通常情况下,如果线程没有 run loop,那么一个线程一次只能执行一个任务,执行完成后线程就会退出。要想 APP 的线程一直能够处理事件或者等待事件(比如异步事件),就要保活线程,也就是不能让线程早早的退出,此时 run loop 就派上用场了,其实也不是必须要给线程指定一个 run loop,如果需要我们的线程能够持续的处理事件,那么就需要给线程绑定一个 run loop。也就是说,run loop 能够保证线程一直可以处理事件。
通常情况下,事件并不是永无休止的产生,所以也就没必要让线程永无休止的运行,run loop 可以在无事件处理时进入休眠状态,避免无休止的 do while 跑空圈,看到这里我们注意到线程和 run loop 都是能进入休眠状态的,这里为了便于理解概念我们看一些表示 run loop 运行状态的代码:
/* Run Loop Observer Activities */ typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) { kCFRunLoopEntry = (1UL << 0), // 进入 Run Loop 循环 (这里其实还没进入) kCFRunLoopBeforeTimers = (1UL << 1), // Run Loop 即将开始处理 Timer kCFRunLoopBeforeSources = (1UL << 2), // Run Loop 即将开始处理 Source kCFRunLoopBeforeWaiting = (1UL << 5), // Run Loop 即将进入休眠 kCFRunLoopAfterWaiting = (1UL << 6), // Run Loop 从休眠状态唤醒 kCFRunLoopExit = (1UL << 7), // Run Loop 退出(和 kCFRunLoop Entry 对应) kCFRunLoopAllActivities = 0x0FFFFFFFU };
3.2 runloop和线程的关系
3.2.1 main runloop
#import <UIKit/UIKit.h> #import "AppDelegate.h" int main(int argc, char * argv[]) { NSString * appDelegateClassName; @autoreleasepool { // Setup code that might create autoreleased objects goes here. appDelegateClassName = NSStringFromClass([AppDelegate class]); NSLog(@"🏃🏃♀️🏃🏃♀️..."); // 这里插入一行打印语句 } return UIApplicationMain(argc, argv, nil, appDelegateClassName); // return 0; // 把上面的 return UIApplicationMain(argc, argv, nil, appDelegateClassName); 语句拆开如下: // int result = UIApplicationMain(argc, argv, nil, appDelegateClassName); // return result; // ⬅️ 在此行打一个断点,执行程序会发现此断点是无效的,因为 main 函数根本不会执行到这里 }
UIKIT_EXTERN int UIApplicationMain(int argc, char * _Nullable argv[_Nonnull], NSString * _Nullable principalClassName, NSString * _Nullable delegateClassName);
UIApplicationMain Creates the application object and the application delegate and sets up the event cycle. Return Value Even though an integer return type is specified, this function never returns. When users exits an iOS app by pressing the Home button, the application moves to the background. 即使指定了整数返回类型,此函数也从不返回。当用户通过按 Home 键退出 iOS 应用时,该应用将移至后台。 Discussion ... It also sets up the main event loop, including the application’s run loop, and begins processing events. ... Despite the declared return type, this function never returns. ... 它还设置 main event loop,包括应用程序的 run loop(main run loop),并开始处理事件。... 尽管声明了返回类型,但此函数从不返回。
在开发者文档中查看UIApplicationMain函数,摘要告诉我们UIApplicationMain函数完成:创建应用程序对象和应用程序代理并设置 event cycle,看到 Return Value 一项 Apple 已经明确告诉我们UIApplicationMain函数是不会返回的,并且在 Discussion 中也告诉我们UIApplicationMain函数启动了 main run loop 并开始着手为我们处理事件。
main函数是我们应用程序的启动入口,然后调用UIApplicationMain函数其内部帮我们开启了 main run loop,换个角度试图理解为何我们的应用程序不退出时,是不是可以理解为我们的应用程序自启动开始就被包裹在 main run loop 的 do while 循环 中呢?那么根据上面UIApplicationMain函数的功能以及我们对 runloop 概念的理解,大概可以书写出如下 runloop 的伪代码: