美文网首页iOS Kit
iOS GCD之任务与队列

iOS GCD之任务与队列

作者: 远方竹叶 | 来源:发表于2020-11-08 17:49 被阅读0次

什么是GCD?

Grand Central Dispatch(GCD)是 Apple 开发的一个多核编程的解决方法。它是一套纯 C 语言的 API,主要用于优化应用程序以支持多核处理器以及其他对称多处理系统。它是一个在线程池模式的基础上执行的并发任务。在 Mac OS X 10.6 雪豹中首次推出,也可在 iOS 4 及以上版本使用。

为什么要用GCD?有什么优势?

  • GCD 可用于多核的并行运算
  • GCD 会自动利用更多的 CPU 内核(比如双核、四核)
  • GCD 会自动管理线程的生命周期(创建线程、调度任务、销毁线程)
  • 程序员只需要告诉 GCD 想要执行什么任务,不需要编写任何线程管理代码

核心

在学习使用之前,先介绍两个核心概念:任务和队列

任务

就是 执行操作 的意思,换句话说就是在线程中执行的那段代码,在 GCD 中是放在 block 中的。执行任务有两种方式:同步执行(sync)和异步执行(async)

  • 异步执行(async)

异步添加任务到指定的队列中,它不会做任何等待,可以继续执行任务。可以在新的线程中执行任务,具备开启新线程的能力。

  • 同步执行(sync)

同步添加任务到指定的队列中,在添加的任务执行结束之前,会一直等待,直到队列里面的任务完成之后再继续执行。只能在当前线程中执行任务,不具备开启新线程的能力。

异步执行(async)虽然具有开启新线程的能力,但是并不一定开启新线程。这跟任务所指定的队列类型有关

队列(Dispatch Queue)

这里的队列指执行任务的等待队列,即用来存放任务的队列。队列是一种特殊的线性表,采用 FIFO(先进先出)的原则,即新任务总是被插入到队列的末尾,而读取任务的时候总是从队列的头部开始读取。每读取一个任务,则从队列中释放一个任务。

GCD 中,有两种队列:串行队列(Serial Dispatch Queue)和并发队列(Concurrent Dispatch Queue)

  • 串行队列(Serial Dispatch Queue)

每次只有一个任务被执行。让任务一个接着一个地执行。(只开启一个线程,一个任务执行完毕后,再执行下一个任务)

  • 并发队列(Concurrent Dispatch Queue)

可以让多个任务并发(同时)执行。(可以开启多个线程,并且同时执行任务)

并发队列的并发功能只有在异步函数下才有效

使用步骤

GCD 的使用非常简单,只有两步

  • 创建一个队列(串行队列/并发队列)
  • 将任务添加到队列中,然后系统就会根据任务类型执行任务(同步执行或异步执行)

队列的创建方法

可以通过 dispatch_queue_create 来创建队列,源码如下:

API_AVAILABLE(macos(10.6), ios(4.0))
DISPATCH_EXPORT DISPATCH_MALLOC DISPATCH_RETURNS_RETAINED DISPATCH_WARN_RESULT
DISPATCH_NOTHROW
dispatch_queue_t
dispatch_queue_create(const char *_Nullable label,
        dispatch_queue_attr_t _Nullable attr);

可以看到,需要传入两个参数:第一个参数表示队列的唯一标识符,用于 DEBUG,可为空;第二个参数用来识别是串行队列还是并发队列。DISPATCH_QUEUE_SERIAL 表示串行队列,DISPATCH_QUEUE_CONCURRENT 表示并发队列。

  • 创建一个串行队列
dispatch_queue_t seialQueue = dispatch_queue_create("xxx", DISPATCH_QUEUE_SERIAL);
  • 创建一个并发队列
dispatch_queue_t concurrentQueue = dispatch_queue_create("xxx", DISPATCH_QUEUE_CONCURRENT);

队列的获取方法

GCD 提供了一个特殊的串行队列:主队列(Main Dispatch Queue)。所有放在主队列中的任务,都会放到主线程中执行。获取方法如下:

dispatch_queue_t mainQueue = dispatch_get_main_queue();

此外,GCD 还提供了一个全局并发队列:(Global Dispatch Queue)

API_AVAILABLE(macos(10.6), ios(4.0))
DISPATCH_EXPORT DISPATCH_CONST DISPATCH_WARN_RESULT DISPATCH_NOTHROW
dispatch_queue_global_t
dispatch_get_global_queue(intptr_t identifier, uintptr_t flags);

可以看到,获取全局队列也需要传入两个参数。第一个参数表示队列优先级,一般用DISPATCH_QUEUE_PRIORITY_DEFAULT。第二个参数暂时没用,用 0 即可。

dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

任务的创建方法

GCD 提供了两种执行任务的创建方法:

// 同步执行任务创建方法
dispatch_sync(queue, ^{
    // 同步执行任务代码
});

// 异步执行任务创建方法
dispatch_async(queue, ^{
    // 异步执行任务代码
});

综上所述,GCD 为开发者提供了两种队列(串行/并发)以及两种任务执行方法(同步/异步),所以这里面就有了四种组合。再结合上面提到的主队列和全局并发队列(可以当作是普通的并发队列),现在就有了6种组合:

区别 主队列 串行队列 并发队列
同步 没有开启新线程,死锁 没有开启新线程,顺序执行任务 没有开启新线程,顺序执行任务
异步 没有开启新线程,顺序执行任务 有开启新线程(1条),顺序执行任务 有开启新线程,并发执行任务

这里我们先给出不同组合的区别,下面详细讲解

基本使用

下面分别介绍下并发队列和串行队列的两种执行方式

并发队列+同步执行

在当前线程中执行任务,不会开启新的线程,执行完一个任务,再执行另一个任务

- (void)syncConcurrent {
    NSLog(@"当前线程:%@", [NSThread currentThread]);
    NSLog(@"并发队列+同步执行---开始");
    
    dispatch_queue_t queue = dispatch_queue_create("sync.concurrent", DISPATCH_QUEUE_CONCURRENT);
    
    dispatch_sync(queue, ^{
        //任务1
        for (int i = 0; i < 2; i++) {
            sleep(2); //模拟耗时操作
            NSLog(@"任务1所在的线程:%@", [NSThread currentThread]);
        }
    });
    
    dispatch_sync(queue, ^{
        //任务2
        for (int i = 0; i < 2; i++) {
            sleep(2); //模拟耗时操作
            NSLog(@"任务2所在的线程:%@", [NSThread currentThread]);
        }
    });
    
    dispatch_sync(queue, ^{
        //任务3
        for (int i = 0; i < 2; i++) {
            sleep(2); //模拟耗时操作
            NSLog(@"任务3所在的线程:%@", [NSThread currentThread]);
        }
    });
    
    NSLog(@"并发队列+同步执行---结束");
}

运行项目,输出结果如下:

可以看到:

  • 所有任务都是在当前线程(主线程)中执行的,没有开启新的线程(同步执行不具备开启新线程的能力)
  • 所有任务的打印都是在 并发队列+同步执行---开始 之后,并发队列+同步执行---结束 之前(同步任务需要等待队列的任务执行结束之后才能执行)
  • 任务是按照先后顺序执行的。虽然并发队列可以开启多个线程,并且同时执行多个任务,但是同步任务不具备开启新线程的能力,所以只有当前线程这一个线程,也就不存在并发。当前线程只有等待当前队列中正在执行的任务执行完毕,才能继续执行下一步操作,所以任务只能一个个按照顺序执行。

并发队列+异步执行

可以开启多个线程,任务交替执行(即我们肉眼看到的同时执行)

- (void)asyncConcurrent {
    NSLog(@"当前线程:%@", [NSThread currentThread]);
    NSLog(@"并发队列+异步执行---开始");
    
    dispatch_queue_t queue = dispatch_queue_create("async.concurrent", DISPATCH_QUEUE_CONCURRENT);
    
    dispatch_async(queue, ^{
        //任务1
        for (int i = 0; i < 2; i++) {
            sleep(2); //模拟耗时操作
            NSLog(@"任务1所在的线程:%@", [NSThread currentThread]);
        }
    });
    
    dispatch_async(queue, ^{
        //任务2
        for (int i = 0; i < 2; i++) {
            sleep(2); //模拟耗时操作
            NSLog(@"任务2所在的线程:%@", [NSThread currentThread]);
        }
    });
    
    dispatch_async(queue, ^{
        //任务3
        for (int i = 0; i < 2; i++) {
            sleep(2); //模拟耗时操作
            NSLog(@"任务3所在的线程:%@", [NSThread currentThread]);
        }
    });
    
    NSLog(@"并发队列+异步执行---结束");
}

运行项目,输出结果如下:

可以看到:

  • 除了当前线程(主线程),系统重新开辟了 3 个线程(异步执行具备开启新线程的能力)
  • 任务是交替执行的(并发队列可开启多个线程,同时 执行多个任务)
  • 所有任务的执行都是在 并发队列+异步执行---开始并发队列+异步执行---结束 之后,说明了当前线程并没有等待,而是直接开启了新的线程,在新的线程中执行任务(异步执行不做等待,可以继续执行任务)

串行队列+异步执行

具备开启新线程的能力,但是因为任务是在串行队列中执行,当前任务执行完毕,才会执行下一个任务

- (void)asyncSerial {
    NSLog(@"当前线程:%@", [NSThread currentThread]);
    NSLog(@"串行队列+异步执行---开始");
    
    dispatch_queue_t queue = dispatch_queue_create("async.serial", DISPATCH_QUEUE_SERIAL);
    
    dispatch_async(queue, ^{
        //任务1
        for (int i = 0; i < 2; i++) {
            sleep(2); //模拟耗时操作
            NSLog(@"任务1所在的线程:%@", [NSThread currentThread]);
        }
    });
    
    dispatch_async(queue, ^{
        //任务2
        for (int i = 0; i < 2; i++) {
            sleep(2); //模拟耗时操作
            NSLog(@"任务2所在的线程:%@", [NSThread currentThread]);
        }
    });
    
    dispatch_async(queue, ^{
        //任务3
        for (int i = 0; i < 2; i++) {
            sleep(2); //模拟耗时操作
            NSLog(@"任务3所在的线程:%@", [NSThread currentThread]);
        }
    });
    
    NSLog(@"串行队列+异步执行---结束");
}

运行项目,输出结果如下:

可以看到:

  • 开启了新的线程(异步执行具备开启新线程的能力,串行队列只开启一个新线程)
  • 所有任务都是在 串行队列+异步执行---开始串行队列+异步执行---结束 之后开始执行的(异步执行不做任何等待,可以继续执行)
  • 任务是按照先后顺序执行的(串行队列每次只有一个任务被执行,任务一个接一个顺序执行)

串行队列+同步执行

不会开启新的线程,任务会顺序执行的。

- (void)syncSerial {
    NSLog(@"当前线程:%@", [NSThread currentThread]);
    NSLog(@"串行队列+同步执行---开始");
    
    dispatch_queue_t queue = dispatch_queue_create("sync.serial", DISPATCH_QUEUE_SERIAL);
    
    dispatch_sync(queue, ^{
        //任务1
        for (int i = 0; i < 2; i++) {
            sleep(2); //模拟耗时操作
            NSLog(@"任务1所在的线程:%@", [NSThread currentThread]);
        }
    });
    
    dispatch_sync(queue, ^{
        //任务2
        for (int i = 0; i < 2; i++) {
            sleep(2); //模拟耗时操作
            NSLog(@"任务2所在的线程:%@", [NSThread currentThread]);
        }
    });
    
    dispatch_sync(queue, ^{
        //任务3
        for (int i = 0; i < 2; i++) {
            sleep(2); //模拟耗时操作
            NSLog(@"任务3所在的线程:%@", [NSThread currentThread]);
        }
    });
    
    NSLog(@"串行队列+同步执行---结束");
}

运行项目,输出结果如下:

可以看到:

  • 所有任务都是在当前线程(主线程)中执行的,没有开启新的线程(同步执行不具备开启新线程的能力)
  • 所有任务都是在 串行队列+同步执行---开始 之后, 串行队列+同步执行---结束 之前(同步任务需要等待队列的任务执行结束之后才能执行)
  • 任务是按照顺序先后执行的(串行队列每次只有一个任务被执行,执行完一个任务,才会执行下一步操作)

主队列

GCD 自带的一种特殊的队列:

  • 所有放在主队列中的任务,都会放到主线程中执行
  • 获取: dispatch_get_main_queue()

主队列也有两种组合方式

主队列+异步执行

只在主线程中执行任务,执行完一个任务,再执行下一个任务

- (void)asyncMain {
    NSLog(@"当前线程:%@", [NSThread currentThread]);
    NSLog(@"主队列+异步执行---开始");
    
    dispatch_queue_t queue = dispatch_get_main_queue();
    
    dispatch_async(queue, ^{
        //任务1
        for (int i = 0; i < 2; i++) {
            sleep(2); //模拟耗时操作
            NSLog(@"任务1所在的线程:%@", [NSThread currentThread]);
        }
    });
    
    dispatch_async(queue, ^{
        //任务2
        for (int i = 0; i < 2; i++) {
            sleep(2); //模拟耗时操作
            NSLog(@"任务2所在的线程:%@", [NSThread currentThread]);
        }
    });
    
    dispatch_async(queue, ^{
        //任务3
        for (int i = 0; i < 2; i++) {
            sleep(2); //模拟耗时操作
            NSLog(@"任务3所在的线程:%@", [NSThread currentThread]);
        }
    });
    
    NSLog(@"主队列+异步执行---结束");
}

运行项目,输出结果如下:

可以看到:

  • 所有任务都是在主线程中执行的,并没有开启新线程(异步执行具备开启新线程的能力,但因为在主队列,所有任务都是在主线程中)
  • 所有任务都是在 主队列+异步执行---开始主队列+异步执行---结束 之后执行(异步执行不做任何等待,可以继续执行任务)
  • 任务是按照顺序执行的(主队列是串行队列,每次只有一个任务被执行,一个任务执行完毕,才会执行下一个任务)
主队列+同步执行

同步执行 + 主队列在不同线程中调用结果也是不一样,在主线程中调用会出现死锁,而在其他线程中则不会。

  1. 在主线程中调用 主队列+同步执行

互相等待(死锁)

可以看到:

程序崩溃了,因为我们在主线程中执行 syncMain 方法,相当于把 syncMain 放到了主线程的队列中。同步执行会等待当前队列中的任务执行完毕,才会继续执行。当我们把任务追加到主队列中,任务 1 就在等待主线程处理完 syncMain 任务,而 syncMain 任务需要等待任务 1 执行完毕,才能继续执行。两个任务相互等待,卡住(死锁)后续都执行不了

  1. 在其他线程中调用 主队列+同步执行

不会开启新线程,执行完一个任务,继续执行下一个任务

// 使用 NSThread 的 detachNewThreadSelector 方法会创建线程,并自动启动线程执行
[NSThread detachNewThreadSelector:@selector(syncMain) toTarget:self withObject:nil];

- (void)syncMain {
    NSLog(@"当前线程:%@", [NSThread currentThread]);
    NSLog(@"在其他线程中主队列+同步执行---开始");
    
    dispatch_queue_t queue = dispatch_get_main_queue();
    
    dispatch_sync(queue, ^{
        //任务1
        for (int i = 0; i < 2; i++) {
            sleep(2); //模拟耗时操作
            NSLog(@"任务1所在的线程:%@", [NSThread currentThread]);
        }
    });
    
    dispatch_sync(queue, ^{
        //任务2
        for (int i = 0; i < 2; i++) {
            sleep(2); //模拟耗时操作
            NSLog(@"任务2所在的线程:%@", [NSThread currentThread]);
        }
    });
    
    dispatch_sync(queue, ^{
        //任务3
        for (int i = 0; i < 2; i++) {
            sleep(2); //模拟耗时操作
            NSLog(@"任务3所在的线程:%@", [NSThread currentThread]);
        }
    });
    
    NSLog(@"在其他线程中主队列+同步执行---结束");
}

运行项目,输出结果如下:

可以看到:

  • 所有任务都是在主线程(非当前线程)中执行的,没有开启新的线程(所有放在主队列中的任务,都会放到主线程中执行)
  • 所有任务都是在 在其他线程中主队列+同步执行---开始 之后,在其他线程中主队列+同步执行---结束之前执行(同步任务需要等待队列的任务执行结束)
  • 任务是按顺序执行的(主队列是串行队列,每次只有一个任务被执行,任务一个接一个按顺序执行)

syncMain 任务放到了其他线程中执行,当任务1、任务2、任务3追加到主队列中,这三个任务都会在主线程中执行。syncMain 任务在其他线程中执行到追加任务1到主队列中,因为主队列现在没有正在执行的任务,所以,会直接执行主队列的任务1,等任务1执行完毕,再接着执行任务2、任务3。所以这里不会卡住线程

相关文章

网友评论

    本文标题:iOS GCD之任务与队列

    本文链接:https://www.haomeiwen.com/subject/zyotbktx.html