函数与队列 和gcd原理分析(上)

GCD 介绍

什么是GCD?

全称是 Grand Central Dispatch 

纯 C 语⾔,提供了⾮常多强⼤的函数

将任务添加到队列,并且指定执行任务的函数。

优势

GCD 是苹果公司为多核的并⾏运算提出的解决⽅案

GCD 会⾃动利⽤更多的CPU内核(⽐如双核、四核)

GCD 会⾃动管理线程的⽣命周期(创建线程、调度任务、销毁线程)

程序员只需要告诉 GCD 想要执⾏什么任务,不需要编写任何线程管理代码

GCD 的使用步骤 

 创建/获取队列:创建/获取一个并发/串行队列;

 创建任务:确定要做的事; 

 将任务添加进队列中(同时指定任务的执行方式):GCD 会自动将队列中的任务取出,放到对应的线程中执行;任务的取出遵循队列的 FIFO 原则:先进先出,后进后出;GCD 中,要执行队列中的任务时,会自动开启一个线程,当任务执行完,线程不会立刻销毁,而是放到了线程池中。如果接下来还要执行任务的话就从线程池中取出线程,这样节省了创建线程所需要的时间。但如果一段时间内没有执行任务的话,该线程就会被销毁,再执行任务就会创建新的线程。

 函数

* 任务使⽤ block 封装

* 任务的 block 没有参数也没有返回值

* 执⾏任务的函数

* 异步 `dispatch_async` 

* 不⽤等待当前语句执⾏完毕,就可以执⾏下⼀条语句

* 会开启线程执⾏ block 的任务

* 异步是多线程的代名词

* 同步 `dispatch_sync` 

* 必须等待当前语句执⾏完毕,才会执⾏下⼀条语句

* 不会开启线程

* 在当前执⾏ block 的任务


函数与队列

同步函数串行队列 

不会开启线程,在当前线程执行任务

任务串行执行,任务一个接着一个

会产生堵塞

同步函数并发队列 

不会开启线程,在当前线程执行任务

任务一个接着一个

异步函数串行队列 

开启一条新线程

任务一个接着一个

异步函数并发队列 

开启线程,在当前线程执行任务

任务异步执行,没有顺序,CPU调度有关

主队列与全局并发队列

主队列

专门用来在主线程上调度任务的串行队列

不会开启线程

如果当前主线程正在有任务执行,那么无论主队列中当前被添加了什么任务,都不会被调度

dispatch_get_main_queue()

全局并发队列

为了方便程序员的使用,苹果提供了全局并发队列dispatch_get_global_queue(0, 0)

全局队列是一个并发队列

在使用多线程开发时,如果对队列没有特殊需求,在执行异步任务时,可以直接使用全局队列

死锁现象

主线程因为你同步函数的原因等着先执行任务

主队列等着主线程的任务执行完毕再执行自己的任务

主队列和主线程相互等待会造成死锁

GCD耗能问题

创建函数,查看执行完毕时间,代码如下:



分别开启dispatch_sync和dispatch_async进行打印运行时间。结果如下:

dispatch_sync: 0.000005

dispatch_async: 0.0000010

这是因为同步和异步都是耗费时间的。都是需要调度的,但是使用这种方式解决的就是并发执行的问题。

dispatch_async

运行查看打印结果为 1 - 5 - 2 - 4 - 3,

首先执行1,接下来会执行dispatch_async,但他是异步执行,也就是说不会堵塞当前主线程,并且dispatch_async的任务与调度的时间足够快时,2可能会比5先执行,也就是说当前除了1的执行顺序是准确之外,2会在1之后,3会在2之后之外,4与5可以在任何位置都是有可能的。

dispatch_sync

打印顺序为:1 - 5 - 2 - 3 - 4,因为dispatch_sync是同步函数,需要等待3执行完成后才能执行4

将DISPATCH_QUEUE_CONCURRENT修改为DISPATCH_QUEUE_SERIAL后再运行。结果为:1 - 5 - 2 - 死锁,因为在串行队列中,dispatch_sync会等待dispatch_async中的任务完成后再执行,但它会阻塞当前队列。如图

当前的dispatch_sync块需要等待3执行完毕后才能执行4

但是因为串行队列的FIFO原则,3需要等待dispatch_sync块执行完毕后才能执行

因此出现死锁

dispatch_async与dispatch_sync


首先quque是并发队列,因此1 2 3没有顺序

但是3会进行堵塞主线程,因此0在3之后

然后7 8 9也没有顺序

GCD的底层原理

1、 队列的种类

队列分为串行队列和并行队列。

串行队列 dispatch_queue_create("xh", DISPATCH_QUEUE_SERIAL);

并行队列 dispatch_queue_create("xh", DISPATCH_QUEUE_CONCURRENT);

通过符号断点查看可知在libdispatch.dylib框架中进行了调度。

libdispatch.dylib源码分析

打印结果:


由结果可知:com.apple.main-thread的label,因为在创建队列时都会给一个label进行创建,如果没有,肯定在系统中存在默认值,因此在libdispatch.dylib框架中进行搜索查看。源码如下:

以上就是主队列创建的信息。

在注释中我们可以知道,主队列执行在main()函数之前,并且在libdispatch.dylib中一定存在init()方法,搜索查找该方法,名称为libdispatch_init,简化源码如下


从该源码中可以看到_dispatch_main_q字样

// 设置当前主队列_dispatch_queue_set_current(&_dispatch_main_q);

// 绑定线程_dispatch_queue_set_bound_thread(&_dispatch_main_q);

再研究一下全局并发队列

全局搜索com.apple.root.default-qos


查看DISPATCH_QUEUE_WIDTH_POOL源码: 


DISPATCH_QUEUE_WIDTH_POOL 是全局并发队列的最大容积 – 4095

DISPATCH_QUEUE_WIDTH_MAX是普通并发队列的最大容积 – 4094

其中相差为1是主队列已经创建。

队列的创建

查看dispatch_queue_create函数,源码如下

查看_dispatch_lane_create_with_target函数的源码,如下:


在这个函数中返回的是_dq,因此查看_dispatch_trace_queue_create函数的内容。

查看_dispatch_introspection_queue_create函数源码,如下:

其中dqic->dqic_queue._dq = dq;为创建dq的方法,dqic同样在当前函数中进行创建。

由上方源码可知,在创建时,返回的dq只是进行了一层包装。我们只要研究dq就可以了

我们回到_dispatch_lane_create_with_target函数,

这里是创建dq 通过dqai.dqai_concurrent判断是并发还是串行 dqai是怎么来的呢

再往上看:

这里创建了dqai

查看_dispatch_queue_attr_to_info的源码


该函数是对dqai的一些赋值,不需要真正的理解。

查看queue的名称

可以看到串行队列的名字为OS_dispatch_queue_serial,并发队列的名字为OS_dispatch_queue_concurrent.

全局搜索这两个名字发现都是名字定义,并没有isa的指向代码。

因此查看DISPATCH_VTABLE()方法。

查看DISPATCH_OBJC_CLASS函数源码如下

查看DISPATCH_CLASS_SYMBOL的内容

查看DISPATCH_CLASS的内容

从此处源码可以知道,queue的名字是使用OS_dispatch_...拼接的名字。

因此在创建时,并发队列使用的是queue_concurrent,串行队列使用的是queue_serial字符串。

拼接之后的名称与打印的名称一致。

queue的isa的创建

 _dispatch_object_alloc源码查看

我们需要查看OS_OBJECT_HAVE_OBJC2的内容,即else中_os_object_alloc_realized的内容。

从上方源码可知isa的创建。

总结

queue是一个对象。

queue创建流程: dispatch_queue_create => _dispatch_object_alloc => _os_object_alloc_realized

DISPATCH_QUEUE_WIDTH_POOL 是并发队列的最大容积 – 4095

DISPATCH_QUEUE_WIDTH_MAX是普通队列的最大容积 – 4094

异步函数 dispatch_async


查看_dispatch_continuation_init函数。源码如下:


查看堆栈

查看_dispatch_worker_thread2函数。源码如下:


查看_dispatch_root_queue_drain


查看_dispatch_continuation_pop_inline函数,源码如下

查看_dispatch_continuation_invoke_inline源码。

查看_dispatch_client_callout函数源码,如下:

最后再进行_dispatch_call_block_and_release处理  

流程完毕~~

下篇继续dispatch_async讲解调用流程

你可能感兴趣的:(函数与队列 和gcd原理分析(上))