iOS 多线程之 GCD 的介绍

iOS 多线程之 GCD 的简单介绍

引言

这篇文章主要是为大家简单的讲解一下多线程的基础理论知识,然后会着重同大家学习GCD的使用。

多线程的知识

进程线程的了解

学过操作系统的同学,都知道进程和线程的知识,这里就简单的说一下。
- 什么是进程
进程是指可以并发执行的程序在某个数据集合上的运行过程,是系统进行资源分配和调度的独立单位。说的通俗一点,进程是指在系统中正在运行的一个应用程序。
- 什么是线程
线程是操作系统进程中能够并发执行的实体,是处理器调度和分派的基本单位。
一个进程想要执行任务,必须得有线程(每个进程至少有一条线程)
- 线程的串行
1个线程中,任务的执行是串行的。如果要在一个线程中执行多个任务,那么只能一个一个的按顺序执行这些任务。也就是说,在同一个时间内,1个线程只能执行1个任务。

多线程

一个进程中可以开启多条线程,每条线程可以并行(同时)执行不同的任务。多线程技术可以提高程序的执行效率。

原理

同一个时间,CPU 只能处理一条线程,只有一条线程在工作(执行)。多线程并发执行,其实就是 CPU 快速的在多个线程之间调度(切换)。如果CPU调度线程的时间足够快,就会造成了多线程并发执行的假象。
在具有多个 CPU 核的情况下,就不是看起来像,而是真的提供了多个 CPU 核并行执行多个线程的技术。
为什么,有人说多线程会造成程序执行效率降低,消耗时间呢?
如果线程非常多的话,CPU 会在N条线程之间调度,CPU 会累死,并且消耗大量的CPU资源,每条线程被调度执行的频次会降低(线程的执行效率降低)

GCD 的简单介绍

什么是 GCD

GCD 是异步执行任务的技术之一。一般将应用程序中记述的线程管理用代码在系统级中实现。开发者可以定义想执行的任务并追加到适当的 Dispatch Queue 中,GCD 就能生成必要的线程并计划执行任务。由于线程管理是作为系统的一部分来实现的,因此可以统一管理,也可执行任务,这样,就比以前的线程更有效率。GCD是纯 C语言,并且它的大多数函数是以 dispatch 开始的。

任务和队列

GCD 主要就是由 队列 和 任务 两部分来实现的。多线程执行过程就是把任务放到队列中去执行的过程。将任务添加到队列中,GCD会自动将队列中的任务取出,放到对应的线程中执行。
提示:任务的取出遵循队列中的 FIFO原则:先进先出,后进后出。

任务

所谓的任务,就是指你要执行的操作,主要分为:同步任务和异步任务

同步和异步的主要区别在于会不会阻塞当前线程,直到 Block 中的任务执行完毕。如果是同步(sync)操作,它会阻塞当前线程并等待 Block 中的任务执行完毕,然后线程才会继续往下运行;如果是异步(async)操作,当前线程会直接往下执行,它不会阻塞当前线程,而是选择新开辟一个线程。
- 同步任务:用同步的方式执行任务 dispatch_sync(dispatch_queue_t queue, dispatcah_bolck_t block);
参数说明: queue:队列 block:任务(把右边的参数放入左边的参数进行执行)
- 异步任务:用异步的方式执行任务 dispatch_async(disaptch_queue_t queue, dispatch_block_t block);

#### 队列

队列,就是用来存放的任务的。主要分为串行队列、并行队列并发、串行决定了任务的执行方式

  • 并发:任务以 FIFO 从序列中移除,然后并发执行,可以按照任何顺序完成。它会自动开启多个线程同时执行任务。

  • 串行:任务以 FIFO 从序列中一个一个执行。一次只调度一个任务,队列中的任务一个接着一个地执行(一个任务执行完毕后,再去执行下一个任务)而且只会开启一条线程。

并行队列(Concurrent Dispatch Queue):在队列中的多个任务(线程)同时执行(不按顺序执行),并发只有在异步函数下才有效。

串行队列(Serial Dispatch Queue):在队列中的多个任务(线程)排队依次执行(按顺序执行)

获取队列

了解完这些知识点,让我们通过 GCD 的 API ,结合代码来了解上面说的这些知识点。
下面,先展示一下使用 GCD 的例子:

dispatch_queue_t concurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(concurrentQueue, ^{ // 这一行代码表示让处理在后台线程中执行。

        /*
        *  长时间耗时操作
        *  如请求下载网络数据
        */
    dispatch_async(dispatch_get_main_queue(), ^{  // 这一行代码,表示让处理返回到主线程
        /*
        * 只在主线程进行的处理
        * 如界面的刷新
        */
    });
});

以上代码就是在后台线程中执行长时间处理,当处理结束后,主线程使用该处理结果的源代码。
补充:
在 GCD 导入之前,我们是使用 Cocoa 框架提供的 NSObject 类performSelectorInBackground:withObject
的实例方法,和 performSelectorOnMainThread 实例方法等简单的多线程编程技术可以实现如上功能。

如何获取并行队列?

好了,我们继续看 GCD 的这个例子,首先它通过这行代码

dispatch_queue_t concurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

获取了一个并行的全局队列。因为 GCD 默认提供了全局的并发队列,供整个应用来使用,不需要我们手动创建。使用 dispatch_get_global_queue 函数来获得全局的并发队列。该函数

dispatch_queue_t dispatch_get_global_queue(dispatch_queue_priority_t priority,unsigned long flags);

其中,dispatch_queue_priority_t priority 表示队列的优先级;它共有四种类型,分别是

类型优先级说明
DISPATCH_QUEUE_PRIORITY_HIGH2
DISPATCH_QUEUE_PRIORITY_DEFAULT0默认
DISPATCH_QUEUE_PRIORITY_LOW-2
DISPATCH_QUEUE_PRIORITY_BACKGROUNDINT16_MIN后台

如何来获取串行队列呢?

GCD 中获得串行有两种途径。

  • 使用 dispatch_queue_create 函数创建串行队列

    该函数dispatch_queue_t dispatch_queue_create(const char *label, dispatch_queue_attr_t attr);,其中 dispatch_queue_attr_t 表示队列属性:
    它有两种属性,一种是等待现在执行中处理的 Serial Dispatch Queue ,另一种是不等待现在执行中的 Concurrent Dispatch queue. 默认为Serial Dispatch Queue

  • 使用主队列(跟主线程相关的队列)

    主队列是 GCD 自带的一种特殊的串行队列,放在主队列中的任务,都会放在主线程中执行。我们通过使用 dispatch_get_main_queue( ) 获得主队列。

各种队列的执行效果

任务方式全局并发队列串行队列主队列
同步(sync)没有开启新线程;串行执行任务没有开启新线程,串行执行任务会造成死锁
异步(async)有开启新线程;并发执行任务有开启新线程,串行执行任务没有开启新线程,串行执行任务

GCD 常用的 API

在这之前,我们也已经写了 dispatch 相关的 API ,那么现在就让我们具体的了解一下这些 API

Dispatch Queue

“Dispatch Queue” 是什么呢?如其名称所示,是执行处理的等待队列。应用程序编程人员通过 dispatch_async 函数等 API,在 Block 语法中记述想要执行的处理并追加到 Dispatch_Queue 中。Dispatch_Queue 按照追加的顺序(FIFO)执行处理。

执行处理时可以并行或串行,必然也就存在两种 Dispatch Queue,也就是上文中讲的两种队列。

Dispatch Queue 的种类说明
Serial Dispatch Queue等待现在执行中处理结束
Concurrent Dispatch Queue不等待现在执行中处理结束

在 Concurrent Dispatch Queue 中执行处理时,执行顺序会根据处理内容和系统状态发生改变。它不同于 Serial Dispatch Queue 执行顺序只能是固定的。

注:
并行执行的处理数量并不是无限制的,它取决于当前系统的状态。即 iOS 和 OS X 基于 Dispatch Queue 中的处理数、CPU 核数以及 CPU 负荷等当前系统的状态来决定 Concurrent Dispatch Queue 中并发执行的处理数。

dispatch_queue_create

通过这个函数,我们可以获得以上介绍的两种队列。以下代码,我们可以获取到一个并行的队列

dispatch_queue_t concurrentQueue =  dispatch_queue_create("concurrentQueue", DISPATCH_QUEUE_CONCURRENT);

该函数有两个参数,第一个参数指定该函数的名称,可以设为 NULL;第二个参数表示队列的类型,如果要生成 Serial Dispatch Queue 时,我们可以将它指定为 NULL 或者 DISPATCH_QUEUE_SERIAL ;如果要设成 Concurrent Queue 时,我们需要把它设为 DISPATCH_QUEUE_CONCURRENT 。

该函数的返回值是表示Dispatch Queue 的“dispatch_queue_t” 类型的。

在我们之前的学习中,在 ARC 下,我们基本上无需考虑释放我们创建的对象。但是在这儿,我们必须要将自己创建的 Dispatch Queue 手动释放。因为 Dispatch Queue 并没有像 Block 那样具有作为 OC 对象来处理的技术,Dispatch Queue 必须通过 dispatch_retain 和 dispatch_release 像 OC 的引用计数一样管理。

我们通过 dispatch_queue_create 函数可生成任意多个 Dispatch Queue。当生成多个 Serial Dispatch Queue 时,各个 Serial Dispatch Queue 将并行执行。我们经常会在多个线程更新相同资源导致数据竞争时使用 Serial Dispatch Queue.

Main Dispatch Queue / Global Dispatch Queue

通过 Main Dispatch Queue / Global Dispatch Queue 这两个 API,我们也可以获取到系统标准的Dispatch Queue.

Main Dispatch Queue

Main Dispatch Queue 是主线程中执行的 Dispatch Queue 。因为主线程只有一个,所有它也是串行队列。

追加到 Main Dispatch Queue 处理的线程在主线程的 RunLoop 中执行。通常我们会把 涉及 UI 操作等必须在主线程中执行的处理追加到 Main Dispatch Queue 中。

Global Dispatch Queue

Global Dispatch Queue 是所有应用程序都能使用的 Concurrent Dispatch Queue。

具体如何获取以及 Global Dispatch Queue 的种类,我们也在获取队列那块儿具体讲了。

这里要提的一点是,通过 Main Dispatch Queue 和 Global Dispatch Queue 执行 dispatch_retain 和 dispatch_release 不会引起任何变化,也不会有什么问题出现。这也是获取并使用 Global Dispatch Queue 比生成、使用、释放 Concurrent Dispatch Queue 更轻松的原因。

dispatch_sync / dispatch_async

相信大家通过上面的阅读,对于 dispatch_sync / dispatch_async 也知道它指的的是什么了吧!接下来,让我们具体的了解一下这两个函数以及所涉及的一些问题。

async 意味着“非同步”,dispatch_async 就是将指定的 Block “非同步”地追加到指定的 Dispatch Queue 中,dispatch_async 不做任何等待。那么,对应的 dispatch_sync 就是将指定的 Block “同步”地追加到指定的 Dispatch Queue 中,dispatch_sync 会一直等待下去。

接下来,我们将以上所学过的函数,通过组合,来理解它们具体的含义以及可能出现的问题

串行队列同步执行任务
  1. 同步不具有创建新的线程的能力, 不会开辟新的线程去执行任务,会在当前的程序的主线程中去执行任务
  2. 按照串行的方式去执行任务
    dispatch_queue_t queue = dispatch_queue_create("chuanxingtongbu",DISPATCH_QUEUE_SERIAL);
    dispatch_sync(queue, ^{
        NSLog(@"%@",[NSThread currentThread]);
    });
并行队列同步执行任务
  1. 同步不具有创建新的线程的能力, 不会开辟新的线程去执行任务,会在当前的程序的主线程中去执行任务
  2. 按照同步的方式去执行任务
    dispatch_queue_t concurrentQueue =  dispatch_queue_create("concurrentQueue", DISPATCH_QUEUE_CONCURRENT);
    dispatch_sync(concurrentQueue, ^{
        NSLog(@"%@",[NSThread currentThread]);
    });
主队列的同步(会造成程序的死锁)

在这儿,大家先知道有这回事儿就好。在后面,我会具体的讲这个的原因

    dispatch_queue_t main =  dispatch_get_main_queue();
    dispatch_sync(main, ^{
        NSLog(@"%@",[NSThread currentThread]);
    });
全局队列的同步

都在主线程上执行,不会死锁,和并行队列同步执行任务一样

    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_sync(queue, ^{
        NSLog(@"A");
    });
主队列的异步(在主线程中顺序执行)

新添加到主队列中的任务会放到队列的最尾部,等到当前主线程中的任务结束之后然后再从队列的头部取出依次执行(FIFO)先进先出

大家可以猜猜,它的执行顺序是什么?

    dispatch_async(dispatch_get_main_queue(), ^{
            NSLog(@"%@",[NSThread currentThread]);
            NSLog(@"=================2");
    });
    NSLog(@"=================3");
串行队列异步执行任务
  1. 异步具有创建新的线程的能力, 会开辟新的线程去执行任务
  2. 按照串行的方式去执行任务
    dispatch_queue_t serialQueue = dispatch_queue_create("com.lai.www", DISPATCH_QUEUE_SERIAL);
    dispatch_async(serialQueue, ^{
         NSLog(@"%@",[NSThread currentThread]);
     });
并发队列异步执行任务(常用)
  1. 异步具有创建新的线程的能力,会开辟新的线程去执行任务,不会在当前的程序的主线程中去执行任务
  2. 按照并发的方式去执行任务
    dispatch_queue_t currentQueue = dispatch_queue_create("com.lai.www", DISPATCH_QUEUE_CONCURRENT);

     dispatch_async(currentQueue, ^{
         NSLog(@"%@",[NSThread currentThread]);
     });
给当前串行队列同步提交任务到当前串行队列

会死锁,我的理解应该是和同步添加主线程队列是一个原因。

  dispatch_queue_t queue = dispatch_queue_create("chuanxingtongbu",DISPATCH_QUEUE_SERIAL);
    dispatch_async(queue, ^{
        NSLog(@"%@",[NSThread currentThread]);
        dispatch_sync(queue, ^{
            NSLog(@"%@",[NSThread currentThread]);
        });
    });

除此之外,给当前队列同步或异步提交任务都不会造成死锁。

异步队列中同步添加主线程

思考下段代码的执行顺序

dispatch_async(dispatch_get_global_queue(0, 0), ^{
        NSLog(@"=================1");
        dispatch_sync(dispatch_get_main_queue(), ^{
            NSLog(@"=================2");
        });
        NSLog(@"=================3");
    });
     NSLog(@"=================4");
//    while (1) {
//
//    }

如果,在打印 4 的后面,加一个 while 死循环,会发生什么?

结果是:主线程会一直执行这个死循环,从而 阻塞了主线程,2 中的sync 就无法执行啦,mainThread 永远不会退出, sync 就永远等待着

到这儿,有关 同步、异步、并行、串行的所有嵌套就基本讲完脸。

Dispatch Group

在追加到 Dispatch Queue 中的多个处理全部结束处理,这种情况时常发生。因此,我们引入了 Dispatch Group 。我们从下面的代码中,看看他到底与我们的 Dispatch Queue 有什么不同。

    NSLog(@"start");
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_group_t group = dispatch_group_create();

    dispatch_group_async(group, queue, ^{
        NSLog(@"A");
    });
    dispatch_group_async(group, queue, ^{
        NSLog(@"B");
    });
    dispatch_group_notify(group, queue, ^{
        NSLog(@"done");
    });

该程序的执行结果是:

2017-10-30 22:53:50.346698+0800 iOS_GCD[2836:1633919] start
2017-10-30 22:53:50.346921+0800 iOS_GCD[2836:1634386] A
2017-10-30 22:53:50.346929+0800 iOS_GCD[2836:1634389] B
2017-10-30 22:53:50.347451+0800 iOS_GCD[2836:1634389] done

向 Global Dispatch Queue 即 Concurrent Dispatch Queue 中追加任务,所以追加处理的执行顺序不定,但是 done 是最后执行的。

无论向什么样的队列中追加任务,都可以使用 Dispatch Group 来监测这些处理的结果,一旦检测到所有的处理执行结束,就可将结束的处理任务,追加到 Dispatch Queue 中。

我们会发现和 dispatch_create 相似的 dispatch_group_create ,只不过它的返回值是 dispatch_group_t 类型的。

而 dispatch_group_async 则和 dispatch_async 相同的是都追加 Block 到指定的 Dispatch Queue 中。不同的是,指定生成的 Dispatch Group 为第一个参数,指定的 Block 属于指定的 Dispatch Group。

同时,和 Dispatch Queue 完全一样的是,它也需要 dispatch_realse 和 dispatch_retain 来手动释放内存。

下面,让我们来了解一下它的两个函数。

  • dispatch_group_wait:当我们调用这个这个函数时,该函数就处于调用的状态不返回,直到等待的时间结束。第二个参数指定等待的时间。当该函数的返回值不为 0 时,表示 group 内的某个处理任务还未完成;当等待时间为 DISPATCH_TIME_FOREVER 时,返回值恒为0
  • dispatch_group_notify: 使用该函数,我们可以将执行的 Block 追加到 Dispatch Queue 中,将第一个参数指定为要监视的 Dispatch Group,在追加到该 group 的全部处理结束后,将第三个参数的 Block 追加道第二个参数的 Dispatch Queue 中。
dispatch_once

我们会在单例类中经常用到这个,该函数保证在应用程序执行中只执行一次指定处理的 API。

其他 API

除了上面介绍的几个常用的 API 之外,我们还会用到 dispatch_barrier_async \ Dispatch Semaphore 等一些其他 API,大家可以下去做进一步的了解。

死锁

上面的代码中,我们也谈到了死锁的概念,那么死锁是如何产生的呢?

死锁产生的四个必要条件

  • 互斥条件:进程对分配到的资源进行排它性使用,即在一段时间内,一个资源只能有一个进程使用。若此时有其它进程请求该资源,则只能等待,知道占有资源的进程使用完毕释放掉才可以访问。

  • 请求和保持条件:进程已经保持了至少一个资源,又提出了新的资源请求,而新的资源此时被其他进程占用。这个时候请求的进程阻塞,但又不释放自己已经占有的资源。

  • 不剥夺条件:进程已经获得的资源,其它进程不能进行抢占,只能由占有资源的进程使用完后自己释放掉。

  • 循环等待条件:发生死锁时,存在进程正在等待其它进程占有的资源释放,而占有资源的进程又正在等待其它进程的资源,由此形成一个进程——资源的环形链,无法解除。

iOS 的死锁

这个,我们也在上面讲了两种:一种是主队列的同步;一种是给当前串行队列同步提交任务到当前串行队列。

其实,关于 iOS 产生死锁的情况也就是这两种。那么,为什么会产生死锁呢?
我们先来了解一下这段代码

    dispatch_queue_t main =  dispatch_get_main_queue();
    dispatch_sync(main, ^{
        NSLog(@"1%@",[NSThread currentThread]); // 任务一
    });
    NSLog(@"2%@",[NSThread currentThread]);
  1. 线程是同步的;
  2. 队列是串行队列
  3. 任务一完成后任务二才去执行

接着弄明白上面三个点之后我们再去分析:首先主线程运行到dispatch_sync,dispatch_sync是不会立刻返回的,它会堵塞线程,等block执行完之后才会返回。运行到dispatch_sync时,会把block里的任务加入到当前线程(即主线程)的任务队列的最后边,然后遵循FIFO来执行任务,此时任务1被加到了任务2的后面。

这时就有一个问题:任务2在等待dispatch_sync返回(即执行完block里的任务1)后才会执行,但是在任务队列里任务2可是在任务1的前面,这时就进入了互相等待的场面,即形成了死锁。

另一种情况发生死锁的原因:
个人感觉是相同的,以下是我的具体分析:

类型一是往主线程里同步提交任务当主线程;类型二是给当前串行队列同步提交任务到当前串行队列。因为主线程也是串行队列,所以两种死锁原因一样。

所以,判断死锁是否产生就是一句话:是否给当前串行队列同步提交任务到当前串行队列。

文末

到这儿,我们今天讲的内容就结束了。因为篇幅问题,还有好多 GCD 的 API 并没有涉及到,但是并不代表他们不重要。因此还是得靠大家下去后再去认真学习,最主要的是自己动手都去把嵌套出现的可能实现一下,这样更能加深印象噢!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值