美文网首页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