iOS多线程
https://juejin.cn/post/6844903566398717960
什么是GCD
Grand Central Dispatch(GCD) 是 Apple 开发的一个多核编程的较新的解决方法。它主要用于优化应用程序以支持多核处理器以及其他对称多处理系统。
GCD的用处:
- GCD 可用于多核的并行运算;
- GCD 会自动利用更多的 CPU 内核(比如双核、四核);
- GCD 会自动管理线程的生命周期(创建线程、调度任务、销毁线程);
- 程序员只需要告诉 GCD 想要执行什么任务,不需要编写任何线程管理代码。
任务与队列
任务:就是执行操作的意思,换句话说就是你在线程中执行的那段代码。
同步执行(sync):
- 队列中的其他任务需要等待这个任务执行完才能执行,或者说它会阻塞当前队列(本质上应该是阻塞线程,但阻塞队列更好理解)
- 只能在当前线程中执行任务,不具备开启新线程的能力。
异步执行(async):
- 其他任务无需等待这个任务执行完就可以执行 ;
- 可以在新的线程中执行任务,具备开启新线程的能力。(但不一定开启新线程)
队列(Dispatch Queue):这里的队列指执行任务的等待队列,即用来存放任务的队列。队列是一种特殊的线性表,采用 FIFO(先进先出)的原则,即新任务总是被插入到队列的末尾,而读取任务的时候总是从队列的头部开始读取。
在 GCD 中有两种队列:『串行队列』 和 『并发队列』。两者都符合 FIFO(先进先出)的原则。两者的主要区别是:执行顺序不同,以及开启线程数不同。
串行队列(Serial Dispatch Queue):
- 每次只有一个任务被执行。让任务一个接着一个地执行。(只开启一个线程,一个任务执行完毕后,再执行下一个任务)
并发队列(Concurrent Dispatch Queue):
- 可以让多个任务并发(同时)执行。(可以开启多个线程,并且同时执行任务)
注意第二张图的并发功能只有在异步方法下才有效 ;
使用步骤
1.创建一个队列
2.向队列中追加不同类型的任务 ;
队列的创建
:可以使用 dispatch_queue_create
方法来创建队列。该方法需要传入两个参数:
-
第一个参数表示队列的唯一标识符,相当于id
-
第二个参数用来识别是串行队列还是并发队列。
DISPATCH_QUEUE_SERIAL
表示串行队列,DISPATCH_QUEUE_CONCURRENT
表示并发队列。
// 串行队列的创建方法
dispatch_queue_t queue = dispatch_queue_create("net.bujige.testQueue", DISPATCH_QUEUE_SERIAL);
// 并发队列的创建方法
dispatch_queue_t queue = dispatch_queue_create("net.bujige.testQueue", DISPATCH_QUEUE_CONCURRENT);
还有一些可以直接获取的默认队列 :
- 使用
dispatch_get_main_queue()
方法获得主队列。 - 使用
dispatch_get_global_queue
方法来获取全局并发队列。需要传入两个参数。第一个参数表示队列优先级,一般用DISPATCH_QUEUE_PRIORITY_DEFAULT
。第二个参数暂时没用,用0
即可。
注意:主队列其实并不特殊。 主队列的实质上就是一个普通的串行队列,只是因为默认情况下,当前代码是放在主队列中的,然后主队列中的代码,又都会放到主线程中去执行,所以才造成了主队列特殊的现象。(或者说主队列特殊在它其实应该算一种嵌套,下面会讲) ;
任务的创建
GCD 提供了同步执行任务的创建方法 dispatch_sync
和异步执行任务创建方法 dispatch_async
。
// 同步执行任务创建方法
dispatch_sync(queue, ^{
// 这里放同步执行任务代码
});
// 异步执行任务创建方法
dispatch_async(queue, ^{
// 这里放异步执行任务代码
});
任务和队列的六种组合方式
在理解这六种组合方式前,最好先去看看参考博客里对任务和队列关系的解释那个例子,很好懂 ;
队列嵌套下,不同组合方式的区别
首先,死锁问题常常发生在同一个串行队列的嵌套使用。其实主线程也是这种情况 ;
如在『异步执行』+『串行队列』的任务中,又嵌套了『当前的串行队列』,然后进行『同步执行』。
dispatch_queue_t queue = dispatch_queue_create("test.queue", DISPATCH_QUEUE_SERIAL);
dispatch_async(queue, ^{ // 异步执行 + 串行队列
dispatch_sync(queue, ^{ // 同步执行 + 当前串行队列
// 追加任务 1
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"1---%@",[NSThread currentThread]); // 打印当前线程
});
});
对于这段死锁的理解:
最后可以发现,sync等待任务1的执行完毕才能执行完,而任务1又因为在串行队列中需要等待sync执行完才能执行,两者互相等待,从而死锁 ;
如果把上面的queue换成主队列,又因为正常代码就是同步添加到主队列中的,所以主队列其实相当于**『同步执行+串行队列』嵌套『同一个串行队列追加同步任务**
这里只考虑同一个队列的嵌套情况,关于多个队列的相互嵌套情况还请自行研究
『不同队列』+『不同任务』 组合,以及 『队列中嵌套队列』 使用的区别:
关于队列和任务我觉得要注意的点
在理解这两者间的关系时,我有几个概念总是会混淆:
首先,顺序执行,异步执行,同步执行这三者之间是不一样的 ;
假设我向队列中追加了任务1,2,3 ;顺序执行指的是任务执行的时机是顺序的 ;
假设同步追加任务1,2,3,那这几个任务在串行队列中一定会顺序执行,且后一个任务要等待前一个任务完成才能执行,而异步追加任务1,2,3,那这几个任务在串行队列中一定会顺序执行,但后一个任务可能在前一个任务未执行就执行了
在主线程中调用 『同步执行 + 主队列』:
- 互相等待卡住不可行
/**
* 同步执行 + 主队列
* 特点(主线程调用):互等卡主不执行。
* 特点(其他线程调用):不会开启新线程,执行完一个任务,再执行下一个任务。
*/
- (void)syncMain {
NSLog(@"currentThread---%@",[NSThread currentThread]); // 打印当前线程
NSLog(@"syncMain---begin");
dispatch_queue_t queue = dispatch_get_main_queue();
dispatch_sync(queue, ^{
// 追加任务 1
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"1---%@",[NSThread currentThread]); // 打印当前线程
});
dispatch_sync(queue, ^{
// 追加任务 2
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"2---%@",[NSThread currentThread]); // 打印当前线程
});
dispatch_sync(queue, ^{
// 追加任务 3
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"3---%@",[NSThread currentThread]); // 打印当前线程
});
NSLog(@"syncMain---end");
}
要是如果不在主线程中调用,而在其他线程中调用会如何呢?
// 使用 NSThread 的 detachNewThreadSelector 方法会创建线程,并自动启动线程执行 selector 任务
[NSThread detachNewThreadSelector:@selector(syncMain) toTarget:self withObject:nil];
这段代码不会卡住,因为syncMain 任务
放到了其他线程里,而 任务 1
、任务 2
、任务3
都在追加到主队列中,这三个任务都会在主线程中执行。syncMain 任务
在其他线程中执行到追加 任务 1
到主队列中,因为主队列现在没有正在执行的任务,所以,会直接执行主队列的 任务1
,等 任务1
执行完毕,再接着执行 任务 2
、任务 3
。所以这里不会卡住线程,也就不会造成死锁问题。
线程间的通信
在 iOS 开发过程中,我们一般在主线程里边进行 UI 刷新,例如:点击、滚动、拖拽等事件。我们通常把一些耗时的操作放在其他线程,比如说图片下载、文件上传等耗时操作。而当我们有时候在其他线程完成了耗时操作时,需要回到主线程,那么就用到了线程之间的通讯。
/**
* 线程间通信
*/
- (void)communication {
// 获取全局并发队列
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
// 获取主队列
dispatch_queue_t mainQueue = dispatch_get_main_queue();
dispatch_async(queue, ^{
// 异步追加任务 1
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"1---%@",[NSThread currentThread]); // 打印当前线程
// 回到主线程
dispatch_async(mainQueue, ^{
// 追加在主线程中执行的任务
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"2---%@",[NSThread currentThread]); // 打印当前线程
});
});
}
- 可以看到在其他线程中先执行任务,执行完了之后回到主线程执行主线程的相应操作。
GCD的API方法
GCD 栅栏方法:dispatch_barrier_async
我们有时需要异步执行两组操作,而且第一组操作执行完之后,才能开始执行第二组操作。这样我们就需要一个相当于 栅栏
一样的一个方法将两组异步执行的操作组给分割起来,当然这里的操作组里可以包含一个或多个任务。
dispatch_barrier_async
的确可以被理解为在栅栏之前的任务执行完成之后才会执行,并且会阻塞队列,直到 barrier block 执行完成。
- 在执行完栅栏前面的操作之后,才执行栅栏操作,最后再执行栅栏后边的操作。
GCD 延时执行方法:dispatch_after
在指定时间(例如 3 秒)之后执行某个任务。可以用 GCD 的dispatch_after
方法来实现。
需要注意的是:dispatch_after
方法并不是在指定时间之后才开始执行处理,而是在指定时间之后将任务追加到主队列中。严格来说,这个时间并不是绝对准确的,但想要大致延迟执行任务,dispatch_after
方法是很有效的。
GCD 一次性代码(只执行一次):dispatch_once
我们在创建单例、或者有整个程序运行过程中只执行一次的代码时,我们就用到了 GCD 的 dispatch_once
方法。使用 dispatch_once
方法能保证某段代码在程序运行过程中只被执行 1 次,并且即使在多线程的环境下,dispatch_once
也可以保证线程安全。
GCD 快速迭代方法:dispatch_apply
GCD 给我们提供了快速迭代的方法 dispatch_apply
。dispatch_apply
按照指定的次数将指定的任务追加到指定的队列中,并等待全部队列执行结束。
**dispatch_apply在并行队列中是可以并发执行的,所以dispatch_apply追加的任务都是异步任务;**并且它要等待追加的任务执行完才算结束 ,否则阻塞当前队列
无论是在串行队列,还是并发队列中,dispatch_apply 都会等待全部任务执行完毕,这点就像是同步操作,也像是队列组中的 dispatch_group_wait
方法。
GCD 队列组:dispatch_group
有时候我们会有这样的需求:分别异步执行2个耗时任务,然后当2个耗时任务都执行完毕后再回到主线程执行任务。这时候我们可以用到 GCD 的队列组。
dispatch_group_notify
-
监听 group 中任务的完成状态,当所有的任务都执行完成后,追加任务到 group 中,并执行任务。
注意:在 GCD 中,
dispatch_group_notify
函数只会等待之前通过dispatch_group_async
添加到指定dispatch_group
中的任务完成,而不会等待dispatch_group
之外的其他任务完成。 -
在功能上感觉可以实现栅栏的功能,可以等待任务完成后再追加任务 ;
当所有任务都执行完成之后,才执行 dispatch_group_notify
相关 block 中的任务。
dispatch_group_wait
- 暂停当前线程(阻塞当前线程),等待指定的 group 中的任务执行完成后,才会往下继续执行。(这里注意dispatch_group_notify只会等待任务的完成,不会阻塞线程) ;
当所有任务执行完成之后,才执行 dispatch_group_wait
之后的操作。但是,使用dispatch_group_wait
会阻塞当前线程。
dispatch_group_enter、dispatch_group_leave
dispatch_group_enter
标志着一个任务追加到 group,执行一次,相当于 group 中未执行完毕任务数 +1dispatch_group_leave
标志着一个任务离开了 group,执行一次,相当于 group 中未执行完毕任务数 -1。- 当 group 中未执行完毕任务数为0的时候,才会使
dispatch_group_wait
解除阻塞,以及执行追加到dispatch_group_notify
中的任务。
GCD 信号量:dispatch_semaphore
信号量是基于计数器的一种多线程同步机制,用来管理对资源的并发访问。
信号量就是一种可用来控制访问资源的数量的标识,设定了一个信号量,在线程访问之前,加上信号量的处理,则可告知系统按照我们指定的信号量数量来执行多个线程。
dispatch_semaphore_create
:创建一个 Semaphore 并初始化信号的总量
dispatch_semaphore_signal
:发送一个信号,让信号总量加 1
dispatch_semaphore_wait
:可以使总信号量减 1,信号总量小于 0 时就会一直等待(阻塞所在线程),否则就可以正常执行。
注意:信号量的使用前提是:想清楚你需要处理哪个线程等待(阻塞),又要哪个线程继续执行,然后使用信号量。
Dispatch Semaphore 在实际开发中主要用于:
dispatch_semaphore实现线程同步和加锁的例子可以看参考博客,本质上还是当某个值被写的时候,要阻塞读取这个值的线程,防止冲突 ;
线程同步
处理在多个线程访问共有资源时候,会因为多线程的特性而引发数据出错的问题。,比如A和B同时对一个值进行读写;要实现线程同步,就要保证A写的时候B不能读,B写的时候A不能读,即两者的读写要等待对方的写完成,和同步任务类似 ;
我们使用GCD的时候如何让线程同步,也有多种方法
1.dispatch_group
2.dispatch_barrier
3.dispatch_semaphore
pthread简介
https://juejin.cn/post/6844903556009443335?searchId=202407311602233142090D8ABF1BE6C207
pthread 是一套通用的多线程的 API,可以在Unix / Linux / Windows 等系统跨平台使用,使用 C 语言编写,需要程序员自己管理线程的生命周期,使用难度较大,我们在 iOS 开发中几乎不使用 pthread,但是还是来可以了解一下的。
具体的使用看参考博客,这里只总结 ;
NSThread
NSThread 是苹果官方提供的,使用起来比 pthread 更加面向对象,简单易用,可以直接操作线程对象。不过也需要需要程序员自己管理线程的生命周期(主要是创建),我们在开发的过程中偶尔使用 NSThread。比如我们会经常调用[NSThread currentThread]
来显示当前的进程信息。
NSThread有关线程创建和启动的代码在参考博客中,
NSthread关于线程间的通信
在开发中,我们经常会在子线程进行耗时操作,操作结束后再回到主线程去刷新 UI。这就涉及到了子线程和主线程之间的通信。
(这里我的理解就是在某个线程执行某个追加任务的操作,将任务追加到另一个线程处理,就是线程间的通信);
// 在主线程上执行操作
- (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);
// 在当前线程上执行操作,
- (id)performSelector:(SEL)aSelector;
- (id)performSelector:(SEL)aSelector withObject:(id)object;
- (id)performSelector:(SEL)aSelector withObject:(id)object1 withObject:(id)object2;
NSThread和GCD的一些直观的区别
GCD要在线程中执行任务要经过加入队列这一中间步骤 ;
NSthread无需队列操作,但这也意味着GCD中的线程同步方法大多无法使用,如信号量,栅栏方法 ;
所以NSthread实现线程同步的话依赖于锁 ;(相关代码看参考博客)
线程状态的转换
- 如果CPU现在调度当前线程对象,则当前线程对象进入运行状态,如果CPU调度其他线程对象,则当前线程对象回到就绪状态。
- 如果CPU在运行当前线程对象的时候调用了sleep方法\等待同步锁,则当前线程对象就进入了阻塞状态,等到sleep到时\得到同步锁,则回到就绪状态。
- 如果CPU在运行当前线程对象的时候线程任务执行完毕\异常强制退出,则当前线程对象进入死亡状态。
只看文字可能不太好理解,具体当前线程对象的状态变化如下图所示。
通过这个图了解线程的状态转换(就绪,运行,阻塞,死亡) ;
NSOperation和NSOperationQueue
NSOperation、NSOperationQueue 是基于 GCD 更高一层的封装,完全面向对象。但是比 GCD 更简单易用、代码可读性也更高。
(这里感觉可以区分一下,NSOperation是基于GCD的封装,GCD是一种通过队列管理任务实现多线程管理的技术,NSThread是oc中的类,其对象可以进行线程操作,pthread就是c语言中的线程)
为什么要使用 NSOperation、NSOperationQueue?
- 可添加完成的代码块,在操作完成后执行。
- 添加操作之间的依赖关系,方便的控制执行顺序。
- 设定操作执行的优先级。
- 可以很方便的取消一个操作的执行。
- 使用 KVO 观察对操作执行状态的更改:isExecuteing、isFinished、isCancelled。
(感觉就是可以在队列中更自由的管理任务了)
NSOperation、NSOperationQueue 操作和操作队列
操作(Operation):
- 执行操作的意思,换句话说就是你在线程中执行的那段代码。
- 在 GCD 中是放在 block 中的。在 NSOperation 中,我们使用 NSOperation 子类 NSInvocationOperation、NSBlockOperation,或者自定义子类来封装操作。
操作队列(Operation Queues):
- 这里的队列指操作队列,即用来存放操作的队列。不同于 GCD 中的调度队列 FIFO(先进先出)的原则。NSOperationQueue 对于添加到队列中的操作,首先进入准备就绪的状态(就绪状态取决于操作之间的依赖关系),然后进入就绪状态的操作的开始执行顺序(非结束执行顺序)由操作之间相对的优先级决定(优先级是操作对象自身的属性)。
- 操作队列通过设置 最大并发操作数(maxConcurrentOperationCount) 来控制并发、串行。
- NSOperationQueue 为我们提供了两种不同类型的队列:主队列和自定义队列。主队列运行在主线程之上,而自定义队列在后台执行。
NSOperation在单独使用的时候(即不放入NSOperationQueue),系统同步执行操作,配合NSOperationQueue可以实现多线程 ;
实现多线程的步骤:1.创建操作 2.创建队列 3.将操作加入队列 ;
系统就会自动将 NSOperationQueue 中的 NSOperation 取出来,在系统分配的线程中执行操作。
NSOperation的创建
NSOperation 是个抽象类,不能用来封装操作。我们只有使用它的子类来封装操作。
- 使用子类 NSInvocationOperation
- 使用子类 NSBlockOperation
- 自定义继承自 NSOperation 的子类,通过实现内部相应的方法来封装操作。
使用子类 NSInvocationOperation
(包装一个方法的操作)
/**
* 使用子类 NSInvocationOperation
*/
- (void)useInvocationOperation {
// 1.创建 NSInvocationOperation 对象
NSInvocationOperation *op = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(task1) object:nil];
// 2.调用 start 方法开始执行操作
[op start];
}
/**
* 任务1
*/
- (void)task1 {
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"1---%@", [NSThread currentThread]); // 打印当前线程
}
}
在没有使用 NSOperationQueue、在主线程中单独使用使用子类 NSInvocationOperation 执行一个操作的情况下,操作是在当前线程执行的,并没有开启新线程。
在其他线程中单独使用子类 NSInvocationOperation,操作是在当前调用的其他线程执行的,并没有开启新线程。
使用子类 NSBlockOperation
(包装一个代码块的操作)
/**
* 使用子类 NSBlockOperation
*/
- (void)useBlockOperation {
// 1.创建 NSBlockOperation 对象
NSBlockOperation *op = [NSBlockOperation blockOperationWithBlock:^{
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"1---%@", [NSThread currentThread]); // 打印当前线程
}
}];
// 2.调用 start 方法开始执行操作
[op start];
}
注意的点:NSBlockOperation 还可以通过`addExecutionBlock:为 NSBlockOperation 添加额外的操作,且这些操作和原来的Block可以并发执行 ;只有当所有相关的操作已经完成执行时,才视为完成。
(这也意味着,如果在主线程中单独start,NSBlockoperation自身相当于一个任务,被同步追加到主队列,这时会阻塞主队列直到Block执行完 ;)
且其他的Block不一定在当前线程执行,不过这也是当然的,都已经并发执行了 ;
使用自定义继承自 NSOperation 的子类
如果使用子类 NSInvocationOperation、NSBlockOperation 不能满足日常需求,我们可以使用自定义继承自 NSOperation 的子类。可以通过重写 main
或者 start
方法 来定义自己的 NSOperation 对象。重写main
方法比较简单,我们不需要管理操作的状态属性 isExecuting
和 isFinished
。当 main
执行完返回的时候,这个操作就结束了。
(其实可以把main方法看成NSInvocation封住的方法一样,这就是封装的操作本身的实现) ;
// YSCOperation.h 文件
#import <Foundation/Foundation.h>
@interface YSCOperation : NSOperation
@end
// YSCOperation.m 文件
#import "YSCOperation.h"
@implementation YSCOperation
- (void)main {
if (!self.isCancelled) {
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2];
NSLog(@"1---%@", [NSThread currentThread]);
}
}
}
@end
/**
* 使用自定义继承自 NSOperation 的子类
*/
- (void)useCustomOperation {
// 1.创建 YSCOperation 对象
YSCOperation *op = [[YSCOperation alloc] init];
// 2.调用 start 方法开始执行操作
[op start];
}
创建NSOperationQueue
NSOperationQueue 一共有两种队列:主队列、自定义队列。其中自定义队列同时包含了串行、并发功能。
-
主队列
- 凡是添加到主队列中的操作,都会放到主线程中执行。
// 主队列获取方法
NSOperationQueue *queue = [NSOperationQueue mainQueue];
自定义队列(非主队列)
- 添加到这种队列中的操作,就会自动放到子线程中执行。
- 同时包含了:串行、并发功能
// 自定义队列创建方法
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
将操作加入队列
两种方法:
- (void)addOperation:(NSOperation *)op;
- 需要先创建操作,再将创建好的操作加入到创建好的队列中去。
- (void)addOperationWithBlock:(void (^)(void))block;
- 无需先创建操作,在 block 中添加操作,直接将包含操作的 block 加入到队列中。
这里就会发现,当我们把操作加入队列后,默认所有操作都是并发执行的,那如何实现队列的同步效果呢 ?
这里就涉及到了NSOperationQueue的关键属性maxConcurrentOperationCount ,这个属性叫做最大并发操作数,可以控制特定队列中可以并发执行任务的最大数 ;
(注意最大并发数指的的队列能并发的操作的数量,不是并发线程的数量,而且一个操作不一定只能在一个线程中运行 ;)
maxConcurrentOperationCount
默认情况下为-1,表示不进行限制,可进行并发执行。maxConcurrentOperationCount
为1时,队列为串行队列。只能串行执行。maxConcurrentOperationCount
大于1时,队列为并发队列。操作并发执行,当然这个值不应超过系统限制,即使自己设置一个很大的值,系统也会自动调整为 min{自己设定的值,系统设定的默认最大值}。
/**
* 设置 MaxConcurrentOperationCount(最大并发操作数)
*/
- (void)setMaxConcurrentOperationCount {
NSOperationQueue* queue = [[NSOperationQueue alloc] init] ;
queue.maxConcurrentOperationCount = 2 ;
[queue addOperationWithBlock:^{
[self task1] ;
}] ;
[queue addOperationWithBlock:^{
[self task1] ;
}] ;
[queue addOperationWithBlock:^{
[self task1] ;
}] ;
[queue addOperationWithBlock:^{
[self task1] ;
}] ;
}
/**
* 任务1
*/
- (void)task1 {
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"1---%@", [NSThread currentThread]); // 打印当前线程
}
}
NSOperation 操作依赖
通过操作依赖,我们可以很方便的控制操作之间的执行先后顺序;
- (void)addDependency:(NSOperation *)op;
添加依赖,使当前操作依赖于操作 op 的完成。- (void)removeDependency:(NSOperation *)op;
移除依赖,取消当前操作对操作 op 的依赖。
- (void)addDependancy {
NSOperationQueue* queue = [[NSOperationQueue alloc] init] ;
NSBlockOperation* op01 = [NSBlockOperation blockOperationWithBlock:^{
[self task1] ;
NSLog(@"1") ;
}] ;
NSBlockOperation* op02 = [NSBlockOperation blockOperationWithBlock:^{
[self task1] ;
NSLog(@"2") ;
}] ;
[op02 addDependency:op01] ;
[queue addOperation:op01] ;
[queue addOperation:op02] ;
}
无论运行几次,其结果都是 op1 先执行,op2 后执行。
NSOperation 优先级
NSOperation 提供了queuePriority
(优先级)属性,queuePriority
属性适用于同一操作队列中的操作,不适用于不同操作队列中的操作。默认情况下,所有新创建的操作对象优先级都是NSOperationQueuePriorityNormal
。但是我们可以通过setQueuePriority:
方法来改变当前操作在同一队列中的执行优先级。
// 优先级的取值
typedef NS_ENUM(NSInteger, NSOperationQueuePriority) {
NSOperationQueuePriorityVeryLow = -8L,
NSOperationQueuePriorityLow = -4L,
NSOperationQueuePriorityNormal = 0,
NSOperationQueuePriorityHigh = 4,
NSOperationQueuePriorityVeryHigh = 8
};
对于添加到队列中的操作,首先进入准备就绪的状态(就绪状态取决于操作之间的依赖关系),然后进入就绪状态的操作的开始执行顺序**(非结束执行顺序)由操作之间相对的优先级决定(优先级是操作对象自身的属性)。**
那么,什么样的操作才是进入就绪状态的操作呢?
- 当一个操作的所有依赖都已经完成时,操作对象通常会进入准备就绪状态,等待执行。
举个例子,现在有4个优先级都是 NSOperationQueuePriorityNormal
(默认级别)的操作:op1,op2,op3,op4。其中 op3 依赖于 op2,op2 依赖于 op1,即 op3 -> op2 -> op1。现在将这4个操作添加到队列中并发执行。
这里的op1和op4就是就绪状态 ;
**(简单的说,当队列中的某个操作所有依赖都已经完成,那么这个操作进入就绪态,这时这些就绪状态的操作的顺序由相对的优先级决定) **;
-
queuePriority
属性决定了进入准备就绪状态下的操作之间的开始执行顺序。并且,优先级不能取代依赖关系。 -
如果一个队列中既包含高优先级操作,又包含低优先级操作,并且两个操作都已经准备就绪,那么队列先执行高优先级操作。比如上例中,如果 op1 和 op4 是不同优先级的操作,那么就会先执行优先级高的操作。
-
如果,一个队列中既包含了准备就绪状态的操作,又包含了未准备就绪的操作,未准备就绪的操作优先级比准备就绪的操作优先级高。那么,虽然准备就绪的操作优先级低,也会优先执行。优先级不能取代依赖关系。如果要控制操作间的启动顺序,则必须使用依赖关系。
NSOperation、NSOperationQueue 线程间的通信
- (void)communicate {
NSOperationQueue* queue = [[NSOperationQueue alloc] init] ;
[queue addOperationWithBlock:^{
[self task1] ;
}] ;
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
[self task1] ;
}] ;
}
通过线程间的通信,先在其他线程中执行操作,等操作执行完了之后再回到主线程执行主线程的相应操作。
NSOperation、NSOperationQueue 非线程安全
NSOperation、NSOperationQueue的线程安全或者说线程同步由锁来实现 ;
NSOperation 常用属性和方法
- 取消操作方法
- (void)cancel;
可取消操作,实质是标记 isCancelled 状态。
- 判断操作状态方法
- (BOOL)isFinished;
判断操作是否已经结束。- (BOOL)isCancelled;
判断操作是否已经标记为取消。- (BOOL)isExecuting;
判断操作是否正在在运行。- (BOOL)isReady;
判断操作是否处于准备就绪状态,这个值和操作的依赖关系相关。
- 操作同步
- (void)waitUntilFinished;
阻塞当前线程,直到该操作结束。可用于线程执行顺序的同步。- (void)setCompletionBlock:(void (^)(void))block;
completionBlock
会在当前操作执行完毕时执行 completionBlock。- (void)addDependency:(NSOperation *)op;
添加依赖,使当前操作依赖于操作 op 的完成。- (void)removeDependency:(NSOperation *)op;
移除依赖,取消当前操作对操作 op 的依赖。@property (readonly, copy) NSArray<NSOperation *> *dependencies;
在当前操作开始执行之前完成执行的所有操作对象数组。
(当你调用 cancel
方法来取消一个操作时,该操作会被标记为已取消。这并不意味着操作会立即停止,而是表示操作处于取消状态。操作会在适当的时机检查自身是否被取消,并根据情况来决定是否继续执行。)
NSOperationQueue 常用属性和方法
- 取消/暂停/恢复操作
- (void)cancelAllOperations;
可以取消队列的所有操作。- (BOOL)isSuspended;
判断队列是否处于暂停状态。 YES 为暂停状态,NO 为恢复状态。- (void)setSuspended:(BOOL)b;
可设置操作的暂停和恢复,YES 代表暂停队列,NO 代表恢复队列。
- 操作同步
- (void)waitUntilAllOperationsAreFinished;
阻塞当前线程,直到队列中的操作全部执行完毕。
- 添加/获取操作
- (void)addOperationWithBlock:(void (^)(void))block;
向队列中添加一个 NSBlockOperation 类型操作对象。- (void)addOperations:(NSArray *)ops waitUntilFinished:(BOOL)wait;
向队列中添加操作数组,wait 标志是否阻塞当前线程直到所有操作结束- (NSArray *)operations;
当前在队列中的操作数组(某个操作执行结束后会自动从这个数组清除)。- (NSUInteger)operationCount;
当前队列中的操作数。
- 获取队列
+ (id)currentQueue;
获取当前队列,如果当前线程不是在 NSOperationQueue 上运行则返回 nil。+ (id)mainQueue;
获取主队列。
注意:
- 这里的暂停和取消(包括操作的取消和队列的取消)并不代表可以将当前的操作立即取消,而是当当前的操作执行完毕之后不再执行新的操作。
- 暂停和取消的区别就在于:暂停操作之后还可以恢复操作,继续向下执行;而取消操作之后,所有的操作就清空了,无法再接着执行剩下的操作。