欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

多线程之GCD的简单使用

程序员文章站 2024-03-24 22:31:40
...

  GCD为Grand Central Dispatch的缩写,是Apple开发的一个多核编程的较新的解决方法。它主要用于优化应用程序以支持多核处理器以及其他对称多处理系统,是一个在线程池模式的基础上执行的并行任务。

  GCD是一个替代诸如NSThread等技术的很高效和强大的技术。GCD完全可以处理诸如数据锁定和资源泄漏等复杂的异步编程问题。GCD的工作原理是让一个程序,根据可用的处理资源,安排他们在任何可用的处理器核心上平行排队执行特定的任务。这个任务可以是一个功能或者一个程序段。下面,我们就通过一些实例来演示一下GCD的相关知识。

一、GCD的基本概念

  
  在正式开始编写代码之前,我们先来简单了解一下和GCD相关的两个概念:任务队列。任务是指要执行什么操作;队列是用来存放要执行的任务。也就是说,要使用GCD,首先要指定任务,确定自己想做什么,然后再将任务添加到队列中。一旦这两项工作准备就绪,GCD会自动将队列中的任务取出,将其放到对应的线程中去执行。并且,任务的取出遵循队列的FIFO原则,即先进先出,后进后出。

  GCD中有两个用来执行任务常用的函数:dispatch_sync(dispatch_queue_t queue, dispatch_block_t block)和dispatch_async(dispatch_queue_t queue, dispatch_block_t block)。其中,参数queue表示队列,参数block表示要执行的任务(即将要执行的任务放到block代码块中)。前一个函数表示用同步的方式去执行任务,后面一个函数表示用异步的方式去执行任务。同步执行和异步执行的区别在于:同步执行时,任务只能在当前线程中执行,不具备开启新线程的能力;而异步执行可以开启新线程,并且是将任务放在新开的线程中去执行

  关于队列需要补充一点,在GCD中,队列可分为并发队列(Concurrent Dispatch Queue)和串行队列(Serial Dispatch Queue)。其中,并发队列可以让多个任务并发的执行,即自动开启多个线程同时执行任务,它只在异步函数下才有效。而串行队列只能让多个任务一个接一个的去执行。

  关于上面提到的同步函数异步函数,以及并发队列串行队列,它们之间的联系和区别总结如下:

  1、同步函数和异步函数的主要区别在于:能不能开启新的线程。其中,同步函数只能在当前线程中执行任务,不具备开启新线程的能力;而异步函数可以在新的线程中执行任务,具备开启新线程的能力;
  2、并发队列和串行队列的主要区别在于:任务的执行方式。并发队列允许多个任务并发的执行,并且它只能在异步函数中有效;而在串行队列中,一个任务执行完毕后,下一个任务才会被执行。

二、GCD的基本使用

  
  新建一个工程,在ViewController中实现- touchesBegan: withEvent:方法,然后再来看一下异步函数和并发队列、异步函数和串行队列,以及同步函数和并发队列、同步函数和串行队列这几种组合情况下程序运行的效果。

  GCD的使用步骤是:先创建一个队列,然后再定制任务,并将这个任务添加到队列中去。创建队列时,使用dispatch_queue_create( )函数,它的返回值是一个dispatch_queue_t类型。dispatch_queue_create( )函数有两个参数,第一个参数是一个C语言字符串,它是用来标识不同的队列,可以理解为给队列取名字,可以为空;第二个参数是一个宏,用来标识所创建这个队列的属性,其取值一般为DISPATCH_QUEUE_CONCURRENT(并发队列),或者DISPATCH_QUEUE_SERIAL(串行队列)。队列创建完毕之后,根据任务需要来决定使用dispatch_async( )函数或者dispatch_sync( )函数。前面一个函数表示异步函数,后面一个函数表示同步函数。

  1、异步函数和并发队列

  先来看一下异步函数和并发队列的使用情况。首先使用dispatch_queue_create( )函数创建一个并发队列,然后再用dispatch_async( )函数来封装任务,为了便于对比查看,我们在一个队列中添加了3个任务,并且在适当的位置加入了打印信息:

// MARK:- 点击屏幕执行代码
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {

    // 点击屏幕以后执行异步函数和并发队列
    [self asyncFuncAndConcurrentQ];
}

// MARK:- 异步函数和并发队列
- (void)asyncFuncAndConcurrentQ {

    // 创建一个并发队列
    dispatch_queue_t queue = dispatch_queue_create("lable", DISPATCH_QUEUE_CONCURRENT);
    /**
     *  dispatch_queue_create(<#const char * _Nullable label#>, <#dispatch_queue_attr_t  _Nullable attr#>)
     *  第一个参数 : 它是一个C语言的字符串儿,可以为空。它是一个标签,用来区分不同的队列;
     *  第二个参数 : 它是用来描述队列类型的信息,一般为DISPATCH_QUEUE_CONCURRENT(并发队列)或者DISPATCH_QUEUE_SERIAL(串行队列)。
     */

    NSLog(@"start------%s", __func__);

    // 定制任务,并将其添加到队列中(一个队列中可以添加多个任务)
    dispatch_async(queue, ^{

        // 要执行的任务
        NSLog(@"线程1:%@", [NSThread currentThread]);
    });
    /**
     *  dispatch_async(<#dispatch_queue_t  _Nonnull queue#>, <#^(void)block#>)
     *  第一个参数 : 用于接收队列,也就是要把上面创建的队列传进来;
     *  第二个参数 : 它是一段block代码,用来封装你要执行的代码。
     */

    dispatch_async(queue, ^{

        // 要执行的任务
        NSLog(@"线程2:%@", [NSThread currentThread]);
    });

    dispatch_async(queue, ^{

        // 要执行的任务
        NSLog(@"线程3:%@", [NSThread currentThread]);
    });

    NSLog(@"end------%s", __func__);
}

  运行程序,然后点击模拟器,注意观察控制台输出情况:

多线程之GCD的简单使用
异步函数和并发队列的执行情况.png

  上面的打印信息共分为两部分:第一部分是用于标识代码执行顺序的start和end,另一部分是反映多任务的执行顺序(是并发执行还是串行执行),以及是否开子线程,如果开子线程,会开几条。先来看输出的第一部分,从代码执行的顺序和输出结果来看,它似乎是先执行start输出那一行,然后连续跳过三个dispatch_async( )函数,接着执行end输出那一行,最后再执行这三个dispatch_async( )函数中的任务。其实,这是并发执行造成的假象。在并发执行的过程中,一般情况下,耗时比较长的代码会后结束;再看输出的第二部分,说明在异步函数和并发队列中,多个任务是并发执行的,并且系统为它们开启了多条子线程。

  2、异步函数和串行队列

  接着来看一下异步函数和串行队列的使用情况。除了创建队列时使用的是串行队列之外,代码的其它部分与上面异步函数和并发队列一样:

// MARK:- 点击屏幕执行代码
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {

    // 点击屏幕执行异步函数和串行队列
    [self asyncFuncAndSerialQ];
}

// MARK:- 异步函数和串行队列
- (void)asyncFuncAndSerialQ {

    // 创建一个串行队列
    dispatch_queue_t queue = dispatch_queue_create("lable", DISPATCH_QUEUE_SERIAL);

    NSLog(@"start------%s", __func__);

    // 使用异步函数封装任务
    dispatch_async(queue, ^{

        // 要执行的任务
        NSLog(@"线程1:%@", [NSThread currentThread]);
    });

    dispatch_async(queue, ^{

        // 要执行的任务
        NSLog(@"线程2:%@", [NSThread currentThread]);
    });

    dispatch_async(queue, ^{

        // 要执行的任务
        NSLog(@"线程3:%@", [NSThread currentThread]);
    });

    NSLog(@"end------%s", __func__);
}

  运行程序,然后点击屏幕,仔细看一下控制台打印出来的信息:

多线程之GCD的简单使用
异步函数和串行队列的执行情况.png

  从代码执行的顺序和输出的结果来看,似乎也是先执行start和end,最后再回过头去执行三个dispatch_async( )函数中的任务。另外,这里的异步函数肯定也会开子线程,不过,它只会开启一条。原因是,尽管有多个任务,但是所有的任务都是添加到串行队列中去的,它们只会串行执行,因此,只需开一条子线程就够了。

  3、同步函数和并发队列

  再来看一下同步函数和并发队列的执行情况。创建队列时使用的是并发队列,只是在封装任务的时候,它使用的是同步函数:

// MARK:- 点击屏幕执行代码
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {

    // 点击屏幕执行同步函数和并发队列
    [self syncFuncAndConcurrentQ];
}

// MARK:- 同步函数和并发队列
- (void)syncFuncAndConcurrentQ {

    // 创建一个并发队列
    dispatch_queue_t queue = dispatch_queue_create("lable", DISPATCH_QUEUE_CONCURRENT);

    NSLog(@"start------%s", __func__);

    // 使用同步函数封装任务
    dispatch_sync(queue, ^{

        // 要执行的任务
        NSLog(@"线程1:%@", [NSThread currentThread]);
    });

    dispatch_sync(queue, ^{

        // 要执行的任务
        NSLog(@"线程2:%@", [NSThread currentThread]);
    });

    dispatch_sync(queue, ^{

        // 要执行的任务
        NSLog(@"线程3:%@", [NSThread currentThread]);
    });

    NSLog(@"end------%s", __func__);
}

  运行程序,然后点击屏幕,看一下控制台输出的情况:

多线程之GCD的简单使用
同步函数和并发队列的执行情况.png

  首先,从代码执行的顺序和输出情况来看,所有的代码都是自上而下,按顺序执行的;其次,同步函数并不会开子线程,所以,即便是有多个任务的并发队列,它也只能串行执行。因为它有且仅有一条线程,而一个线程在同一时间内只能执行一个任务。

  4、同步函数和串行队列

  最后再来看一下同步函数和串行队列的执行情况。这里,除了创建队列时使用的是串行队列以外,其它代码和同步函数并发队列中是一样的:

// MARK:- 点击屏幕执行代码
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {

    // 点击屏幕执行同步函数和串行队列
    [self syncFuncAndSerialQ];
}

// MARK:- 同步函数和串行队列
- (void)syncFuncAndSerialQ {

    // 创建一个串行队列
    dispatch_queue_t queue = dispatch_queue_create("lable", DISPATCH_QUEUE_SERIAL);

    NSLog(@"start------%s", __func__);

    // 使用同步函数封装任务
    dispatch_sync(queue, ^{

        // 要执行的任务
        NSLog(@"线程1:%@", [NSThread currentThread]);
    });

    dispatch_sync(queue, ^{

        // 要执行的任务
        NSLog(@"线程2:%@", [NSThread currentThread]);
    });

    dispatch_sync(queue, ^{

        // 要执行的任务
        NSLog(@"线程3:%@", [NSThread currentThread]);
    });

    NSLog(@"end------%s", __func__);
}

  运行程序,然后点击屏幕,好好看一下控制台输出情况:

多线程之GCD的简单使用
同步函数和串行队列的执行情况.png

  从代码执行顺序和控制台输出情况,以及任务执行顺序和是否开线程来看,其结果与同步函数和并发队列的运行一模一样。这主要是因为,同步函数不会开启子线程,再加上它本身就是串行队列,所以不管有多少任务,它都是在主线程中串行执行的。这也间接说明,并发队列在同步函数中是没什么卵用的,其结果和串行队列没有太大区别。

三、GCD中两个特殊的队列

  
  在GCD中,除了上面讲到的并发队列和串行队列之外,还有两个特殊队列——全局并发队列主队列。这两个队列都是系统提供的,不需要自己去创建,只需要通过特定的函数来获取就可以了。下面,我们就简单的介绍一下,这两个队列如何使用。

  1、全局并发队列

  就以异步函数为例,先通过dispatch_get_global_queue( )函数来获取一个全局并发队列,然后封装任务的方式和- asyncFuncAndConcurrentQ方法一样:

// MARK:- 点击屏幕执行代码
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {

    // 点击屏幕以后执行异步函数和并发队列
    [self asyncFuncAndConcurrentQ];
}

// MARK:- 异步函数和并发队列
- (void)asyncFuncAndConcurrentQ {

    // 获取GCD中已经存在的全局队列
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    /**
     *  dispatch_get_global_queue(<#long identifier#>, <#unsigned long flags#>)
     *  第一个参数 : 它表示队列的优先级,有四种取值:DISPATCH_QUEUE_PRIORITY_BACKGROUND < DISPATCH_QUEUE_PRIORITY_LOW < DISPATCH_QUEUE_PRIORITY_DEFAULT < DISPATCH_QUEUE_PRIORITY_HIGH;
     *  第二个参数 : 它表示预留给将来使用的标识,通常情况下传一个0就可以了。
     */

    NSLog(@"start------%s", __func__);

    // 使用异步函数封装任务
    dispatch_async(queue, ^{

        // 要执行的任务
        NSLog(@"线程1:%@", [NSThread currentThread]);
    });

    dispatch_async(queue, ^{

        // 要执行的任务
        NSLog(@"线程2:%@", [NSThread currentThread]);
    });

    dispatch_async(queue, ^{

        // 要执行的任务
        NSLog(@"线程3:%@", [NSThread currentThread]);
    });

    NSLog(@"end------%s", __func__);
}

  获取全局并发队列的函数也有两个参数,第一个参数用来表示这个队列的优先级,其取值有四种,优先级从低到高为:DISPATCH_QUEUE_PRIORITY_BACKGROUND < DISPATCH_QUEUE_PRIORITY_LOW < DISPATCH_QUEUE_PRIORITY_DEFAULT < DISPATCH_QUEUE_PRIORITY_HIGH;第二个参数表示是用来预留给未来使用的标识,通常传0就可以了。运行程序,看一下控制台输出情况:

多线程之GCD的简单使用
异步函数和全局并发队列的执行情况.png

  虽然是全局并发队列,但是,它的功能和我们自己创建的并发队列其实没有差别。因此,在异步函数中,它的执行效果与上面异步函数和并发队列一样。

  需要特别注意的是,在异步函数和并发队列中,如果有多个任务,系统肯定会开启多条子线程。但是,这并不意味着有N(N > 1)个任务,系统就会开N(N > 1)条子线程。究竟会开多少条线程,这个是不可控的,完全取决于系统的心情。通常情况下,如果有N(N > 1)个任务,系统一般会开启n(1 < n < N)条子线程。

  2、异步函数和主队列

  主队列是GCD自带的一种特殊的串行队列,通常情况下,放在主队列中的任务,最后都会被放到主线程中去执行。使用dispatch_get_main_queue( )函数就可以获得主队列。下面就来看一下异步函数和主队列的使用:

// MARK:- 点击屏幕执行代码
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {

    // 点击屏幕执行异步函数和主队列
    [self asyncFuncAndMainQ];
}

// MARK:- 异步函数和主队列
- (void)asyncFuncAndMainQ {

    // 获取主队列
    dispatch_queue_t queue = dispatch_get_main_queue();

    NSLog(@"start------%s", __func__);

    // 使用异步函数封装任务
    dispatch_async(queue, ^{

        // 要执行的任务
        NSLog(@"线程1:%@", [NSThread currentThread]);
    });

    dispatch_async(queue, ^{

        // 要执行的任务
        NSLog(@"线程2:%@", [NSThread currentThread]);
    });

    dispatch_async(queue, ^{

        // 要执行的任务
        NSLog(@"线程3:%@", [NSThread currentThread]);
    });

    NSLog(@"end------%s", __func__);
}

  运行程序,然后点击屏幕,仔细看一下控制台输出的情况:

多线程之GCD的简单使用
异步函数和主队列的执行情况.png

  从代码执行的顺序来看,它似乎也是先执行start那一行,然后连续跳过三个dispatch_async( )函数,接着执行end那一行,最后再顺序的去执行那三个dispatch_async( )函数。只不过,它没有开线程。

  通常情况下,异步函数是会开线程的,但是,这里是个例外。我们在前面说过,放在主队列里面的任务最后都会被放到主线程里去执行,因为已经有了可以执行任务的线程,因此就没必要再开线程了。也就是说,异步函数和主队列这种组合,不管你有多少任务,它都是不会再开线程的!这是一个特例,一定要记住。

  3、同步函数和主队列

  接下来再看一下同步函数和主队列。我们先按照上面的执行步骤,在获取主队列以后,再将任务封装到同步函数中。按照之前的方式,当我们点击屏幕以后,直接调用- syncFuncAndMainQ方法,看看能不能行:

// MARK:- 点击屏幕执行代码
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {

    // 点击屏幕执行同步函数和主队列
    [self syncFuncAndMainQ];
}

// MARK:- 同步函数和主队列
- (void)syncFuncAndMainQ {

    // 获取主队列
    dispatch_queue_t queue = dispatch_get_main_queue();

    NSLog(@"start------%s", __func__);

    // 使用同步函数封装任务
    dispatch_sync(queue, ^{

        // 要执行的任务
        NSLog(@"线程1:%@", [NSThread currentThread]);
    });

    dispatch_sync(queue, ^{

        // 要执行的任务
        NSLog(@"线程2:%@", [NSThread currentThread]);
    });

    dispatch_sync(queue, ^{

        // 要执行的任务
        NSLog(@"线程3:%@", [NSThread currentThread]);
    });

    NSLog(@"end------%s", __func__);
}

  运行程序,当我们点击屏幕,你会发现,编译器直接报错。原因是,产生了死锁:

多线程之GCD的简单使用
在主线程中调用同步函数和主队列会产生死锁.png

  下面来分析一下,为什么会出现死锁。首先,- touchesBegan: withEvent:方法是在主线程中执行的,当我们调用- syncFuncAndMainQ方法以后,第一步是通过dispatch_get_main_queue( )函数获得主队列;程序接着往下执行时,会调用第一个dispatch_sync( )函数,这个函数会把它block代码块中封装的任务装到主队列中去,然后主队列再把这个任务交给主线程去执行。现在问题来了,此时主线程没空!因为,此时主线程正在执行第一个dispatch_sync( )函数,没空去执行这个block代码块中封装的任务。但是,如果不执行这个block代码块中的任务,程序就无法接着往下走了,所以就产生了死锁。

  同步函数的一大特点就是,必须等一个任务执行完毕以后,它才会接着执行下一个任务。这期间,如果有一个任务的执行出现问题,那么后面所有的任务都不可能执行。而主队列的特点是,如果发现主线程有任务正在执行,那么它就会暂停队列中任务的调度,直到主线程出现空闲为止。在上面的代码中,主线程要在- syncFuncAndMainQ方法执行完毕以后才会出现空闲,而这显然是不可能的,它会一直卡在第一个dispatch_sync( )函数的NSLog那一行。

  要解决上面这个问题,就不能在主线程中调用- syncFuncAndMainQ方法。为此,我们可以在- touchesBegan: withEvent:方法中新开一条子线程,然后再在子线程中调用- syncFuncAndMainQ方法:

// MARK:- 点击屏幕执行代码
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {

    // 点击屏幕执行同步函数和主队列
//    [self syncFuncAndMainQ];  // 在主线程中调用- syncFuncAndMainQ方法会出现死锁
    // 开一条子线程,然后在子线程中调用- syncFuncAndMainQ方法
    [NSThread detachNewThreadSelector:@selector(syncFuncAndMainQ) toTarget:self withObject:nil];
}

// MARK:- 同步函数和主队列
- (void)syncFuncAndMainQ {

    // 获取主队列
    dispatch_queue_t queue = dispatch_get_main_queue();

    NSLog(@"start------%s", __func__);

    // 使用同步函数封装任务
    dispatch_sync(queue, ^{

        // 要执行的任务
        NSLog(@"线程1:%@", [NSThread currentThread]);
    });

    dispatch_sync(queue, ^{

        // 要执行的任务
        NSLog(@"线程2:%@", [NSThread currentThread]);
    });

    dispatch_sync(queue, ^{

        // 要执行的任务
        NSLog(@"线程3:%@", [NSThread currentThread]);
    });

    NSLog(@"end------%s", __func__);
}

  运行程序,点击屏幕以后,熟悉的界面又出来了:

多线程之GCD的简单使用
在子线程中调用同步函数和主队列的执行情况.png

  它的执行效果与同步函数和串行队列,以及同步函数和并发队列是一样的。通过上面的示例说明,如果你在同步函数中使用了主队列,那么调用它的时候,千万不要把它放在主线程中去执行,否则会产生死锁。正确的做法是,先开一条子线程,然后再在这个子线程中去执行同步函数中主队列里面的任务。

四、GCD中同步函数和异步函数的总结

  
  通过上面的代码示例,我们可以做一个简单的总结:只有在异步函数中,并且队列不是主队列时,系统才会开启子线程。详细情况参见下表:

多线程之GCD的简单使用
GCD中各种队列的执行效果.png

五、GCD中线程间的通信

  
  我们在上一篇笔记《NSThread线程间的通信》中,通过下载网络图片的方式来演示了如何在NSThread线程间进行通信,下面,我们也通过这样一个例子来演示如何在GCD的线程间进行通信:

// MARK:- 点击屏幕执行代码
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {

    // 开启子线程来下载图片(因为只有一个任务,所以用串行队列或者并发队列都可以,这里用全局并发队列)
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

        // 确定URL地址
        NSURL *url = [NSURL URLWithString:@"https://gss0.baidu.com/-fo3dSag_xI4khGko9WTAnF6hhy/zhidao/pic/item/0b55b319ebc4b745359202e2c8fc1e178a82153b.jpg"];

        // 从网络上下载图片的二进制数据到本地
        NSData *imageData = [NSData dataWithContentsOfURL:url];

        // 将图片的二进制数据转换为图片
        UIImage *image = [UIImage imageWithData:imageData];

        // 打印当前线程
        NSLog(@"下载图片的线程为:%@", [NSThread currentThread]);

        // 回到主线程中设置图片,并且刷新UI界面
        dispatch_async(dispatch_get_main_queue(), ^{  // 这里用同步函数还是异步函数都无所谓,不会发生死锁,因为我们在前面开启了子线程

            // 将图片设置到UIImageView控件上去
            self.imageView.image = image;

            // 打印当前线程
            NSLog(@"设置图片并刷新UI的线程为:%@", [NSThread currentThread]);

        });
    });
}

  运行程序,然后点击屏幕,看看网络图片有没有下载下来:

多线程之GCD的简单使用
在子线程中下载网络数据,在主线程中刷新UI.gif

  在GCD中,所有的函数都是可以嵌套的,因此,要回到主线程就非常简单,只需要再嵌套一个同步函数或者异步函数,然后再将主队列传进去就可以了。需要说明的是,因为我们下载网络数据是放在子线程中去执行的,所以,即便是这里使用同步函数和主队列,并不会产生死锁。

  GCD的基本知识暂时先讲到这里,后面的篇幅中会接着扩展GCD的其它知识。详细代码参见GCDExercise