Bootstrap

iOS基础---多线程:GCD、NSThread、NSOperation

系列文章目录

iOS基础—多线程:GCD、NSThread、NSOperation
iOS基础—Category vs Extension


一、GCD

1.GCD的任务、函数、队列

a.任务

任务就是需要执行的操作,是GCD放在block中在线程执行的那段代码。在GCD中使用 block 封装,且没有任何参数和返回值,可用​ dispatch_block_t 声明一个任务。

dispatch_block_t task = ^{
     NSLog(@"Hellow world!");
 };

任务的执行方式有同步执行和异步执行两种执行方式。两者的主要区别是是否等待队列的任务执行结束,以及是否具备开启新线程的能力

  • 同步执行(sync):同步添加任务到队列中,在队列之前的任务执行结束之前会一直等待;同步执行的任务只能在当前线程中执行,不具备开启新线程的能力。

  • 异步执行(async):异步添加任务到队列中,不需要理会队列中其他的任务,添加即执行;异步执行可以在新的线程中执行,具备开启新的线程的能力。

b.函数

任务需要指定执行函数,即任务执行的方式为同步还是异步。

dispatch_sync(队列, ^{
    // 这里放同步任务代码
});

1.必须等待当前语句执行完成之后才会执行下一条语句
2.不会开启线程
3.会造成堵塞现象
4.在当前线程执行block任务



dispatch_async(队列, ^{
    // 这里放异步执行任务代码
});

1.可以开启新线程执行任务(不一定会开启新线程执行任务,如果是串行队列则不会开辟线程)
2.不需要等待当前语句执行完毕就可以执行下一条语句

c.队列

GCD的队列分为串行队列并发队列两种,两者都符合 FIFO(先进先出)的原则。两者的主要区别是:执行顺序不同,以及开启线程数不同

  • 串行队列:只开启一个线程,每次只能有一个任务执行,等待执行完毕后才会执行下一个任务。

  • 并发队列:可以让对个任务同时执行,也就是开启多个线程,让多个任务同时执行。

可以用 ​dispatch_queue_t 声明一个队列,用​ dispatch_queue_create 创建队列:

// 串行队列的创建方法
dispatch_queue_t serial = dispatch_queue_create("com.wml.test.thread.demo", DISPATCH_QUEUE_SERIAL);

// 并发队列的创建方法
dispatch_queue_t concurrent = dispatch_queue_create("com.wml.test.thread.demo", DISPATCH_QUEUE_CONCURRENT);

二者的区别如下图所示:
在这里插入图片描述
GCD默认提供队列:

对于串行队列,GCD默认提供了主队列。

  • 它是在 main() 函数执行之前就已经被创建的一个串行队列,一般用于刷新UI。
  • 默认情况下,平常所写代码是直接放在主队列中的。
  • 所有放在主队列中的任务,都会放到主线程中执行。
  • 可使用​ dispatch_get_main_queue() 方法获得主队列。
// 主队列的获取方法
dispatch_queue_t mainQueue = dispatch_get_main_queue();

对于并发队列,GCD默认提供了全局队列。

  • 在使⽤多线程开发时,如果对队列没有特殊需求,在执⾏异步任务时,可以直接使⽤全局队列。
  • 可以使用​ dispatch_get_global_queue() 方法来获取全局队列。
第一个参数表示队列优先级:
DISPATCH_QUEUE_PRIORITY_HIGH
DISPATCH_QUEUE_PRIORITY_LOW
DISPATCH_QUEUE_PRIORITY_BACKGROUND、
DISPATCH_QUEUE_PRIORITY_DEFAULT(常用)

第二个参数暂时没用,用0即可。

// 全局并发队列的获取方法
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

2.GCD的使用

简单的GCD使用示例:

  • 创建一个任务。
  • 创建一个队列(串行队列或并发队列)。
  • 将任务添加到队列中。
  • 系统根据任务类型执行任务(同步执行或异步执行)。
  • 将任务取消。(通过 dispatch_block_create 创建的任务才能被取消)。
//任务
dispatch_block_t customBlock = dispatch_block_create(DISPATCH_BLOCK_INHERIT_QOS_CLASS, ^{
         NSLog(@"This is a custom block execution.");
    });

//队列(并发队列)
dispatch_queue_t conque = dispatch_queue_create("TD", DISPATCH_QUEUE_CONCURRENT);
    
//将任务添加到队列中
dispatch_async(conque, customBlock);
		
//将任务取消
dispatch_block_cancel(customBlock);

a.同步函数+并发队列

不会开启新线程,在当前线程串行执行任务,后追加任务阻塞当前线程任务。

int main(int argc, const char * argv[]) {
    NSLog(@"currentThread---%@",[NSThread currentThread]);  // 打印当前线程
    NSLog(@"syncConcurrent---begin");
    
    dispatch_queue_t queue = dispatch_queue_create("net.bujige.testQueue", DISPATCH_QUEUE_CONCURRENT);
    
    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(@"syncConcurrent---end");
    return 0;
}
  • 所有任务都是在当前线程中执行,没有开启新的线程 --> 同步执行不具备开启新线程的能力。
  • 所有任务都在打印的 syncConcurrent—begin 和 syncConcurrent—end 之间执行的 --> 同步执行需要等待队列中之前的任务执行结束–>任务按顺序执行。

在这里插入图片描述

b.异步函数+并发队列

开启多条新线程(不一定等于任务数),在多条线程上并发执行任务,不阻塞当前线程任务。

int main(int argc, const char * argv[]) {
    NSLog(@"currentThread---%@",[NSThread currentThread]);  // 打印当前线程
    NSLog(@"asyncConcurrent---begin");
        
    dispatch_queue_t queue = dispatch_queue_create("net.bujige.testQueue", DISPATCH_QUEUE_CONCURRENT);
        
    dispatch_async(queue, ^{
        // 追加任务 1
        [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
        NSLog(@"1---%@",[NSThread currentThread]);      // 打印当前线程
    });
        
    dispatch_async(queue, ^{
        // 追加任务 2
        [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
        NSLog(@"2---%@",[NSThread currentThread]);      // 打印当前线程
    });
        
    dispatch_async(queue, ^{
        // 追加任务 3
        [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
        NSLog(@"3---%@",[NSThread currentThread]);      // 打印当前线程
    });
        
    NSLog(@"asyncConcurrent---end");
    return 0;
}

由于 main 函数的执行速度很快,程序可能会在并发任务完成之前就终止。这可能导致看不到一些或所有的异步任务的输出。

在这里插入图片描述

我们可以在main函数return前sleep几秒,等待异步函数的输出:

在这里插入图片描述

  • 除了当前线程(主线程),系统又开启了 3 个线程,并且任务是交替/同时执行的。 --> 异步执行具备开启新线程的能力。且并发队列可开启多个线程,同时执行多个任务
  • 所有任务是在打印的 asyncConcurrent—begin 和 asyncConcurrent—end 之后才执行的。说明当前线程没有等待,而是直接开启了新线程,在新线程中执行任务。 --> 异步执行不做等待,可以继续执行任务

c.同步函数+串行队列

不会开启新线程,在当前线程串行执行任务,后追加任务阻塞当前线程任务。

int main(int argc, const char * argv[]) {
    NSLog(@"currentThread---%@", [NSThread currentThread]);
        NSLog(@"syncSerial---begin");
        
        dispatch_queue_t serial = dispatch_queue_create("com.sy.GCD", DISPATCH_QUEUE_SERIAL);
        
        dispatch_sync(serial, ^{
            [NSThread sleepForTimeInterval:2.0];
            NSLog(@"1---%@", [NSThread currentThread]);
        });
        
        dispatch_sync(serial, ^{
            [NSThread sleepForTimeInterval:2.0];
            NSLog(@"2---%@", [NSThread currentThread]);
        });
        
        dispatch_sync(serial, ^{
            [NSThread sleepForTimeInterval:2.0];
            NSLog(@"3---%@", [NSThread currentThread]);
        });
        
        NSLog(@"syncSerial---end");
    return 0;
}

运行结果如下:

在这里插入图片描述

d.异步函数+串行队列

开启1个新线程,在新线程上串行执行任务,不阻塞当前线程任务。

int main(int argc, const char * argv[]) {
    NSLog(@"currentThread---%@", [NSThread currentThread]);
    NSLog(@"asyncSerial---begin");
        
    dispatch_queue_t serial = dispatch_queue_create("com.sy.GCD", DISPATCH_QUEUE_SERIAL);
        
    dispatch_async(serial, ^{
        [NSThread sleepForTimeInterval:2.0];
        NSLog(@"1---%@", [NSThread currentThread]);
    });
        
    dispatch_async(serial, ^{
        [NSThread sleepForTimeInterval:2.0];
        NSLog(@"2---%@", [NSThread currentThread]);
    });
        
    dispatch_async(serial, ^{
        [NSThread sleepForTimeInterval:2.0];
        NSLog(@"3---%@", [NSThread currentThread]);
    });
        
    NSLog(@"asyncSerial---end");
    sleep(10);
    return 0;
}

运行结果如下:

在这里插入图片描述

e.同步函数+主队列

主线程中调用同步函数+主队列。

同步函数会等待当前队列中的任务执行完毕,才会执行。现在的情况是main函数在主线程中执行,我们在main函数中追加任务1到主队列,任务1会等待main函数执行完毕,而main函数执行完任务1后才算完成。故形成死锁。
在这里插入图片描述

f.异步函数+主队列

int main(int argc, const char * argv[]) {
    NSLog(@"currentThread---%@", [NSThread currentThread]);
    NSLog(@"asyncMain---begin");
    
    dispatch_queue_t mainQ = dispatch_get_main_queue();
       
    dispatch_async(mainQ, ^{
        NSLog(@"1---%@", [NSThread currentThread]);
    });
       
    sleep(5);
    NSLog(@"asyncMain---end");
    return 0;
}

上面的代码运行结果如下:

在这里插入图片描述

我们发现,虽然将异步任务提交到了主队列上运行,但异步任务并没有对应的输出结果。这是因为:

在命令行程序中,主队列的任务执行依赖于主运行循环(main run loop)。在标准的命令行应用程序中,通常不存在一个活跃的主运行循环,除非你明确地创建和维护它。因此,即使用 sleep(5) 延迟了 main 函数的结束,主队列上的任务可能还是不会执行,因为没有运行循环来处理这些任务。

我们有两种解决方案:

  • 使用全局并发队列:为了在命令行程序中看到异步任务的效果,你可以使用全局并发队列而不是主队列。全局并发队列不依赖于主运行循环,适合于后台任务处理。如下图所示:
    在这里插入图片描述
  • 创建和运行主运行循环:如果你确实需要使用主队列(比如为了测试与 GUI 应用相关的代码),你可以创建并启动一个主运行循环。如下图所示:
    在这里插入图片描述

3.GCD线程间的通信

在 iOS 开发过程中,我们在主线程进行UI刷新,把图片下载、文件上传、网络请求等一些耗时的操作放在其他的线程,当这些耗时的操作完成后需要将数据同步给UI,就需要回到主线程刷新UI,那么就要用到线程之间的通讯。GCD提供了非常简便的方法进行线程间的通讯。

在这里插入图片描述

4.GCD中的其它方法

a.栅栏方法

  • 有时需要异步执行两组操作,每组操作可能包含一个或多个任务,而且第一组操作执行完之后,才能开始执行第二组操作。
  • 可以用​ dispatch_barrier_async 方法在两个操作组间形成栅栏。
  • dispatch_barrier_async 允许你在并发队列中插入一个障碍(barrier)任务。该任务会在确保所有先前提交的任务完成后执行,并在其完成后才允许后续任务执行。

如下面代码所示,​dispatch_barrier_async 就如同栅栏一样将两组任务隔开,来维护互相的同步关系。
在这里插入图片描述
如下面代码所示,​dispatch_barrier_async 就如同栅栏一样将两组任务隔开,来维护互相的同步关系。

在这里插入图片描述

b.延时方法

  • dispatch_after 方法在指定时间之后将任务异步追加到主队列中。
  • 不是在指定时间之后才开始执行处理,而是在指定时间之后将任务追加到主队列中。严格来说,这个时间并不是绝对准确的,但想要大致延迟执行任务,​dispatch_after 方法是有效的。
- (void)after {
    NSLog(@"currentThread---%@",[NSThread currentThread]);  // 打印当前线程
    NSLog(@"asyncMain---begin");
    
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        // 2.0 秒后异步追加任务代码到主队列,并开始执行
        NSLog(@"after---%@",[NSThread currentThread]);  // 打印当前线程
    });
}

c.全局执行一次的方法

  • 创建单例。
  • 整个程序运行过程中只执行一次的代码。
  • 在多线程的环境下,​dispatch_once 可以保证线程安全。
+ (instancetype)shareInstance{
    static Test *instance = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        instance = [Test alloc]init];
    });
    return instance;
}

d.快速迭代方法

  • 通常会用for循环遍历,但是GCD提供了快速迭代的方法 ​dispatch_apply
  • dispatch_apply 按照指定的次数将指定的任务追加到指定的队列中,并等待全部队列执行结束。
  • 利用并发队列进行异步执行。比如说遍历0~5这6个数字,for循环的做法是每次取出一个元素,逐个遍历。dispatch_apply 可以在多个线程中同时(异步)遍历多个数字。

因为是在并发队列中异步执行任务,所以各个任务的执行时间长短不定,最后结束顺序也不定。但是 apply—end 一定在最后执行。这是因为 dispatch_apply 方法会等待全部任务执行完毕。

在这里插入图片描述

e.队列组

  • 分别异步执行2个耗时任务,然后当2个耗时任务都执行完毕后再回到主线程执行任务。这时候可以用到GCD队列组。
  • 队列组使用 dispatch_group_create 来创建一个分组。
  • 调用队列组的​ dispatch_group_async 先把任务放到队列中,然后将队列放入队列组中。
  • 调用队列组的 ​dispatch_group_notify 回到指定线程执行任务,不会阻塞当前进程。或者使用​dispatch_group_wait 回到当前线程继续向下执行,但会阻塞当前线程。
- (void)groupNotifyTest{
    NSLog(@"current thread:%@", [NSThread currentThread]);
    dispatch_group_t group = dispatch_group_create();
    dispatch_group_async(group, dispatch_get_global_queue(0, 0), ^{
        [NSThread sleepForTimeInterval:2];
        NSLog(@"thread-1:%@", [NSThread currentThread]);
    });

    dispatch_group_async(group, dispatch_get_global_queue(0, 0), ^{
        [NSThread sleepForTimeInterval:2];
        NSLog(@"thread-2:%@", [NSThread currentThread]);
    });

    dispatch_group_notify(group, dispatch_get_main_queue(), ^{
        [NSThread sleepForTimeInterval:2];
        NSLog(@"thread-3:%@", [NSThread currentThread]);
        NSLog(@"group-end");
    });
}

运行结果如下:

在这里插入图片描述

  • 在这里需要说明的一点是dispatch_group_wait,该方法需要传入两个参数,第一个参数是group即调度组,第二个参数是timerout即指定等待的时间。
  • 一旦调用dispatch_group_wait函数,该函数就处理调用的状态而不返回值,只有当函数的currentThread停止,或到达wait函数指定的等待的时间,或Dispatch Group中的操作全部执行完毕之前,执行该函数的线程停止。
  • 当指定timeout为DISPATCH_TIME_FOREVER时就意味着永久等待。
  • 当指定timeout为DISPATCH_TIME_NOW时就意味不用任何等待即可判定属于Dispatch Group的处理是否全部执行结束。
  • 如果dispatch_group_wait函数返回值不为0,就意味着虽然经过了指定的时间,但Dispatch Group中的操作并未全部执行完毕。
  • 如果dispatch_group_wait函数返回值为0,就意味着Dispatch Group中的操作全部执行完毕。

可以使用队列组的​ dispatch_group_enter、​dispatch_group_leave组合来实现 dispatch_group_asyncdispatch_group_enter 标志着一个任务追加到 group,每执行一次相当于group中未执行完毕任务数 +1;dispatch_group_leave 标志着一个任务离开了group,每执行一次相当于group中未执行完毕任务数 -1。当 group 中未执行完毕任务数为0的时候,才会使 dispatch_group_wait 解除阻塞,或执行追加到 dispatch_group_notify 中的任务。

- (void)groupEnterTest {
    dispatch_group_t group = dispatch_group_create();
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
  
    dispatch_group_enter(group);
    dispatch_async(queue, ^{
        [NSThread sleepForTimeInterval:2];
        NSLog(@"thread_1:%@", [NSThread currentThread]);
        dispatch_group_leave(group);
    });

    dispatch_group_enter(group);
    dispatch_async(queue, ^{
        [NSThread sleepForTimeInterval:2];
        NSLog(@"thread_2:%@", [NSThread currentThread]);
        dispatch_group_leave(group);
    });

    dispatch_group_notify(group, dispatch_get_main_queue(), ^{
        [NSThread sleepForTimeInterval:2];
        NSLog(@"thread_3:%@", [NSThread currentThread]);
        NSLog(@"group_end");
    });
}

结果如下图所示:

在这里插入图片描述

5.信号量

a.线程同步

信号量的基本概念是,你可以设置一个资源的计数,当计数大于0时,线程可以访问资源,每次访问后计数减1;当计数为0时,其他线程试图访问资源会被阻塞,直到其他线程释放资源,计数再次变为正数。

Dispatch Semaphore 提供了三个方法:

  • dispatch_semaphore_create:创建一个 Semaphore 并初始化信号的总量
  • dispatch_semaphore_signal:发送一个信号,让信号总量加 1
  • dispatch_semaphore_wait:如果信号量的值为 0,意味着没有可用的资源(阻塞所在线程),否则就可以正常执行,使总信号量减 1
- (void)semaphoreSync {
    NSLog(@"currentThread---%@",[NSThread currentThread]);  // 打印当前线程
    NSLog(@"semaphore---begin");
    
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
    
    __block int number = 0;
    dispatch_async(queue, ^{
        // 追加任务 1
        [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
        NSLog(@"1---%@",[NSThread currentThread]);      // 打印当前线程
        
        number = 100;
        dispatch_semaphore_signal(semaphore);
    });
    
    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
    NSLog(@"semaphore---end,number = %d",number);
}

运行结果如下:

在这里插入图片描述

b.线程并发数

我们也可以用semaphore来控制线程的最大并发数:

dispatch_semaphore_t dispatch_semaphore_create(long value);// value参数为任务并发同时执行时线程最大并发数
dispatch_semaphore_wait(semaphore, timeout); // 信号阻塞,信号发送后->判断是否有空闲的计数可用,如果有可用计数执行后面的任务,如果没有可用计数就让当前线程阻塞
dispatch_semaphore_signal(semaphore); // 信号发送,信号发送后->通知有空闲的计数可用,其他阻塞的任务就可以执行

int main(int argc, const char * argv[]) {
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(3);
     
    dispatch_group_t group = dispatch_group_create();
     
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
     
    for (int i=0; i<20; i++) {  
      // for 20次,每次创建一个线程执行任务,当线程任务达到3个后,创建任务就会被阻塞,至到有任务完成并signal后,wait等待线程才会解除阻塞,继续被执行
        dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
        dispatch_group_async(group, queue, ^{
            NSLog(@"%zd ---%@",i,[NSThread currentThread]);
            [NSThread sleepForTimeInterval:0.2];
            dispatch_semaphore_signal(semaphore);
        });
    }
     
    dispatch_group_wait(group, 4); // group任务等待4秒,如果group所有任务任务4秒还未执行完成,执行下面任务
     
    dispatch_group_notify(group, queue, ^{
        NSLog(@"dispatch_group_notify"); // group内任务全部执行完毕,通知回调
    });
     
    NSLog(@"semaphore end...");
    // 创建并运行主运行循环
    NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
    [runLoop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:5]];
    
    return 0;
}

运行结果如下,会有三个并发线程来执行任务:

在这里插入图片描述

二、NSThread

1.NSThread的简介

NSThread 是苹果官方提供的,使用起来比 pthread 更加面向对象,简单易用,可以直接操作线程对象。不过也需要需要程序员自己管理线程的生命周期(主要是创建),在开发的过程中偶尔使用 NSThread。比如经常调用 [NSThread currentThread] 来显示当前的进程信息或调用 [NSThread isMainThread] 来判断当前的进程是否为主线程。

2.NSThread的使用

a.创建线程

方法一:
 /// 返回NSThread使用给定参数初始化的对象。
/// target        目标对象。
/// selector    	方法选择器 该选择器最多使用一个参数,并且不能具有返回值。
/// object        单个参数传递给目标。可以传 nil。

mytest *obj = [[mytest alloc]init];
NSThread *thread1 = [[NSThread alloc] initWithTarget:obj selector:@selector(asyncTaskWithMain) object:nil];
thread1.name = @"thread1";
[thread1 start];

方法二:
NSThread *thread2 = [[NSThread alloc] initWithBlock:^{
			//code...
    }
}];
thread2.name = @"thread2";
[thread2 start];

方法三:
NSThread *thread3 = [[NSThread alloc] init];
thread3.name = @"thread3";
[thread3 start];

b.启动线程

//启动线程(内部调用main方法)。
- (void)start;

此方法异步生成新线程,并在新线程上调用接收方的main方法。
一旦线程开始执行,executing属性返回YES,这可能发生在start方法返回之后。
如果使用目标和选择器初始化接收器,则默认的main方法会自动调用该选择器。
如果此线程是应用程序中分离的第一个线程,则此方法将带有对象nil的NSWillBecomeMultiThreadedNotification发送到默认通知中心。

[thread1 start];

--------------------------------------------------------------------------------------------------------------------------
//线程的入口方法。
- (void)main;

此方法的默认实现是调用指定目标上的选择器。如果自定义NShread子类,则可以重写此方法使用它来实现线程的任务。
不需要调用super完成默认的实现。不直接调用此方法。始终通过调用start方法来启动线程。

@implementation MyThread
- (void)main {
  NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
    NSLog(@"%@", MyThread.currentThread.name);
}
@end

MyThread *thread1 = [[MyThread alloc] init];
[thread1 start];

--------------------------------------------------------------------------------------------------------------------------
  //分离新线程并开启,使用指定的选择器作为线程的任务。
/// selector	方法选择器 该选择器最多使用一个参数,并且不能具有返回值。
/// target 		方法调用者对象。
/// object		单个参数传递给目标。可以传 nil。
[NSThread detachNewThreadSelector:@selector(run) toTarget:self withObject:nil];

[NSThread detachNewThreadWithBlock:^{
    for (int i = 0; i < 100; i++) {
        NSLog(@"%i-%@",i,NSThread.currentThread.name);
    }
}];

c.停止/阻塞线程

//阻止当前线程直到指定的时间。当线程被阻塞时,不会发生运行循环处理。
+ (void)sleepUntilDate:(NSDate *)date;

//用于使当前线程暂停执行指定的时间间隔。单位是秒。
+ (void)sleepForTimeInterval:(NSTimeInterval)ti;

//终止当前线程。
+ (void)exit;

//停止执行当前任务。将线程设置为取消状态。
- (void)cancel;

d.获取线程的状态

//一个布尔值,判断线程是否正在执行。如果线程正在执行,返回 YES,否则返回 NO。
@property(readonly, getter=isExecuting) BOOL executing;

//一个布尔值,判断线程是否已完成执行。如果线程已完成执行,返回 YES,否则返回 NO。
@property(readonly, getter=isFinished) BOOL finished;

//一个布尔值,判断线程是否被取消执行(可以取消)。如果线程被取消执行,返回 YES,否则返回 NO。
//调用对象的cancel方法可将此属性的值设置为 YES。如果线程支持取消,应该定期(在main方法)检查该属性,如果返回 YES 就退出。
@property(readonly, getter=isCancelled) BOOL cancelled;

e.主线程相关

//一个布尔值,判断当前线程是否是主线程。如果是主线程,返回 YES,否则返回 NO。
@property(class, readonly) BOOL isMainThread;

//一个布尔值,判断指定线程是否是主线程。如果是主线程,返回 YES,否则返回 NO。
@property(readonly) BOOL isMainThread;

//获取主线程对象。
@property(class, readonly, strong) NSThread *mainThread;

f.查询线程环境

//返回应用程序是否是多线程的。
+ (BOOL)isMultiThreaded;

//获取当前执行的线程对象。
@property(class, readonly, strong) NSThread *currentThread;

//获取调用堆栈返回地址的数组。数组元素都是一个包装 NSUInteger 值的 NSNumber 对象。
@property(class, readonly, copy) NSArray<NSNumber *> *callStackReturnAddresses;

//获取调用堆栈符号的数组。
@property(class, readonly, copy) NSArray<NSString *> *callStackSymbols;

g.线程属性

//线程的名称。
@property(copy) NSString *name;

//线程的堆栈大小,以字节为单位。默认是512K。
@property NSUInteger stackSize;

//线程的优先级,由0.0到1.0之间的浮点数指定,其中1.0是最高优先级。默认是 0.5。优先级更高的线程,被CPU调度到的概率会更高。
@property double threadPriority;

//设置当前线程的优先级。如果优先级设置成功 返回 YES,否则 返回 NO 。
+ (BOOL)setThreadPriority:(double)p;

//用于表示执行任务的优先级
@property NSQualityOfService qualityOfService;
//NSQualityOfServiceUserInteractive:最高优先级,主要用于提供交互UI的操作,比如处理点击事件,绘制图像到屏幕上
//NSQualityOfServiceUserInitiated:次高优先级,主要用于执行需要立即返回的任务
//NSQualityOfServiceDefault:默认优先级,当没有设置优先级的时候,线程默认优先级
//NSQualityOfServiceUtility:普通优先级,主要用于不需要立即返回的任务
//NSQualityOfServiceBackground:后台优先级,用于完全不紧急的任务

3.NSThread 线程间通讯

线程间通信的体现:1个线程传递数据给另1个线程; 在1个线程中执行完特定任务后,转到另1个线程继续执行任务。

a.在当前线程上执行操作

在当前线程上执行操作,可以调用 NSObject 的 performSelector: 相关方法:

/// 将指定的消息发送到接收方并返回消息的结果。
/// aSelector 指定的消息,消息不应包含参数。
- (id)performSelector:(SEL)aSelector;

/// 将指定的消息发送到接收方并返回消息的结果。
/// aSelector 指定的消息
/// object 作为消息唯一参数(对象)。
- (id)performSelector:(SEL)aSelector withObject:(id)object;

/// 将指定的消息发送到接收方并返回消息的结果。
/// aSelector 指定的消息
/// object1 作为消息的第一个参数(对象)
/// object2 作为消息的第二个参数(对象)
- (id)performSelector:(SEL)aSelector withObject:(id)object1 withObject:(id)object2;

//以上方法位于NSObject (NSThreadPerformAdditions)分类中,所有继承NSObject实例化对象都可调用以下方法

b.在主线程上执行操作

苹果声明 UI 更新一定要在 UI 线程(主线程)中执行,虽然不是所有后台线程更新 UI 都会出错。

/// 使用默认模式在主线程上调用接收方的方法。
/// aSelector 要调用的方法的选择器。该方法没有返回值,并且应采用 id 类型的单个参数,或者不带参数。
/// arg       方法的参数。如果方法没有参数,则传递 nil。
/// wait			(完成操作之前)线程是否阻塞。指定 YES 阻止线程(后面的操作等待至该操作完成在执行),否则,指定 NO。
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)arg waitUntilDone:(BOOL)wait;

/// 使用指定的模式在主线程上调用接收器的方法。
/// aSelector 要调用的方法的选择器。该方法没有返回值,并且应采用 id 类型的单个参数,或者不带参数。
/// arg       方法的参数。如果方法没有参数,则传递 nil。
/// wait			(完成操作之前)线程是否阻塞。指定 YES 阻止线程(后面的操作等待至该操作完成在执行),否则,指定 NO。
/// array			允许执行操作的模式的数组。该数组必须至少包含一模式。如果nil为此参数指定或为空数组,则此方法将不执行操作。
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)arg waitUntilDone:(BOOL)wait modes:(NSArray<NSString *> *)array;

c.在指定线程上执行操作

/// 使用默认模式在指定线程上调用接收器的方法。
/// aSelector 要调用的方法的选择器。该方法没有返回值,并且应采用 id 类型的单个参数,或者不带参数。
/// thr				指定的线程
/// arg       方法的参数。如果方法没有参数,则传递 nil。
/// wait			(完成操作之前)线程是否阻塞。指定 YES 阻止线程(后面的操作等待该操作完成再执行),否则,指定 NO。
/// array			允许执行操作的模式的数组。该数组必须至少包含一模式。如果nil为此参数指定或为空数组,则此方法将不执行操作。
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(id)arg waitUntilDone:(BOOL)wait;

/// 使用指定的模式在指定的线程上调用接收器的方法。
/// aSelector 要调用的方法的选择器。该方法没有返回值,并且应采用 id 类型的单个参数,或者不带参数。
/// thr				指定的线程
/// arg       方法的参数。如果方法没有参数,则传递 nil。
/// wait			(完成操作之前)线程是否阻塞。指定 YES 阻止线程(后面的操作等待至该操作完成在执行),否则,指定 NO。
/// array			允许执行操作的模式的数组。该数组必须至少包含一模式。如果nil为此参数指定或为空数组,则此方法将不执行操作。
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(id)arg waitUntilDone:(BOOL)wait modes:(NSArray *)array;

d.在新后台线程上执行操作

/// 在新线程上执行操作 此方法在您的应用程序中创建一个新线程,如果尚未将其置于多线程模式,则将其置于多线程模式。
/// aSelector 要调用的方法的选择器。该方法没有返回值,并且应采用 id 类型的单个参数,或者不带参数。
/// arg       方法的参数。如果方法没有参数,则传递 nil。
- (void)performSelectorInBackground:(SEL)aSelector withObject:(nullable id)arg 

三、NSOperation

1.NSOperation的简介

NSOperation 作用:

配合使用 NSOperation 和 NSOperationQueue 也是苹果提供的一套多线程解决方案。NSOperation 是基于 GCD 更高一层的封装,完全面向对象。但是比 GCD 更简单易用、代码可读性也更高。

NSOperation 和 GCD 的区别:

  1. GCD 是纯 C 语言的 API,而NSOperation则是OC的对象。
  2. 在 GCD 中,任务用 block 来表示,而 block 是个轻量级的数据结构;NSOperation中的操作, 则是个更加重量级的OC对象。

使用NSOperation的好处:

  1. 可以方便的调用cancel方法来取消某个操作,而 GCD 中的任务是无法被取消的。(iOS8之后可以调用dispatch_block_cancel 来取消,需要注意必须用 dispatch_block_create 创建 dispatch_block_t )。
  2. 可以指定操作间的依赖关系, 方便的控制执行顺序。
  3. 可以通过KVO提供对操作对象的精细控制(如监听当前操作是否被取消或是否已经完成等)。
  4. 可以方便的指定操作优先级。
  5. 可以自定义NSOperation的子类可以实现操作重用。

操作/操作队列:

NSOperation 操作:

  1. 封装执行的任务。在 GCD 中将任务放在 block 中。
  2. NSOperation 类是一个抽象类,不直接使用,而是使用子类(NSInvocationOperation 或 NSBlockOperation),或者自定义NSOperation,通过实现内部相应的方法来封装操作。执行实际任务。

NSOperationQueue 操作队列:
用来存放和控制操作的队列。不同于 GCD 中的调度队列 FIFO(先进先出)的原则。操作队列根据操作的优先级和就绪程度执行排队的操作。

实现多线程使用步骤:

  1. NSOperation 需要配合 NSOperationQueue 来实现多线程:
  2. 创建操作:先将需要执行的操作封装到一个 NSOperation 对象中。
  3. 创建队列:创建 NSOperationQueue 对象。
  4. 将 NSOperation 对象添加到 NSOperationQueue 对象中。

2.NSOperation 基本使用

封装(创建)操作

单独使用操作,可以通过调用 start 方法来自己执行操作。NSOperation是个抽象类,不能用来封装操作。只有使用它的子类来封装操作。有三种方式来封装操作。

  1. 使用子类 NSInvocationOperation
  2. 使用子类 NSBlockOperation
  3. 自定义继承自 NSOperation 的子类,通过实现内部相应的方法来封装操作

a.NSInvocationOperation

单独使用 NSInvocationOperation 在当前线程同步执行,并没有开启新线程

///单独使用 NSInvocationOperation 在当前线程同步执行,并没有开启新线
- (void)invocationOperation {
    //01 封装操作对象
    NSInvocationOperation *op1 = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(download) object:nil];
    NSInvocationOperation *op2 = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(download) object:nil];
    NSInvocationOperation *op3 = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(download) object:nil];
    NSInvocationOperation *op4 = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(download) object:nil];
    //02 执行操作
    [op1 start];
    [op2 start];
    [op3 start];
    [op4 start];
}

- (void)download {
  [NSThread sleepForTimeInterval:2]; //模拟耗时操作
  NSLog(@"%@",NSThread.currentThread);
}

运行的结果如下:

在这里插入图片描述

b.NSBlockOperation

  • 单独使用使 NSBlockOperation 执行一个操作,操作是在当前线程执行的,并没有开启新线程。
  • 但是,NSBlockOperation 还提供了一个方法 addExecutionBlock:,通过该方法可以添加额外的任务。
  • 这些任务(包括 blockOperationWithBlock: 中的任务)可以在不同的线程中同时(并发)执行。
  • 如果添加的任务多,blockOperationWithBlock: 中的任务也可能会在其他(非当前)线程中执行,由系统决定。
  • 只有当该操作所有的任务已经完成执行时,才视为完成该操作。
- (void)blockOperation {
    //01 封装操作对象
    NSBlockOperation *op1 = [NSBlockOperation blockOperationWithBlock:^{
        [NSThread sleepForTimeInterval:2];//模拟耗时操作
        NSLog(@"1--%@", NSThread.currentThread);
    }];

    NSBlockOperation *op2 = [NSBlockOperation blockOperationWithBlock:^{
        [NSThread sleepForTimeInterval:2];//模拟耗时操作
        NSLog(@"2--%@", NSThread.currentThread);
    }];
    
    NSBlockOperation *op3 = [NSBlockOperation blockOperationWithBlock:^{
        [NSThread sleepForTimeInterval:2];//模拟耗时操作
        NSLog(@"3.0--%@", NSThread.currentThread);
    }];
    
    NSBlockOperation *op4 = [NSBlockOperation blockOperationWithBlock:^{
        [NSThread sleepForTimeInterval:2];//模拟耗时操作
        NSLog(@"4--%@", NSThread.currentThread);
    }];
  
    //添加额外的任务
    //当一个操作中的任务数量>1的时候,就会开启子线程和当前线程一起执行任务
    [op3 addExecutionBlock:^{
        [NSThread sleepForTimeInterval:2];//模拟耗时操作
        NSLog(@"3.1--%@", NSThread.currentThread);
    }];
    
    [op3 addExecutionBlock:^{
        [NSThread sleepForTimeInterval:2];//模拟耗时操作
        NSLog(@"3.2--%@", NSThread.currentThread);
    }];
    //02 执行操作
    [op1 start];
    [op2 start];
    [op3 start];
    [op4 start];
}

运行的结果如下:

在这里插入图片描述

c.NSOperation 自定义

如果使用子类不能满足需求,可以自定义 NSOperation。然后重写 - (void)main 方法,在里面实现想执行的任务。当 main 执行完返回的时候,才视为完成该操作。

重写- (void)main方法的注意点:

  • 自己创建自动释放池(因为如果是异步操作,无法访问主线程的自动释放池)
  • 经常通过- (BOOL)isCancelled方法检测操作是否被取消,对取消做出响应
@interface MyOperation : NSOperation
@end

@implementation MyOperation
- (void)main {
     @autoreleasepool {
        if (self.isCancelled) return;
        for (int i = 9995; i < 10000; i++) {
            NSLog(@"%i - %@",i,NSThread.currentThread);
        }
     }
}
@end

int main(int argc, const char * argv[]) {
    MyOperation *testObj = [[MyOperation alloc]init];
    [testObj start];

    // 创建并运行主运行循环
    NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
    [runLoop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:10]];
    return 0;
}

运行结果如下:

在这里插入图片描述

d.NSOperation 常用属性和方法

取消操作方法:

//取消操作,此方法不会强制操作代码停止,而是标记 isCancelled 状态为YES。
//如果操作已经完成执行,则此方法无效。只能取消队列中处理等待状态的操作,当前操作不可分割必须执行完毕。
- (void)cancel;

获取操作状态:

//获取操作状态
//指示操作是否已取消状态的布尔值。默认值为 NO。调用此对象的cancel方法可将此属性的值设置为 YES。操作对象负责定期调用此方法,并在返回 YES 时停止操作。
//在完成操作任务之前,应该始终检查此属性的值,通常在自定义main方法的开头以及每一个耗时操作之前检查它。
//一个操作在开始执行之前或在执行过程中的任何时候都有可能被取消。因此,在main方法开头检查属性的值(在整个方法中定期检查)可以在操作被取消时尽快退出。
@property(readonly, getter=isCancelled) BOOL cancelled;

//指示操作当前是否正在执行状态的布尔值。如果操作当前正在执行其主任务,则此属性的值为 YES;否则,此属性的值为 NO。
//在实现并发操作对象时,必须重写此属性的实现,以便可以返回操作的执行状态。在自定义实现中,每当操作对象的执行状态更改时,都必须为键路径生成KVO通知。
//对于非并发操作,不需要重新实现此属性。
@property(readonly, getter=isExecuting) BOOL executing;

//指示操作是否是已完成状态的布尔值。如果操作已完成任务,则此属性的值为YES;如果正在执行该任务或尚未启动该任务,则该属性的值为 NO。
//在实现并发操作对象时,必须重写此属性的实现,以便可以返回操作的完成状态。在自定义实现中,每当操作对象的完成状态更改时,都必须为键路径生成KVO通知。
//对于非并发操作,不需要重新实现此属性。
@property(readonly, getter=isFinished) BOOL finished;

//指示操在是否是就绪状态(可以执行)的布尔值。操作的就绪状态取决于它们对其他操作的依赖性,还可能取决于您定义的自定义条件。
//如果要使用自定义条件来定义操作对象的就绪状态,请重新实现此属性并返回一个准确反映接收方就绪状态的值。
//如果这样做,您的自定义实现必须从中获取默认属性值super,并将该就绪值合并到该属性的新值中。
//在自定义实现中,每当操作对象的就绪状态发生更改时,都必须为关键路径生成KVO通知。
@property(readonly, getter=isReady) BOOL ready;

依赖关系:

//添加依赖,使当前操作依赖于操作op的完成。如果当前操作已经在执行任务,那么添加依赖项没有实际效果。
//此方法可能会更改当前操作的isReady和dependencies属性。
//在一组操作之间创建任何循环依赖关系会导致操作之间出现死锁。
- (void)addDependency:(NSOperation *)op;

//移除依赖,取消当前操作对操作 op 的依赖。此方法可能会更改当前操作的isReady和dependencies属性。
- (void)removeDependency:(NSOperation *)op;

//当前操作依赖的所有操作对象数组。此属性包含一个NSOperation对象数组。要将对象添加到此数组,请使用addDependency:方法。
//操作对象必须在其所有依赖的操作完成执行后才能执行。操作完成执行后,不会从此依赖数组中删除操作。
//可以使用此数组来跟踪所有相关操作,包括已经完成执行的操作。从此列表中删除操作的唯一方法是使用removeDependency:方法。
@property(readonly, copy) NSArray<NSOperation *> *dependencies;

3.NSOperationQueue 基本使用

NSOperationQueue 一共有两种队列:主队列、自定义队列。其中自定义队列同时包含了串行、并发功能。

a.主队列

通过mainQueue获得,凡是放到主队列中的任务都将在主线程执行。

// 主队列获取方法
//注:不包括操作使用addExecutionBlock:添加的额外操作,额外操作可能在其他线程执行。

NSOperationQueue *queue = [NSOperationQueue mainQueue];

b.自定义队列

通过 alloc init 获得。自定义队列同时具备了并发和串行的功能(默认是并发队列),通过设置最大并发数属性来控制任务是并发执行还是串行执行

// 自定义队列创建方法
NSOperationQueue *queue = [[NSOperationQueue alloc] init];

c.将操作加入到队列中

NSOperation 需要配合 NSOperationQueue 来实现多线程。一个操作对象一次最多只能在一个操作队列中,如果该操作已经在另一个队列中,则此方法将抛出 NSInvalidArgumentException 异常。如果操作当前正在执行或已完成执行,此方法将抛出 NSInvalidArgumentException 异常。

  • addOperation:

使用 addOperation: 将操作加入到操作队列后能够开启新线程,进行并发执行。

//01 创建队列
NSOperationQueue *queue = [[NSOperationQueue alloc] init];

//02 封装操作
NSBlockOperation *op1 = [NSBlockOperation blockOperationWithBlock:^{
    for (int i = 0; i < 2; i++) {
        [NSThread sleepForTimeInterval:2];
         NSLog(@"1-%i---%@",i, NSThread.currentThread);
    }
}];

//03 把操作添加到队列
[queue addOperation:op1]; //内部调用  [op1 start];

用上述方式编写用例,运行结果如下:

在这里插入图片描述

  • addOperationWithBlock:

此方法首先将单个 Block 包装在操作对象中,将操作加入到操作队列。不能获取操作对象信息。能够开启新线程,进行并发执行

 NSOperationQueue *queue = [[NSOperationQueue alloc] init];
  
  //直接将包含操作的 block 加入到队列
    [queue addOperationWithBlock:^{
        for (int i = 0; i < 2; i++) {
            [NSThread sleepForTimeInterval:2];
            NSLog(@"1-%i---%@",i, NSThread.currentThread);
        }
    }];
  • addOperations: waitUntilFinished:
    一组操作加入到操作队列, 如果 wait 为 YES 当前线程将被阻塞,添加的一组操作按顺序执行(操作内的任务并发),直到所有指定的操作完成执行。
int main(int argc, const char * argv[]) {
    //01 创建队列
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];

    //02 封装操作
    NSBlockOperation *op1 = [NSBlockOperation blockOperationWithBlock:^{
        for (int i = 0; i < 2; i++) {
            [NSThread sleepForTimeInterval:2];
            NSLog(@"1-%i---%@",i, NSThread.currentThread);
        }
    }];
    NSBlockOperation *op2 = [NSBlockOperation blockOperationWithBlock:^{
        for (int i = 0; i < 2; i++) {
            [NSThread sleepForTimeInterval:2];
            NSLog(@"2-%i---%@",i, NSThread.currentThread);
        }
    }];
    NSBlockOperation *op3 = [NSBlockOperation blockOperationWithBlock:^{
        for (int i = 0; i < 2; i++) {
            [NSThread sleepForTimeInterval:2];
            NSLog(@"3-%i---%@",i, NSThread.currentThread);
        }
    }];
    NSBlockOperation *op4 = [NSBlockOperation blockOperationWithBlock:^{
        for (int i = 0; i < 2; i++) {
            [NSThread sleepForTimeInterval:2];
            NSLog(@"4-%i---%@",i, NSThread.currentThread);
        }
    }];

    //先创建操作,再将操作加入到队列
    //03 把操作添加到队列
    [queue addOperation:op1]; //内部调用  [op1 start];
    //后面的操作必须等待op2, op3(按顺序)执行完成。
    [queue addOperations:@[op2, op3] waitUntilFinished:YES];
    [queue addOperation:op4]; //内部调用  [op4 start];
    
    // 创建并运行主运行循环
    NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
    [runLoop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:10]];
    
    return 0;
}

如果不用这个方法,那么op4的执行顺序是未定义的;下面是用了该方法后,op4在op2和op3执行完毕后才执行:
在这里插入图片描述

  • addBarrierBlock:

该方法阻止在该操作完成之前执行任何后续操作,与 dispatch_barrier_async 函数一样。

int main(int argc, const char * argv[]) {
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
       
       [queue addOperationWithBlock:^{
           for (int i = 0; i < 2; i++) {
               [NSThread sleepForTimeInterval:2];
               NSLog(@"1-%i---%@",i, NSThread.currentThread);
           }
       }];
       
       [queue addOperationWithBlock:^{
           for (int i = 0; i < 2; i++) {
               [NSThread sleepForTimeInterval:2];
               NSLog(@"2-%i---%@",i, NSThread.currentThread);
           }
       }];
       
       //其作用类似于 dispatch_barrier_async 函数。
       [queue addBarrierBlock:^{
           for (int i = 0; i < 2; i++) {
               [NSThread sleepForTimeInterval:2];
               NSLog(@"%i+++++%@",i, NSThread.currentThread);
              }
          }];
       [queue addOperationWithBlock:^{
           for (int i = 0; i < 2; i++) {
               [NSThread sleepForTimeInterval:2];
               NSLog(@"3-%i---%@",i, NSThread.currentThread);
           }
       }];
       
       [queue addOperationWithBlock:^{
           for (int i = 0; i < 2; i++) {
               [NSThread sleepForTimeInterval:2];
               NSLog(@"4-%i---%@",i, NSThread.currentThread);
           }
       }];

    
    // 创建并运行主运行循环
    NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
    [runLoop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:20]];
    
    return 0;
}

运行结果如下:

在这里插入图片描述

d.控制串行/并发执行

NSOperationQueue 创建的自定义队列通过设置 maxConcurrentOperationCount 控制串行、并发。

// 创建一个新的 NSOperationQueue
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
queue.maxConcurrentOperationCount = 1;// 用来控制队列中可以有多少个操作同时参与并发执行。

//该属性控制的不是并发线程的数量,而是同时能并发执行的最大操作数。
//一个操作也并非只能在一个线程中运行(如果一个操作有多个任务时,这些任务可以在不同的线程中并发执行)
//该属性默认情况下为-1,表示不进行限制,可进行并发执行。
//设置为1时,且每个操作只有一个任务时,队列为串行队列。只能串行执行。一个操作完成之后,下一个操作才开始执行

用上述代码进行演示,可以看到设置为1时,是串行队列:

在这里插入图片描述

e.操作依赖和监听

NSOperationNSOperationQueue 它能添加操作之间的依赖关系。通过操作依赖,可以很方便的控制操作之间的执行先后顺序。

- (void)addDependency:(NSOperation *)op; //添加依赖,使当前操作依赖于操作 op 的完成。

- (void)removeDependency:(NSOperation *)op; //移除依赖,取消当前操作对操作 op 的依赖。`

@property (readonly, copy) NSArray<NSOperation *> *dependencies; //在当前操作开始执行之前完成执行的所有操作对象数组。

@property(copy) void (^completionBlock)(void); //在操作的主任务完成后要执行的块。

用下面这段代码来演示依赖和监听:

int main(int argc, const char * argv[]) {
    //01 创建队列
        NSOperationQueue *queue = [[NSOperationQueue alloc]init];
        NSOperationQueue *queue2 = [[NSOperationQueue alloc]init];
        
        //02 封装操作对象
        NSBlockOperation *op1 = [NSBlockOperation blockOperationWithBlock:^{
            NSLog(@"1----%@",NSThread.currentThread);
        }];
        
        NSBlockOperation *op2 = [NSBlockOperation blockOperationWithBlock:^{
            NSLog(@"2----%@",NSThread.currentThread);
        }];
        
        NSBlockOperation *op3 = [NSBlockOperation blockOperationWithBlock:^{
            NSLog(@"3----%@",NSThread.currentThread);
        }];
        
        NSBlockOperation *op4 = [NSBlockOperation blockOperationWithBlock:^{
            NSLog(@"4----%@",NSThread.currentThread);
        }];
        
        NSBlockOperation *op5 = [NSBlockOperation blockOperationWithBlock:^{
            NSLog(@"5----%@",NSThread.currentThread);
        }];
        
        //监听任务执行完毕
        op4.completionBlock = ^{
            NSLog(@"op4完成%@",NSThread.currentThread);
        };
        
        //03 设置操作依赖:4->3->2->1->5
        //⚠️ 不能设置循环依赖,结果就是两个任务都不会执行, 如果有依这两个任务的任务也不会执行
        [op5 addDependency:op1];
        [op1 addDependency:op2];
        [op2 addDependency:op3];
        [op3 addDependency:op4];
        
        //04 把操作添加到队列
        [queue addOperation:op1];
        [queue addOperation:op2];
        [queue addOperation:op3];
        [queue2 addOperation:op4];
        [queue2 addOperation:op5];

    
    // 创建并运行主运行循环
    NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
    [runLoop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:20]];
    
    return 0;
}

运行的结果如下:

在这里插入图片描述

f.操作优先级

NSOperation 提供了queuePriority(优先级)属性,queuePriority 属性适用于同一操作队列中的操作,不适用于不同操作队列中的操作。默认情况下,所有新创建的操作对象优先级都是NSOperationQueuePriorityNormal。但是我们可以通过 setQueuePriority: 方法来改变当前操作在同一队列中的执行优先级。

// 优先级的取值
typedef NS_ENUM(NSInteger, NSOperationQueuePriority) {
    NSOperationQueuePriorityVeryLow = -8L,
    NSOperationQueuePriorityLow = -4L,
    NSOperationQueuePriorityNormal = 0,(默认)
    NSOperationQueuePriorityHigh = 4,
    NSOperationQueuePriorityVeryHigh = 8
};

// 优先级的设置
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
NSBlockOperation *operation1 = [NSBlockOperation blockOperationWithBlock:^{
    NSLog(@"Operation 1 is executed.");
}];
operation1.queuePriority = NSOperationQueuePriorityLow;
  • 当一个操作的所有依赖都已经完成(或没有依赖)时,操作对象通常会进入准备就绪状态,等待执行。
  • 就绪状态下的操作开始执行顺序(非结束执行顺序)由操作之间相对的优先级决定(优先级是操作对象自身的属性)
  • queuePriority 属性,决定了就绪状态下所有操作之间的开始执行顺序。并且,优先级不能取代依赖关系。
  • 一个队列中,既包含高优先级操作,又包含低优先级操作,并且都已经准备就绪,那么队列先执行高优先级操作。
  • 一个队列中,未准备就绪的操作优先级比准备就绪的操作优先级高。也会优先执行准备就绪的操作。
  • 优先级不能取代依赖关系。如果要控制操作间的启动顺序,则必须使用依赖关系。
int main(int argc, const char * argv[]) {
    //01 创建队列
        NSOperationQueue *queue = [[NSOperationQueue alloc]init];
        
        //02 封装操作对象
        NSBlockOperation *op1 = [NSBlockOperation blockOperationWithBlock:^{
            NSLog(@"1----%@",NSThread.currentThread);
        }];
        op1.queuePriority = NSOperationQueuePriorityLow;
        
        NSBlockOperation *op2 = [NSBlockOperation blockOperationWithBlock:^{
            NSLog(@"2----%@",NSThread.currentThread);
        }];
        op2.queuePriority = NSOperationQueuePriorityNormal;
        
        NSBlockOperation *op3 = [NSBlockOperation blockOperationWithBlock:^{
            NSLog(@"3----%@",NSThread.currentThread);
        }];
        op3.queuePriority = NSOperationQueuePriorityVeryHigh;
        
        //04 把操作添加到队列
        [queue addOperation:op1];
        [queue addOperation:op2];
        [queue addOperation:op3];
    
    // 创建并运行主运行循环
    NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
    [runLoop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:5]];
    
    return 0;
}

运行结果如下,可以证明就绪状态下的操作顺序由优先级决定:

在这里插入图片描述

总结

1. Grand Central Dispatch (GCD)

优点:

  1. 简化了多线程代码的编写,无需直接管理线程。
  2. 高效地使用系统资源,自动管理线程池。
  3. 支持任务同步、异步执行和延时执行。
  4. 提供了队列(串行队列和并发队列)和组的概念,方便进行任务组织和同步。

缺点:

  1. 低级别的 API,需要自己处理任务之间的同步和数据竞争。
  2. 对于复杂的依赖关系或执行顺序的控制,代码可能会变得复杂。

2. NSOperationQueue

优点:

  1. 基于对象的高级抽象,易于理解和使用。
  2. 支持设置操作间的依赖关系,自动处理依赖执行。
  3. 可以方便地取消已经加入队列但还未执行的操作。
  4. 支持设置最大并发操作数,方便控制并发级别。
  5. 可以与 GCD 配合使用,结合两者的优势。

缺点:

  1. 相较于 GCD,性能略低,因为其提供的是更高级的抽象。
  2. 操作完成的通知需要通过 KVO 或其他机制手动处理。

3. NSThread

优点:

  1. 提供最大的控制权,可以精细管理线程的行为。
  2. 可以直接与底层 Unix 线程 API 交互,适用于需要高度控制的场景。

缺点:

  1. 需要手动管理线程的生命周期,包括创建、启动、同步和销毁。
  2. 线程资源消耗较大,不适合大量并发。
  3. 编写多线程安全的代码难度较高,容易出错。

使用场景

  • GCD: 适用于执行简单的并发任务,如并行处理数据、异步执行耗时任务等。
  • NSOperationQueue: 适用于有复杂任务依赖关系的场景,需要取消操作的功能,或者需要通过面向对象方式管理任务。
  • NSThread: 适用于需要精细控制线程行为的场景,或者需要与 C/C++ 库紧密集成的低级应用。

总结

在实际开发中,通常推荐使用 GCDNSOperationQueue,因为它们提供了更简洁、更安全的并发编程方式。NSThread 因其复杂性和资源消耗,在现代 iOS 开发中使用较少。

;