【iOS】多线程以及GCD和NSOperation

news2024/9/23 1:36:05

iOS多线程

  • 线程基础
    • 进程与线程
    • NSThread类
  • GCD
    • 认识GCD
    • 任务
    • 队列
    • 队列与任务结合
    • 线程间通信
    • 队列组dispatch group
    • 延迟执行操作
  • NSOperation
    • 简介
    • 基本使用
    • NSOperation、NSOperationQueue 常用属性和方法归纳
      • NSOperation 常用属性和方法
      • NSOperationQueue 常用属性和方法
    • 小结

线程基础

现代的操作系统,包括iOS,都支持线程(Thread)的概念。每个进程可以包含多个线程,借助多个CPU的内核,可以同时处理多个任务。即便是在早期单核CPU的时代,操作系统也可以把CPU资源在多个线程之间进行切换,给用户的感觉是在“同时”处理多个任务。

进程与线程

每个应用程序都有一个进程。例如在Mac系统中,可以通过活动监视器来查看当前Mac正在运行的进程。每个进程可以包含多个线程,这些线程可以同时运行,并各自处理一些任务。早期的CPU只有一个核心,因此会交替处理各个线程中的任务,让用户产生一种多任务同时处理的“感觉”。当CPU拥有多个内核时,各个线程就可以分配到不同的CPU内核中进行处理,即为真正意义上的多任务同时处理,如图所示:
在这里插入图片描述
对于任意一个iOS应用,程序运行起来后,默认会产生一个主线程(MainThread)。主线程专门用来处理UIKit对象的操作,如界面的显示与更新、处理用户交互事件等(请牢记这点,所有与UI相关的操作都要在主线程中进行)。

对于一个应用来说,之所以引入多个线程,很大程度上是由于存在一些操作是非常耗时的,例如:发送网络请求并等待服务器的响应,这种耗时操作是不能够放在主线程中进行操作的,因为在等待的时间内主线程被使用,用户是不能做任何交互动作的,因而会极大影响用户体验。对于耗时的操作,需要再另外创建一个线程,放到后台处理,当处理完成得到结果后,再返回主线程去设置UI界面,这就涉及线程间通信的相关知识。

多线程同时处理任务,还会涉及线程安全(thread-safe)的问题,当多个线程对一个对象进行同时操作时,就会影响结果的正确性。因此,线程对某个对象操作时,需要使用到“锁”的机制,即当一个线程操作一个对象的过程中,会给这个对象上锁,其他线程就不能够访问该对象了。加锁虽然解决了线程安全的问题,但带来的另外一个弊端就是影响了程序运行的效率。

当给一个自定义类中添加属性时,属性关键字其中就有atomicnonatomic的区分,其中,atomic是线程安全的,当有线程访问这个属性时,会为该属性的setter方法加锁,atomic是默认值。但是在实际的开发中,都会把给属性设置nonatomic关键字,因为对于移动设备来说,效率更加重要,但也需要程序员注意线程安全问题。

NSThread类

NSThred类是苹果官方提供的管理线程的类,提供了一些线程管理的方法。但是随着GCD和NSOperation的推出,NSThread的使用场景已经大大减少。在实际的开发中,偶尔会使用NSThread类来获取一些线程信息。常用的一些方法如下:

  • 获取当前线程对象。
+ (NSThread *) currentThread;
  • 获取主线程对象。
+ (NSThread *) mainThread;
  • 使主线程休眠ti秒。
+ (void)sleepForTimeInterval:(NSTimeInterval)ti

GCD

GCD(Grand Central Dispatch)技术,苹果首先应用于OS X中,随后在iOS中也引入了GCD技术。特别是苹果公司把CPU升级为多核后,GCD的使用变得更加广泛和重要。相比于NSThread,GCD中已经完全屏蔽了有关线程的概念,而是引入了任务和队列,把任务放到队列中执行,指定任务和队列的类型。其他有关线程管理的事务完全交由GCD来处理,大大简化了多任务开发的难度。

认识GCD

GCD是苹果公司推出的专门用于简化多线程编程的技术。在GCD中,程序员已经不再需要去关心有关线程的操作(如:线程创建、线程销毁、线程调度),而是引入了任务和队列两个核心概念。

由于GCD是苹果公司推出的技术,因此GCD能够很好地调度苹果设备的CPU资源,不论是在Mac平台,还是在iOS平台。特别是在iPhone中引入多核CPU之后,GCD的使用就变得越发重要。

由于GCD对线程管理进行了封装,因此,当工程师使用GCD时,只需要把任务(通常封装在一个Block中)添加到一个队列中执行,有关线程调度的工作完全交由GCD完成。

在使用GCD处理多任务执行时,只要按照如下步骤执行即可。

  • 在Block中定义需要执行的任务内容。
  • 把任务添加到队列queue中。

GCD对队列中的任务,按照“先进先出”的原则,根据任务添加到队列的顺序来对队列进行处理,GCD会根据任务和队列的类型,自动在多个线程之间分配工作。

任务

在GCD中,需要处理的事务统一使用Block封装起来,称为任务。任务有两种类型,同步任务和异步任务。通过调用不同的函数,来设置任务的类型。同时,任务编写在函数的Block参数中。

异步任务:执行任务时,会在另外的线程中执行,即可能会创建新的线程。

dispatch_async(dispatch_queue_t queue, dispatch_block_t block);

同步任务:执行任务时,会在当前的线程中执行,当前线程有可能是主线程,也有可能是子线程。

dispatch_sync(dispatch_queue_t queue, dispatch_block_t block);

队列

GCD中,队列是一个重要概念。系统提供了若干预定义的队列,其中包括可以获取应用程序的主队列(任务始终在主线程上执行,与更新UI相关的操作必须在主队列中完成)。另外,工程师可以自由创建不同类型的队列,例如:并行队列和串行队列,队列的类型决定了任务的执行方式。GCD队列严格按照“先进先出”的原则,添加到GCD队列中的任务,始终会按照加入队列的顺序被执行。

并行队列:并行队列中的任务可以在多个线程之间分配执行,分配的原则由GCD控制,因此,并行队列中的任务,虽然执行时按照先进先出进行分配的,但由于各个任务被分配到不同的线程执行,因此其完成时间有可能不同,即:后分配的任务有可能先执行完成;并发队列一定需要和异步执行的任务(使用dispatch_async())结合起来使用才有意义。

串行队列:串行队列中的任务是按照顺序一个一个完成的,当一个任务完成后,才去执行下一个任务;因此,串行队列对应一个线程执行。

主队列:主队列也是一个串行队列,主队列中的任务都在主线程中执行。

程序员可以通过如下的函数来创建不同类型的队列。

  • 获取系统定义的并行队列。一般来说,开发中通常不需要自己创建并行队列,使用系统提供的即可。
dispatch_get_global_queue(intptr_t identifier, uintptr_t flags);
  • 创建队列。可以创建并行队列,也可以创建串行队列。该方法常用于创建串行队列。
dispatch_queue_create(const char * _Nullable label, dispatch_queue_attr_t  _Nullable attr);
  • 获取主队列。
dispatch_get_main_queue(void);

队列与任务结合

在GCD中存在队列和任务两个核心概念,同时队列又分为并行队列、串行队列和主队列,任务包括异步任务和同步任务。GCD的使用方法就是把任务放到队列中执行,因而根据不同的任务类型和队列类型,就会存在6种组合。

  1. 异步任务+并行队列
    把异步任务放到并行队列进行执行,异步任务会在不同的线程中执行,这是最常使用的一种组合。
- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    //获取一个并行队列,其队列优先级为默认,保留参数置为0
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    //创建异步任务,并放到并行队列中执行
    dispatch_async(queue, ^{
        for (int i = 0; i < 2; i++) {
            NSLog(@"task1:%d", i);
        }
        NSLog(@"task1----%@", [NSThread currentThread]);
    });
    dispatch_async(queue, ^{
        for (int i = 0; i < 2; i++) {
            NSLog(@"task2:%d", i);
        }
        NSLog(@"task2----%@", [NSThread currentThread]);
    });
    dispatch_async(queue, ^{
        for (int i = 0; i < 2; i++) {
            NSLog(@"task3:%d", i);
        }
        NSLog(@"task3----%@", [NSThread currentThread]);
    });
}

运行结果如图所示。异步任务+并行队列组合情况下,每个任务会在不同的线程中同时执行。

在这里插入图片描述

  1. 异步任务+串行队列
    对于异步任务放在串行队列中执行时,任务只会在一个新开的线程中,按照顺序进行执行。
- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    //获取一个串行队列,其字符串标识符为com.1101iOS,枚举类型设为NULL会创建一个serial队列
    dispatch_queue_t queue = dispatch_queue_create("com.1101iOS", NULL);
    //创建异步任务,并放到串行队列中执行
    dispatch_async(queue, ^{
        for (int i = 0; i < 2; i++) {
            NSLog(@"task1:%d", i);
        }
        NSLog(@"task1----%@", [NSThread currentThread]);
    });
    dispatch_async(queue, ^{
        for (int i = 0; i < 2; i++) {
            NSLog(@"task2:%d", i);
        }
        NSLog(@"task2----%@", [NSThread currentThread]);
    });
    dispatch_async(queue, ^{
        for (int i = 0; i < 2; i++) {
            NSLog(@"task3:%d", i);
        }
        NSLog(@"task3----%@", [NSThread currentThread]);
    });
}

运行结果如图所示。可以看到,所有任务都在一个线程中执行,并且完成一个后,再执行下一个。请添加图片描述

  1. 异步任务+主队列
    把异步任务放在主队列中执行,由于主队列是一个特殊的串行队列,因此任务是串行执行的,但由于主队列对应序号为1的线程,因此,即便是异步任务,也不会再创建新的线程。
- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    //获取主队列
    dispatch_queue_t mainQueue = dispatch_get_main_queue();
    //创建异步任务
    dispatch_async(mainQueue, ^{
        for (int i = 0; i < 2; i++) {
            NSLog(@"task1:%d", i);
        }
        NSLog(@"task1----%@", [NSThread currentThread]);
    });
    dispatch_async(mainQueue, ^{
        for (int i = 0; i < 2; i++) {
            NSLog(@"task2:%d", i);
        }
        NSLog(@"task2----%@", [NSThread currentThread]);
    });
    dispatch_async(mainQueue, ^{
        for (int i = 0; i < 2; i++) {
            NSLog(@"task3:%d", i);
        }
        NSLog(@"task3----%@", [NSThread currentThread]);
    });
}

运行结果如下所示。
请添加图片描述

  1. 同步任务+并行队列
    同步任务的执行是在当前线程中完成的,因此,即便是把同步任务放在并行队列中执行,由于只有1个线程,任务也是一个一个按顺序执行(串行执行)的。
- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    //获取并行队列
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    //同步执行
    dispatch_async(queue, ^{
        for (int i = 0; i < 2; i++) {
            NSLog(@"task1:%d", i);
        }
        NSLog(@"task1----%@", [NSThread currentThread]);
    });
    dispatch_async(queue, ^{
        for (int i = 0; i < 2; i++) {
            NSLog(@"task2:%d", i);
        }
        NSLog(@"task2----%@", [NSThread currentThread]);
    });
    dispatch_async(queue, ^{
        for (int i = 0; i < 2; i++) {
            NSLog(@"task3:%d", i);
        }
        NSLog(@"task3----%@", [NSThread currentThread]);
    });
}

运行结果如图所示,由于viewDidLoad方法是在主线程中执行的,因此,创建的同步任务也在主线程中执行。
请添加图片描述

  1. 同步任务+串行队列
    同步任务放在串行队列中执行,任务会在当前线程依次执行。
- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    //创建串行队列
    dispatch_queue_t queue = dispatch_queue_create("com.1101iOS", NULL);
    //同步执行
    dispatch_async(queue, ^{
        for (int i = 0; i < 2; i++) {
            NSLog(@"task1:%d", i);
        }
        NSLog(@"task1----%@", [NSThread currentThread]);
    });
    dispatch_async(queue, ^{
        for (int i = 0; i < 2; i++) {
            NSLog(@"task2:%d", i);
        }
        NSLog(@"task2----%@", [NSThread currentThread]);
    });
    dispatch_async(queue, ^{
        for (int i = 0; i < 2; i++) {
            NSLog(@"task3:%d", i);
        }
        NSLog(@"task3----%@", [NSThread currentThread]);
    });
}

运行结果如图所示,由于viewDidLoad方法是在主线程中执行的,因此,创建的同步任务也在主线程中执行。
请添加图片描述

  1. 同步任务+主队列
    这种情况下,主线程会被阻塞,程序会挂死,不能使用。
- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    //获取主队列
    dispatch_queue_t mainQueue = dispatch_get_main_queue();
    //同步执行
    dispatch_async(mainQueue, ^{
        for (int i = 0; i < 2; i++) {
            NSLog(@"task1:%d", i);
        }
        NSLog(@"task1----%@", [NSThread currentThread]);
    });
    dispatch_async(mainQueue, ^{
        for (int i = 0; i < 2; i++) {
            NSLog(@"task2:%d", i);
        }
        NSLog(@"task2----%@", [NSThread currentThread]);
    });
    dispatch_async(mainQueue, ^{
        for (int i = 0; i < 2; i++) {
            NSLog(@"task3:%d", i);
        }
        NSLog(@"task3----%@", [NSThread currentThread]);
    });
}

当调用dispatch_sync会阻塞线程,直到任务(Block)执行完成之后才能继续执行。主线程中初始时需要执行的代码如图A所示。主线程需要执行1、2、3这几块代码。


图A

执行完1之后主线程需要执行的代码如图B所示。将任务(Block)添加到主线程中去执行。同时阻塞主线程,直到任务(Block)执行完成。此时Block 1等待3的代码执行完成,而代码1需要等Block 1执行完成。这就造成了1和Block 1相互等待,成了死锁。
请添加图片描述
图B

线程间通信

由于UI的更新和操作是由主线程负责的,因此,当使用子线程获取到数据时(例如通过网络获取得到了服务器返回的数据),需要返回主线程对UI界面进行更新,这时就涉及线程间的通信。

  1. 线程间通信简介
    在涉及网络数据获取的过程中,一般会使用异步任务+并发队列进行数据获取。当获取到网络服务器返回的数据后,需要在主线程中把数据显示在屏幕上,因此这就涉及线程间的通信。在并行队列中的任务还可以嵌套子任务,嵌套的子任务可以设置为在主线程中执行的任务。

  2. 示例代码
    下面的示例代码模拟了一个从网络获取一张图片的数据,并在屏幕上显示在一个UIImageView控件上的过程。在这个案例中,设置一个网络下载的异步任务,并放在并行队列中执行。此时,GCD会在一个新的线程中执行下载任务,当网络图片下载完成后,由于涉及更新UI,所以必须嵌套一个子任务在主队列中执行,如图所示。
    在这里插入图片描述

由于涉及发送网络请求,需要提前在info.plist文件中设置App Transport SecuritySettings参数,如下所示。

请添加图片描述

在按钮的点击方法中,在异步任务+并行队列中实现网络图片的下载操作,并嵌套一个子任务在主线程中更新界面:

- (void) downloadImage {
    //获取并行队列
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    NSLog(@"0---%@", [NSThread currentThread]);
    
    //异步任务
    dispatch_async(queue, ^{
       //从网络下载图片
        NSString *urlString = @"https://img-blog.csdnimg.cn/75ccdf747f2a4d259bbb2b1c26110878.jpeg";
        NSURL *url = [NSURL URLWithString:urlString];
        NSData *imageData = [NSData dataWithContentsOfURL:url];
        UIImage *image = [UIImage imageWithData:imageData];
        NSLog(@"1---%@", [NSThread currentThread]);
        
        //返回主线程设置UI
        dispatch_async(dispatch_get_main_queue(), ^{
            self.imageView.image = image;
            NSLog(@"2---%@", [NSThread currentThread]);
        });
        
    });
    NSLog(@"3---%@", [NSThread currentThread]);
}

通过日志打印的顺序可以看出程序执行的顺序,即:网络下载任务是在新的线程中执行的,并且图片下载完毕后,回到主线程设置图片显示。

队列组dispatch group

在使用GCD进行任务操作时,有时会希望若干个任务执行之间有先后执行的依赖关系,例如,当A、B两个异步任务完成后,再去完成C任务,这时就可以使用队列组dispatch group来完成。

  1. 队列组基本介绍
    在串行队列中,任务是按照进入队列的顺序依次执行,因此任务和任务之间是有明确的先后顺序的。但是对于并行队列的任务来说,由于任务会被自动分配到不同的线程中执行,因此任务完成的顺序是不确定的。假如希望给并行队列中的任务设置执行顺序,例如,当任务A和任务B完成后,再去完成任务C,就需要使用到任务组dispatch group。

在GCD中,苹果官方提供了如下一些有关队列组操作的函数。

创建队列组:

dispatch_group_t group = dispatch_group_create();

向队列组中插入一个异步任务:

dispatch_group_async(dispatch_group_t group, dispatch_queue_t queue, dispatch_block_t block);

队列组中其他任务执行完成后,执行的任务,通常可以用来设置UI界面:

dispatch_group_notify(dispatch_group_t group, dispatch_queue_t queue, dispatch_block_t block);

队列组中的异步任务执行会在另外的进程中执行,等所有的任务都执行完成后,会通知执行dispatch_group_notify()中的任务。

延迟执行操作

在开发过程中,有时会希望把一些操作封装起来延迟一段时间后再执行。iOS开发中,有两种常用的方法可以实现延迟执行:一种是使用GCD;另外一种是使用NSRunLoop类中提供的方法。

  1. 使用GCD实现延迟执行
    在GCD中可以使用dispatch_after()函数,封装一段代码到Block中,在设置的延迟时间dispatch_time_t之后执行。
dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0*NSEC_PER_SEC));
    dispatch_after(time, dispatch_get_main_queue(), ^{
        NSLog(@"延迟2.0秒");
    });
  1. 使用NSRunLoop类中的方法实现延迟执行
    在NSRunLoop类中,也提供了有关延迟执行的方法。由于这些方法是对NSObject类的扩展,因此,所有的类都可以使用。
@interface NSObject (NSDelayedPerforming)
- (void) performSelector:(SEL)aSelector withObject:(id)anArgument afterDelay:(NSTimeInterval)delay inModes:(NSArray<NSRunLoopMode> *)modes;
- (void) performSelector:(SEL)aSelector withObject:(id)anArgument afterDelay:(NSTimeInterval)delay;
@end

下面的代码中也实现了延迟2.0秒打印一段日志的操作。

- (void)viewDidLoad {
    [super viewDidLoad];
    [self performSelector:@selector(printLog) withObject:nil afterDelay:2.0];
}
- (void) printLog {
    NSLog(@"延迟2秒后打印出来的日志");
}

NSOperation

GCD的核心是C语言写的系统服务,而NSOperation底层是通过GCD实现,也可以说NSOperation是GCD更高层次的抽象,这是他们之间最本质的区别。因此如果希望自定义任务,建议使用NSOperation。

简介

NSOperation在iOS 4后也基于GCD实现,但是相对于GCD来说可控性更强,并且可以加入操作依赖。NSOperation是一个抽象类,因此系统提供了NSBlockOperation和NSInvocationOperation两个子类,并且可以创建继承自NSOperation的自定义类。相比于GCD,NSOperation更加面向对象,开发者除了不需要去了解线程相关的概念之外,甚至连GCD中需要了解的异步/同步、并行/串行都不太需要深入了解,开发者只要懂得任务和队列即可。

基本使用

  1. NSOperation的类族
    由于NSOperation是一个抽象类,因此不能够直接使用NSOperation,但苹果提供了两个NSOperation的子类,NSBlockOperation和NSInvocationOperation,除此之外,还可以使用自定义CustomOperation类。如下:

请添加图片描述
对于NSBlockOperation类来讲,可以把任务封装在一个Block块之内。NSBlockOperation类提供了如下操作方法,用于创建一个NSBlockOperation对象,任务封装在Block中。

+ (instancetype)blockOperationWithBlock:(void (^)(void))block;

对于NSInvocationOperation类来说,需要执行的任务直接指定已定义的方法。

- (nullable instancetype)initWithTarget:(id)target selector:(SEL)sel object:(nullable id)arg;
  1. 无队列情况下执行任务
    在不创建队列的情况下,可以直接调用NSOperation类的start方法来执行某个任务。该任务是在当前线程进行执行的,如果是在主线程调用的start方法,那么则会在主线程中执行任务。

下面的示例代码中,在当前线程中串行执行两个任务。

//调用operation的start方法,在当前线程中串行执行,无队列
- (void) executeInCurrentThread {
    NSBlockOperation *task1 = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"task1-----%@", [NSThread currentThread]);
    }];
    NSBlockOperation *task2 = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"task2-----%@", [NSThread currentThread]);
    }];
    //调用start方法,会在当前线程中串行执行
    [task1 start];
    [task2 start];
}

运行结果:
请添加图片描述

  1. 在队列中执行任务
    当把任务放到队列(NSOperationQueue类)中进行执行时,系统会自动并行执行所有任务,对于开多少条线程之类的事务,完全交由系统处理,开发者只要把任务添加到队列中即可。另外,可以通过设置队列的maxConcurrentOperationCount属性,来设置并行执行任务的数量。
    下方的示例代码中,在一个队列中插入了5个任务,这5个任务并行执行。系统会同时开启多个线程,多个任务并行执行。另外,maxConcurrentOperationCount决定了“并发”任务的数量,而不是创建线程的数量。即便设置为1,不同的任务也有可能在不同的线程中执行。
- (void) executeInQueue {
    NSBlockOperation *task1 = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"task1-----%@", [NSThread currentThread]);
    }];
    NSBlockOperation *task2 = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"task2-----%@", [NSThread currentThread]);
    }];
    NSBlockOperation *task3 = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"task3-----%@", [NSThread currentThread]);
    }];
    NSBlockOperation *task4 = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"task4-----%@", [NSThread currentThread]);
    }];
    NSBlockOperation *task5 = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"task5-----%@", [NSThread currentThread]);
    }];
    //创建队列
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    //设置队列属性
    queue.maxConcurrentOperationCount = 5;
    //添加任务到队列
    [queue addOperation:task1];
    [queue addOperation:task2];
    [queue addOperation:task3];
    [queue addOperation:task4];
    [queue addOperation:task5];
}

运行结果如图所示。可见系统同时执行5个任务,执行的顺序是不确定的,并且创建了5条线程。
请添加图片描述

  1. 在任务中添加新任务
    通过调用NSOperation类的addExecutionBlock方法,可以为某个NSOperation对象增加额外的任务。当把这些新增的任务放到队列中执行时,也是并行执行的。
- (void)addExecutionBlock:(void (^)(void))block;

下面的示例代码中在任务中添加新任务,所有的任务都会并行执行。

- (void) addTaskInOperation {
    NSBlockOperation *task1 = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"task1-----%@", [NSThread currentThread]);
    }];
    NSBlockOperation *task2 = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"task2-----%@", [NSThread currentThread]);
    }];
    //task1中添加task
    [task1 addExecutionBlock:^{
        NSLog(@"task1 add task-----%@", [NSThread currentThread]);
    }];
    //task2中添加task
    [task2 addExecutionBlock:^{
        NSLog(@"task2 add task-----%@", [NSThread currentThread]);
    }];
    //创建队列
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    //添加任务到队列
    [queue addOperation:task1];
    [queue addOperation:task2];
}

运行结果如图:

请添加图片描述

  1. 在队列中直接添加任务
    通过调用NSOperationQueue的addOperationWithBlock方法,可以向队列中直接添加任务Block。
- (void) addTaskInQueue {
    NSBlockOperation *task1 = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"task1-----%@", [NSThread currentThread]);
    }];
    NSBlockOperation *task2 = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"task2-----%@", [NSThread currentThread]);
    }];
    //task1中添加task
    [task1 addExecutionBlock:^{
        NSLog(@"task1 add task-----%@", [NSThread currentThread]);
    }];
    //task2中添加task
    [task2 addExecutionBlock:^{
        NSLog(@"task2 add task-----%@", [NSThread currentThread]);
    }];
    //创建队列
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    //添加任务到队列
    [queue addOperation:task1];
    [queue addOperation:task2];
    //在queue中添加任务
    [queue addOperationWithBlock:^{
        NSLog(@"queue task-----%@", [NSThread currentThread]);
    }];
}

如下,向队列queue中直接添加任务,也是并行执行的。

请添加图片描述

  1. 在任务中创建completionBlock
    在NSOperation类中,可以通过设置completionBlock来创建所有任务执行完成后,自动调用的一个Block。该Block的执行是在任务执行后被调用的,有先后顺序
- (void) addCompletionBlock {
    NSBlockOperation *task1 = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"task1-----%@", [NSThread currentThread]);
    }];
    NSBlockOperation *task2 = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"task2-----%@", [NSThread currentThread]);
    }];
    //task1中添加task
    [task1 addExecutionBlock:^{
        NSLog(@"task1 add task-----%@", [NSThread currentThread]);
    }];
    task1.completionBlock = ^{
        NSLog(@"task1 end!!! %@", [NSThread currentThread]);
    };
    //task2中添加task
    [task2 addExecutionBlock:^{
        NSLog(@"task2 add task-----%@", [NSThread currentThread]);
    }];
    task2.completionBlock = ^{
        NSLog(@"task2 end!!! %@", [NSThread currentThread]);
    };
    //创建队列
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    //添加任务到队列
    [queue addOperation:task1];
    [queue addOperation:task2];
}

结果如图:
请添加图片描述

  1. NSOperation与GCD的区别
    NSOperation与GCD都是苹果公司提供的用于多线程操作的技术,但是在使用过程中也稍有区别。
    GCD是底层的C语言构成的API,而NSOperationQueue及相关对象是Objective-C的对象。在GCD中,在队列中执行的是由Block构成的任务,这是一个轻量级的数据结构;而NSOperation作为一个对象,提供了更多的选择。
  • 在NSOperationQueue中,可以随时取消已经设定要准备执行的任务(当然,已经开始的任务就无法阻止了),而GCD没法停止已经加入queue的Block(其实是有的,但需要许多复杂的代码)。
  • NSOperation能够方便地设置依赖关系,可以让一个Operation依赖于另一个Operation,这样的话尽管两个Operation处于同一个并行队列中,但前者会直到后者执行完毕后再执行。而对于GCD,没有办法设置依赖关系,但是可以通过dispatch_barrier_async来实现这种效果
  • KVO可以应用在NSOperation中,可以监听一个Operation是否完成或取消,这样能比GCD更加有效地掌控执行的后台任务。
  • 在NSOperation中,能够设置NSOperation的priority优先级,能够使同一个并行队列中的任务区分先后地执行。而在GCD中,只能区分不同任务队列的优先级,如果要区分Block任务的优先级,也需要大量的复杂代码。
  • 用户能够对NSOperation进行继承,在这之上添加成员变量与成员方法,提高整个代码的复用度,这比简单地将Block任务排入执行队列更有自由度,能够在其之上添加更多自定制的功能。
  • 效率上:直接使用GCD确实会更高效,而NSOperation会多一些开销,但是相比起上面这么多的优势,这使得使用NSOperation仍然是很划算的选择,具体怎么选择要看我们自己。

NSOperation、NSOperationQueue 常用属性和方法归纳

NSOperation 常用属性和方法

1.取消操作方法

- (void)cancel; 可取消操作,实质是标记 isCancelled 状态
2.判断操作状态方法

- (BOOL)isFinished; 判断操作是否已经结束
- (BOOL)isCancelled; 判断操作是否已经标记为取消
- (BOOL)isExecuting; 判断操作是否正在在运行
- (BOOL)isReady; 判断操作是否处于准备就绪状态,这个值和操作的依赖关系相关
3.操作同步

- (void)waitUntilFinished; 阻塞当前线程,直到该操作结束。可用于线程执行顺序的同步
- (void)setCompletionBlock:(void (^)(void))block; 会在当前操作执行完毕时执行 completionBlock
- (void)addDependency:(NSOperation *)op; 添加依赖,使当前操作依赖于操作 op 的完成
- (void)removeDependency:(NSOperation *)op; 移除依赖,取消当前操作对操作 op 的依赖
@property (readonly, copy) NSArray<NSOperation *> *dependencies; 在当前操作开始执行之前完成执行的所有操作对象数组

NSOperationQueue 常用属性和方法

1.取消/暂停/恢复操作

- (void)cancelAllOperations; 可以取消队列的所有操作
- (BOOL)isSuspended; 判断队列是否处于暂停状态。 YES 为暂停状态,NO 为恢复状态
- (void)setSuspended:(BOOL)b; 可设置操作的暂停和恢复,YES 代表暂停队列,NO 代表恢复队列
2.操作同步

- (void)waitUntilAllOperationsAreFinished; 阻塞当前线程,直到队列中的操作全部执行完毕
3.添加/获取操作

- (void)addOperationWithBlock:(void (^)(void))block; 向队列中添加一个 NSBlockOperation 类型操作对象
- (void)addOperations:(NSArray *)ops waitUntilFinished:(BOOL)wait; 向队列中添加操作数组,wait 标志是否阻塞当前线程直到所有操作结束
- (NSArray *)operations; 当前在队列中的操作数组(某个操作执行结束后会自动从这个数组清除)
- (NSUInteger)operationCount; 当前队列中的操作数
4.获取队列

+ (id)currentQueue; 获取当前队列,如果当前线程不是在 NSOperationQueue 上运行则返回 nil
+ (id)mainQueue; 获取主队列

小结

根据实际开发来说,GCD使用情况较多,简单高效。从编程原则上来看,应该是使用高层次的抽象,避免使用低层次的抽象。那么无疑我们应当选择NSOperation,因为复杂的任务可以自己通过NSOperation实现,日常还是GCD的天下,毕竟GCD有更高的并发和执行能力。。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/510936.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

网易Android framework开发岗面试经历分享(附面试题汇总+案例解析)

背景 今年可以说是非常难&#xff0c;部门被拆&#xff0c;很多同事都被变相裁员了&#xff0c;虽然说去其他部门工作可以给我们的放宽一点要求&#xff0c;但我还是想在Android开的岗位上发展&#xff0c;所以也提出了离职&#xff1b;离职后的两个月中&#xff0c;投了一些A…

类ChatGPT逐行代码解读(1/2):从零实现Transformer、ChatGLM-6B

前言 最近一直在做类ChatGPT项目的部署 微调&#xff0c;关注比较多的是两个&#xff1a;一个LLaMA&#xff0c;一个ChatGLM&#xff0c;会发现有不少模型是基于这两个模型去做微调的&#xff0c;说到微调&#xff0c;那具体怎么微调呢&#xff0c;因此又详细了解了一下微调代…

tensorflow GPU训练环境布置

tensorflow GPU训练环境布置 一、显卡驱动安装1.1 如何处理**Failed to initialize NVML: Driver/library version mismatch的问题**1.2 卸载旧的版本1.3 驱动安装 1.3.1 利用apt 安装1.3.2 手动安装 二、安装CUDA2.1 确定CUDA版本2.2 下载文件1. 找匹配版本2. 选合适的平台 2…

微服务---Redis实用篇-黑马头条项目-商户查询缓存功能(缓存穿透,缓存雪崩,缓存击穿问题及解决思路)

1、商户查询缓存 1.1 什么是缓存? 前言:什么是缓存? 就像自行车,越野车的避震器 举个例子:越野车,山地自行车,都拥有"避震器",防止车体加速后因惯性,在酷似"U"字母的地形上飞跃,硬着陆导致的损害,像个弹簧一样; 同样,实际开发中,系统也需要"避震…

libcad.so Crack,转换为多种文件格式

libcad.so Crack,转换为多种文件格式 支持所有流行的2D和3D CAD格式。 高速准确的可视化。 轻松访问CAD实体属性&#xff0c;包括坐标、文本、图层等。 转换为多种文件格式。 从DWG和DXF文件生成G代码。 Unicode支持。 libcad.so是一个用于Linux的库。它可以将CAD功能添加到不同…

【笔试强训选择题】Day11.习题(错题)解析

作者简介&#xff1a;大家好&#xff0c;我是未央&#xff1b; 博客首页&#xff1a;未央.303 系列专栏&#xff1a;笔试强训选择题 每日一句&#xff1a;人的一生&#xff0c;可以有所作为的时机只有一次&#xff0c;那就是现在&#xff01;&#xff01;&#xff01; 文章目录…

MyBatis--原生的 API--注解的方式和MyBatis--mybatis-config.xml-配置文件详解

目录 原生的 API&注解的方式 MyBatis-原生的 API 调用 为了大家的观看我把基于xml的配置和类也展示了如果你想要详细的观看可以看 连接这个博客 创建Monster 创建MonsterMapper接口 创建MonsterMapper.xml 配置mybits的xml 创建MyBatisUtils 原生的 API 快速入门-代…

《计算机网络—自顶向下方法》 第二章Wireshark实验:DNS协议分析

域名系统 DNS(Domain Name System) 是互联网使用的命名系统&#xff0c;用于把便于大家使用的机器名字转换为 IP 地址。许多应用层软件经常直接使用 DNS&#xff0c;但计算机的用户只是间接而不是直接使用域名系统。 互联网采用层次结构的命名树作为主机的名字&#xff0c;并使…

Pytorch高级训练框架Ignite详细介绍与常用模版

引言 Ignite是Pytorch配套的高级框架&#xff0c;我们可以借其构筑一套标准化的训练流程&#xff0c;规范训练器在每个循环、轮次中的行为。本文将不再赘述Ignite的具体细节或者API&#xff0c;详见官方教程和其他博文。本文将分析Ignite的运行机制、如何将Pytorch训练代码转为…

Kubectl-AI: 一款 OpenAI GPT 自动生成应用 K8s yaml神器

首页: 官网 下载安装 wget https://github.com/sozercan/kubectl-ai/releases/download/v0.0.10/kubectl-ai_linux_amd64.tar.gz tar xvf kubectl-ai_linux_amd64.tar.gz -C /usr/local/bin/kubectl-ai需要OpenAI API密钥或Azure OpenAI服务 API密钥和端点以及有效的Kubernet…

系统移植——linux内核移植——分析内核编译过程

uImage镜像文件 1.进入linux内核源码目录 ubuntuubuntu:~$ cd FSMP1A/linux-stm32mp-5.10.61-stm32mp-r2-r0/linux-5.10.61/ 打开Makefile文件 vi Makefile 搜索include 因为 $(SRCARCH)->arm 所以上述指令为 arch/arm/Makefile 2.进入linux内核源码目录下,arch/arm目录下…

Windows 11 本地部署 Stable Diffusion web UI

Windows 11 本地部署 Stable Diffusion web UI 0. 什么是 Stable Diffusion1. 什么是 Stable Diffusion web UI2. Github 地址3. 安装 CUDA Toolkit 11.84. 安装 cuDNN v8.9.1 for CUDA 11.x5. 配置环境变量6. 安装 Python 3.10.67. 安装 Stable Diffusion web UI8. 启动 Stabl…

吊打面试官的Java项目经验一:物流系统

引言&#xff1a; java面试一般分为两部分&#xff0c;技术面试和项目面试&#xff0c;相信大多数小伙伴们都刷过很多技术性的面试题&#xff0c;连博主本人也刷过很多无聊的面试题&#xff0c;但是对于项目经验的面试&#xff0c;可能很多刚入行小伙伴属于一个空白期&#xff…

【软考|软件设计师】编辑距离算法

目录 编辑距离算法&#xff1a; 步骤&#xff1a; 实例&#xff1a; 题&#xff1a; 完整代码如下&#xff1a; 调试&#xff1a; 代码解析&#xff1a; 具体过程参考&#xff1a; 编辑距离算法&#xff1a; 是一种计算两个自符串之间差异程度的方法&#xff0c;它通过…

现场工程师出马:VMware+LVM卷快速在windows Server上部署Kafka集群

最近遇到的疑难现场问题层出不穷&#xff0c;本次遭遇的挑战是在4台windows Server 服务器上部署Kafka集群。这是一种比较少见的操作&#xff0c;原因是有些依赖的驱动对虚拟化支持不好&#xff0c;只能运行在实体win机上。 原有的上层业务是由B团队开发运维&#xff0c;现在B…

今年的博客数量上两百了

今年的博客数量上两百了 不知不觉在 C S D N CSDN CSDN中写了那么多篇文章。与 C S D N CSDN CSDN相伴的生活中&#xff0c;我过得很充实。

并发编程10:Java对象内存布局和对象头

文章目录 10.1 面试题10.2 Object object new Object()谈谈你对这句话的理解&#xff1f;10.3 对象在堆内存中布局10.3.1 权威定义----周志明老师JVM10.3.2 对象在堆内存中的存储布局 10.4 再说对象头的MarkWord10.5 聊聊Object obj new Object()10.5.1 运行结果展示10.5.2 压…

C++入门(命名空间、缺省参数、函数重载、引用、内联函数)

全文目录 引言C输入与输出命名空间概念使用使用域作用限定符::使用某个成员使用using namespace 引入整个命名空间域使用using引入某个成员 缺省参数概念分类 函数重载定义与调用原理 引用定义需要注意 使用引用作为返回型参数引用作为返回值 引用与指针的区别 内联函数总结 引…

华为OD机试真题 Java 实现【猜字谜】【2023Q2】

一、题目描述 小王设计了一人简单的清字谈游戏&#xff0c;游戏的迷面是一人错误的单词&#xff0c;比如nesw&#xff0c;玩家需要猜出谈底库中正确的单词。猜中的要求如 对于某个谜面和谜底单词&#xff0c;满足下面任一条件都表示猜中&#xff1a; 变换顺序以后一样的&…

np保存数据为txt或者csv格式

目录 1、基础参数 2、参数详解 2.1、fmt 2.2、delimiter 2.3、newline 2.4、header 1、基础参数 numpy.savetxt(fname,arrry,fmt%.18e,delimiter ,newline\n,header,footer,comments# ,encodingNone,) 2、参数详解 fname:要存入的文件、文件名、或生成器。arrry:要存储…