阅读 1331

iOS底层学习——GCD函数和队列原理探索

开发中,我们常用GCD来处理一些异步流程,感觉很熟悉,但是又很陌生。一些概念还是很模糊,比如GCD是什么,任务是什么,串行队列并发队列区别,同步函数和异步函数,队列和函数的配合使用,GCD下层封装等等。本篇我们来一一分析。

1.GCD相关概念

1.GCD

GCD全称是 Grand Central Dispatch,纯C语⾔,提供了⾮常多强⼤的函数。

GCD的优势:

  • GCD 是苹果公司为多核的并⾏运算提出的解决⽅案
  • GCD 会⾃动利⽤更多的CPU内核(⽐如双核、四核)
  • GCD 会⾃动管理线程的⽣命周期(创建线程、调度任务、销毁线程)
  • 程序员只需要告诉 GCD 想要执⾏什么任务,不需要编写任何线程管理代码

总结:GCD将任务添加到队列,并指定执行任务的函数。

2.任务

任务封装成block,任务的block没有参数也没有返回值。任务通过队列的调度,由线程来执行。

任务是如何封装并调用的呢?这是一个问题!

3.函数

执行任务的函数分为:异步函数和同步函数

  • 异步函数dispatch_async
    • 不⽤等待当前语句执⾏完毕,就可以执⾏下⼀条语句
    • 会开启线程执⾏ block 的任务
    • 异步是多线程的代名词
  • 同步函数dispatch_sync
    • 必须等待当前语句执⾏完毕,才会执⾏下⼀条语句
    • 不会开启线程
    • 在当前线程执⾏ block 的任务

4.队列

队列分为两种:串行队列并发队列。不同的队列中,任务排列的方式是不一样的,任务通过队列的调度,由线程池安排的线程来执行。

不管是串行队列还是并发队列,都会遵循FIFO的原则,即先进入先调度的原则;任务的执行速度或者说执行时长,与各自任务的复杂度有关。

  • 串行队列:通路比较窄,任务按照一定的顺序进行排列,一个一个执行
  • 并发队列:通道比较广,同一时间可能有多个任务执行

image.png

队列是什么,如何封装的,如何调度任务的,这也是我们需要研究的内容。

5.队列与函数

上面理解了队列、函数、任务的区别,队列用来调用任务,函数用来执行任务。那么队列和函数不同的配合会有怎样的运行效果呢?

  • 同步函数串行队列
    1. 不会开启线程,在当前线程中执行任务
    2. 任务串行执行,任务一个接着一个执行
    3. 会产生阻塞
  • 同步函数并发队列
    1. 不会开启线程,在当前线程中执行任务
    2. 任务一个接着一个执行
  • 异步函数串行队列
    1. 会开启一个线程
    2. 任务一个接着一个执行
  • 异步函数并发队列
    1. 开启线程,在当前线程执行任务
    2. 任务异步执行,没有顺序,CPU调度有关

2.GCD相关案例分析

1.任务耗时分析

引入一个案例,以此来分析不同函数执行任务的耗时性。见下图:

image.png

  1. 两次计时之间,什么也不做,只创建了一个串行队列,耗时0.000001秒,见下图:

    image.png

  2. 在主线程中执行textMethod,耗时时间有所增加,0.000142秒,见下图:

    image.png

  3. 将任务放入一个串行队列,并同步执行该任务,耗时0.000117秒。见下图:

    image.png

  4. 将任务放入串行队列,并异步执行该任务,异步函数会开启一个线程,并执行该任务。耗时0.000007秒,见下图:

    image.png

通过上面的案例可以得出以下结论:

  • 不管是采用什么方式,只要执行任务都会耗时
  • 异步执行是耗时相对较少,异步可以用来解决我们在开发中的并发、多线程等问题

下面结合一些比较有代表性的案例进行分析

2.主队列添加同步任务

在当前的main队列中添加一个任务,并同步执行该任务会怎么样呢?会崩!见下图:

image.png

因为在当前的流程中,默认队列就是主队列,也是一个串行队列,任务执行的顺序是:

  1. NSlog(@"0")
  2. dispathc_sync任务块
  3. NSlog(@"2")

而此时第二步中的块任务,会向当前的的主队列中添加一个任务NSlog(@"1")。此时就产生相互等待的问题,也就是我们常说的死锁!为什么呢?见下图:

image.png

因为当前是一个串行队列,dispathc_sync任务块要执行完成需要执行NSlog(@"1"),而NSlog(@"1")需要等到NSlog(@"2")执行完成后才能执行,而NSlog(@"2")又要等dispathc_sync任务块执行完成才会执行。这样主队列就会进入一个相关等待状态,也就是死锁!验证一下,见下图:

image.png

果然报错,如何解决这个问题呢?将主队列改成自定义的串行队列。见下图:

image.png

3.并发队列添加异步任务

下面的案例,是一个并发队列,通道比较宽,所以这个嵌套过程并不会引起崩溃!见下图:

image.png

运行结果是怎样的?

  1. 因为任务复杂度基本一致,所以打印顺序:1-5-2-4-3
  2. dispathc_async会开启一个新的线程去执行其中的任务;

验证一下,见下图:

image.png

  • 如果将异步函数改成同步函数会怎样的?

    因为并发队列,所以不会导致队列任务的阻塞,同时因为是同步执行,所以不会开启新的线程,按照顺序去执行流程。见下图:

    image.png

4.串行队列添加同步任务

参考下面的案例,创建一个串行队列,并向该串行队列中添加同步任务3。运行结果会怎样的呢?

image.png

  • 其实该案例和主队列添加同步任务是一样的,主队列也是串行队列。此案例也会崩溃!为什么?见下面串行队列的任务图:

    image.png

    从任务2开始进入一个串行队列,dispathc_sync任务块要执行完成需要执行NSlog(@"3"),而NSlog(@"3")需要等到NSlog(@"4")执行完成后才能执行,而NSlog(@"4")又要等dispathc_sync任务块执行完成才会执行。这样串行队列就会进入一个相关等待状态,也就是死锁!虽然此时是在子线程中,但是队列通道遵循FIFO原则,并且必须等待当前语句执⾏完毕,才会执⾏下⼀条语句。所以会崩溃!

验证一下:

image.png

5.并发队列多任务

并发队列中,添加同步任务和异步任务,会有怎样的结果呢?见下面案例:

image.png

结果分析:

  • 主队列中有10个任务
  • 其中任务3任务0是同步任务,所以任务3一定在任务0前面
  • 任务1任务2任务3的顺序是不确定的
  • 任务7任务8任务9一定在任务0的后面

3.队列源码探索

要想研究其内部实现,找到源码的出处,通过汇编查看,没有找到相关出处。我们可以通过下dispatch_queue_create符号断点,查看diaspatch的出处。见下图:

image.png

GCD来自libdispatch.dylib动态库。下载libdispatch.dylib源码,探索GCD

1.主队列

  1. dispatch_get_main_queue()分析

    进入主队列定义的地方,见下图:

    image.png

    从上面的注释中我们可以发现一些内容:

    1. 主队列在应用程序中,用于主程序和main Runloop
    2. 主队列的创建在main函数之前即完成,也即是dyld应用程序加载阶段即已创建

    主队列是通过DISPATCH_GLOBAL_OBJECT获取。全局搜索DISPATCH_GLOBAL_OBJECT,该函数是通过宏进行定义的,定义如下:

    #define DISPATCH_GLOBAL_OBJECT(type, object) ((type)&(object))
    复制代码

    其中第一个参数作为一个队列类型,第二个参数为主队列对象,可以理解为通过类型和主队列对象,封装后得到main队列。全局搜索_dispatch_main_q =,找到了主队列初始化定义的地方,见下图:

    image.png

    主队列是一个结构体,并且继承自DISPATCH_GLOBAL_OBJECT_HEADER,全局搜索DISPATCH_GLOBAL_OBJECT_HEADER,其定义如下图:

    image.png

  2. 通过lable查找主队列定义

    上面的分析方式其实有些取巧,下面通过label来定位主队列。在主线程中进行debug调试时,运行堆栈信息中,能看到下图的内容:

    image.png

    图中可以看到线程以及线程执行的任务栈信息,在主线程中,可以看到队列是一个serial类型,并且其对应的lablecom.apple.main-thread

    通过上面线索,分别打印自定义串行队列并发队列主队列全局队列。见下图:

    image.png

    在创建自定义队列时,需要传入队列的名称,也就是队列lable,以及队列的类型。通过上面的打印信息,队列都有其对应的名称。根据队列的label,在libdispatch.dylib源码中进行全局搜索。见下图:

    image.png

    最终定位到的位置和dispatch_get_main_queue()分析时结果是一样的,主队列是一个结构体,并且com.apple.main-thread是默认label

  3. 那么主队列是在何处初始化的呢?

    前面查看主队列定义的注释时,已经有说明,在main函数之前进行了初始化,我们在研究dyld时,分析objc_init()初始化时得出过这样的结论:libSystem_init -> libdispatch_init -> objc_init。那么主队列的初始化是否在dispatch_init()方法中呢?

    image.png

    dispatch_init()中成功找到了主队列初始化的地方,获取默认队列,并将主队列地址绑定到当前队列和主线程中

  4. 总结

    主队列在main函数之前,应用程序加载调用dispatch_init()方法时,即完成创建。主队列为当前的默认队列,并绑定到主线程中。

还有个问题没有搞清楚,通过debug调试可以看到主队列是串行队列serial,但是串行队列在哪里确定下来的呢?在下面分析!

2.全局队列

  • dispatch_get_global_queue(0, 0)

    进入全局队列定义的地方,见下图:

    image.png

    创建全局并发队列时可以传参数,根据不同服务质量或者优先等级提供不同的并发队列。那么我们可以得出一个结论:应该有一个全局的集合,去维护这些并发队列。

  • 用全局队列的lable-com.apple.root.default搜索

    得到一个队列集合,根据不同的服务质量提供不同的全局队列,见下图:

    image.png

  • 总结

    系统会维护一个全局队列集合,根据不同的服务质量或者优先等级提供不同的全局队列。我们在开发工作中默认使用:dispatch_get_global_queue(0, 0)

3.自定义队列创建过程

由于libdispach.dylib的源码不能编译,只能通过关键步骤逐步去定位分析。在源码中全局搜索dispatch_queue_create,结果很多,因为创建时第一个参数是一个常量,优化搜索条件,全局搜索dispatch_queue_create(const,找到创建队列的入口,见下图:

image.png

  • lable为队列名称

  • attr当前传入的,要创建的队列类型

  • 接续调用_dispatch_lane_create_with_target方法,并传入默认队列类型

    image.png

    通过上图可以明确,默认队列被定义为NULL

接着全局搜索_dispatch_lane_create_with_target(const,定位到队列创建的地方,见下面源码:

//	lable 	-> 名称
//	dqa 	-> 要创建的类型
//	tq   	-> 默认类型 - 串行NULL
DISPATCH_NOINLINE
static dispatch_queue_t
_dispatch_lane_create_with_target(const char *label, dispatch_queue_attr_t dqa,
		dispatch_queue_t tq, bool legacy)
{
	// dqai 创建 - dqa传入的属性串行还是并行
	dispatch_queue_attr_info_t dqai = _dispatch_queue_attr_to_info(dqa);

	//
	// Step 1: Normalize arguments (qos, overcommit, tq)
	//
	dispatch_qos_t qos = dqai.dqai_qos;
#if !HAVE_PTHREAD_WORKQUEUE_QOS
	if (qos == DISPATCH_QOS_USER_INTERACTIVE) {
		dqai.dqai_qos = qos = DISPATCH_QOS_USER_INITIATED;
	}
	if (qos == DISPATCH_QOS_MAINTENANCE) {
		dqai.dqai_qos = qos = DISPATCH_QOS_BACKGROUND;
	}
#endif // !HAVE_PTHREAD_WORKQUEUE_QOS

	_dispatch_queue_attr_overcommit_t overcommit = dqai.dqai_overcommit;
	if (overcommit != _dispatch_queue_attr_overcommit_unspecified && tq) {
		if (tq->do_targetq) {
			DISPATCH_CLIENT_CRASH(tq, "Cannot specify both overcommit and "
					"a non-global target queue");
		}
	}

	if (tq && dx_type(tq) == DISPATCH_QUEUE_GLOBAL_ROOT_TYPE) {
		// Handle discrepancies between attr and target queue, attributes win
		if (overcommit == _dispatch_queue_attr_overcommit_unspecified) {
			if (tq->dq_priority & DISPATCH_PRIORITY_FLAG_OVERCOMMIT) {
				overcommit = _dispatch_queue_attr_overcommit_enabled;
			} else {
				overcommit = _dispatch_queue_attr_overcommit_disabled;
			}
		}
		if (qos == DISPATCH_QOS_UNSPECIFIED) {
			qos = _dispatch_priority_qos(tq->dq_priority);
		}
		tq = NULL;
	} else if (tq && !tq->do_targetq) {
		// target is a pthread or runloop root queue, setting QoS or overcommit
		// is disallowed
		if (overcommit != _dispatch_queue_attr_overcommit_unspecified) {
			DISPATCH_CLIENT_CRASH(tq, "Cannot specify an overcommit attribute "
					"and use this kind of target queue");
		}
	} else {
		if (overcommit == _dispatch_queue_attr_overcommit_unspecified) {
			// Serial queues default to overcommit!
			overcommit = dqai.dqai_concurrent ?
					_dispatch_queue_attr_overcommit_disabled :
					_dispatch_queue_attr_overcommit_enabled;
		}
	}
	if (!tq) {
		tq = _dispatch_get_root_queue(
				qos == DISPATCH_QOS_UNSPECIFIED ? DISPATCH_QOS_DEFAULT : qos,
				overcommit == _dispatch_queue_attr_overcommit_enabled)->_as_dq;
		if (unlikely(!tq)) {
			DISPATCH_CLIENT_CRASH(qos, "Invalid queue attribute");
		}
	}

	//
	// Step 2: Initialize the queue
	//
	if (legacy) {
		// if any of these attributes is specified, use non legacy classes
		if (dqai.dqai_inactive || dqai.dqai_autorelease_frequency) {
			legacy = false;
		}
	}

	// 类的类型
	const void *vtable; // - 设置类 类是通过宏定义拼接而成
	dispatch_queue_flags_t dqf = legacy ? DQF_MUTABLE : 0;
	if (dqai.dqai_concurrent) {
		// OS_dispatch_##name##_class
		// OS_dispatch_queue_concurrent - 宏定义拼接类类型
		vtable = DISPATCH_VTABLE(queue_concurrent);
	} else {
		vtable = DISPATCH_VTABLE(queue_serial);
	}
	switch (dqai.dqai_autorelease_frequency) {
	case DISPATCH_AUTORELEASE_FREQUENCY_NEVER:
		dqf |= DQF_AUTORELEASE_NEVER;
		break;
	case DISPATCH_AUTORELEASE_FREQUENCY_WORK_ITEM:
		dqf |= DQF_AUTORELEASE_ALWAYS;
		break;
	}
	if (label) {
		const char *tmp = _dispatch_strdup_if_mutable(label);
		if (tmp != label) {
			dqf |= DQF_LABEL_NEEDS_FREE;
			label = tmp;
		}
	}
	// dq创建的地方 - alloc init   队列也是个对象
	//	OS_dispatch_queue_serial
	//	OS_dispatch_queue_concurrent
	//	即然是对象应该有ISA的走向
	dispatch_lane_t dq = _dispatch_object_alloc(vtable, // 类
			sizeof(struct dispatch_lane_s)); // alloc
	_dispatch_queue_init(dq, dqf, dqai.dqai_concurrent ? // 区分串行和并发队列
			DISPATCH_QUEUE_WIDTH_MAX : 1, DISPATCH_QUEUE_ROLE_INNER |
			(dqai.dqai_inactive ? DISPATCH_QUEUE_INACTIVE : 0)); // init

	dq->dq_label = label;
	dq->dq_priority = _dispatch_priority_make((dispatch_qos_t)dqai.dqai_qos,
			dqai.dqai_relpri);
	if (overcommit == _dispatch_queue_attr_overcommit_enabled) {
		dq->dq_priority |= DISPATCH_PRIORITY_FLAG_OVERCOMMIT;
	}
	if (!dqai.dqai_inactive) {
		_dispatch_queue_priority_inherit_from_target(dq, tq);
		_dispatch_lane_inherit_wlh_from_target(dq, tq);
	}
	_dispatch_retain(tq);
	dq->do_targetq = tq;
	_dispatch_object_debug(dq, "%s", __func__);
	return _dispatch_trace_queue_create(dq)._dq; // 通过这层可以理解为经过一层封装后,返回的依然是dq
}
复制代码

下面深入研究该方法的核心功能点。该方法的返回值为dq,虽然调用了_dispatch_trace_queue_create方法对dq进行了封装处理,但是最终返回的依然是dq,所以dq才是我们的研究重点!

  • dq创建流程

            // dq创建方 - alloc init   队列也是个对象
            //	OS_dispatch_queue_serial
            //	OS_dispatch_queue_concurrent
            //	即然是对象应该有ISA的走向
            dispatch_lane_t dq = _dispatch_object_alloc(vtable, // 类
                            sizeof(struct dispatch_lane_s)); // alloc
            _dispatch_queue_init(dq, dqf, dqai.dqai_concurrent ? // 区分串行和并发队列
                            DISPATCH_QUEUE_WIDTH_MAX : 1, DISPATCH_QUEUE_ROLE_INNER |
                            (dqai.dqai_inactive ? DISPATCH_QUEUE_INACTIVE : 0)); // init
    
            dq->dq_label = label;
            dq->dq_priority = _dispatch_priority_make((dispatch_qos_t)dqai.dqai_qos,
                            dqai.dqai_relpri);
            if (overcommit == _dispatch_queue_attr_overcommit_enabled) {
                    dq->dq_priority |= DISPATCH_PRIORITY_FLAG_OVERCOMMIT;
            }
            if (!dqai.dqai_inactive) {
                    _dispatch_queue_priority_inherit_from_target(dq, tq);
                    _dispatch_lane_inherit_wlh_from_target(dq, tq);
            }
            _dispatch_retain(tq);
            dq->do_targetq = tq;
            _dispatch_object_debug(dq, "%s", __func__);
    复制代码
    • 调用_dispatch_object_alloc方法进行初始化,内存申请

      调用_dispatch_object_alloc方法进行队列对象的初始化。与NSObject对象初始化类似,传入创建对象的模板和大小。见下图:

      image.png

      然后调用_os_object_alloc_realized方法,完成初始化流程。在该函数中,通过调用calloc进行初始化,同时isa指向cls,也就是vtable。见下图:

      image.png

    • _dispatch_queue_init为构造函数

      在调用这个方法时,第三个参数是:

      dqai.dqai_concurrent ? DISPATCH_QUEUE_WIDTH_MAX : 1
      复制代码

      不难理解,判断是否为并发队列,如果是,传入DISPATCH_QUEUE_WIDTH_MAX,否则传入1。也就是说,串行队列这里传入1,如果是并发队列,则传入DISPATCH_QUEUE_WIDTH_MAX。其定义见下图:

      image.png

      进入_dispatch_queue_init构造函数的实现,发现重要的线索,DQF_WIDTH(width);也就是用来确定队列的类型,以此来区分串行队列和并发队列,见下图:

      image.png

      到这里我们也就可以确定主队列的类型,在主队列的结构体定义中,DQF_WIDTH(1);,所以主队列是串行队列。

    • dq进行设置,如dq_labeldq_priority

dq的创建初始化流程,已经清楚了,那在dp初始化过程中传入的参数vtabledqaidqf,分别是什么,又起到什么作用呢?我们继续分析。

  • dqai初始化

    // dqai 创建 - dqa传入的属性串行还是并行
    dispatch_queue_attr_info_t dqai = _dispatch_queue_attr_to_info(dqa);
    复制代码

    _dispatch_lane_create_with_target第一行,就进行了dqai的初始化,其中参数dqa为要创建的队列的类型。查看_dispatch_queue_attr_to_info源码:

    image.png

    初始化了dqai,并判断dqa的类型,如果是并发队列,则设置并发队列为true,否则默认为串行队列。在调用_dispatch_queue_initdq进行构造时,对队列类型进行了区分,也就是DQF_WIDTH(width);的传参,串行队列width=1,否则为并发队列。

  • vtable

    vtable是什么呢?可以理解为是一个类,或者说构造队列的模板类vtable初始化流程见下面源码:

            const void *vtable; // - 设置类 类是通过宏定义拼接而成
            if (dqai.dqai_concurrent) {
                    // OS_dispatch_##name##_class
                    // OS_dispatch_queue_concurrent - 宏定义拼接类类型
                    vtable = DISPATCH_VTABLE(queue_concurrent);
            } else {
                    vtable = DISPATCH_VTABLE(queue_serial);
            }
    复制代码

    dqai在前面创建时会区分队列的类型,根据队列的类型来初始化不同的vtableDISPATCH_VTABLE是一个宏定义的方法,全局搜索DISPATCH_VTABLE的定义,最终找到以下完整的定义流程:

    // DISPATCH_VTABLE定义
    #define DISPATCH_VTABLE(name) DISPATCH_OBJC_CLASS(name)
    
    // vtable symbols - 模板
    #define DISPATCH_OBJC_CLASS(name) (&DISPATCH_CLASS_SYMBOL(name))
    
    // 拼接形成类
    #define DISPATCH_CLASS_SYMBOL(name) OS_dispatch_##name##_class
    复制代码

    DISPATCH_VTABLE函数的传参根据不同的队列类型传参不一致。

    • 并发队列

      传参queue_concurrent,最终拼接后,队列类型,也就是对应的类为:OS_dispatch_queue_concurrent

    • 串行队列

      传参queue_serial,最终拼接后,队列类型,也就是对应的类为:OS_dispatch_queue_serial

    所以vtable对应的就是队列的类型。通过拼接完成类的定义,这和我们在应用层使用的队列类型是一致的,见下图:

    image.png

  • 总结

    至此我们可以总结一下自定义队列的创建过程。在底层,会根据上层传入的队列名称lable队列类型进程封装处理。根据类型初始化对应的vtable,也就是对应的队列类(模板类)。通过allocinit方法完成队列的内存开辟和构造初始化过程,设置队列的对象的isa指向,并完成队列类型的区分。

4.函数执行

上面我们已经总结了,执行任务的函数分为:异步函数和同步函数

  • 同步函数dispatch_sync
  • 异步函数dispatch_async

那么这些行数的执行逻辑是怎么样的呢?如何调度队列中的任务并处理任务?对于异步函数底层是如何开辟线程的呢?

1.同步函数

libdispatch.dylib源码中,全局搜索dispatch_sync(dis,找到了同步函数的入口。见下图:

image.png

两个参数分别是:队列dq任务work。继续跟踪,调用_dispatch_sync_f函数,全局搜索_dispatch_sync_f(dis,见下图:

image.png

继续跟踪源码,再全局搜索_dispatch_sync_f_inline(dis,见下图:

image.png

至此:

  • 封装流程
    1. dispatch_sync
    2. _dispatch_sync_f
    3. _dispatch_sync_f_inline
  • 参数
    1. dq队列dq
    2. ctxt任务work
    3. func : 宏定义的函数_dispatch_Block_invoke
          #define _dispatch_Block_invoke(bb) \ ((dispatch_function_t)((struct Block_layout *)bb)->invoke)
      复制代码

那么在_dispatch_sync_f_inline方法中,哪一行才是我们需要研究的重点呢?先简单分析一下,首先最前面的一段判断流程:

if (likely(dq->dq_width == 1)) {
    return _dispatch_barrier_sync_f(dq, ctxt, func, dc_flags);
}
复制代码

此部分显然是串行队列流程,因为上面已经分析了,dq_width=1时,表示串行队列。那么_dispatch_sync_f_slow_dispatch_sync_recurse会总哪一步呢?我们可以通过下符号断点来确定,通过下符号断点,发现自定义并发队列,其成功走到_dispatch_sync_f_slow中。见下图:

image.png

进入_dispatch_sync_f_slow方法中,见下图:

image.png

哪一步才是需要研究的呢?显然传入的地方不太可能是null,所以大概率会走_dispatch_sync_function_invoke方法,同样采用下符号断点的方式跟踪。见下图:

image.png

进入_dispatch_sync_function_invoke方法后,会调用_dispatch_sync_function_invoke_inline方法,见下图:

image.png

继续跟踪源码,进入_dispatch_sync_function_invoke_inline实现中,见下图:

image.png

方法很多哪个方法才是我们所需要研究的重点呢?我们通过bt看一下同步函数的运行堆栈,见下图:

image.png

_dispatch_client_callout方法最终完成了任务的调用执行,进入_dispatch_client_callout方法,见下图:

image.png

搜索发现_dispatch_client_callout方法有很多,但是最终都通过func完成了work的调用执行。

  • 总结

    在同步流程中通过宏定义的函数_dispatch_Block_invoke,也就是func,完成任务work的执行。

2.异步函数

libdispatch.dylib源码中,全局搜索dispatch_async(dis,找到了异步函数的入口。见下图:

image.png

  • 任务的封装_dispatch_continuation_init

    在该方法里,对work进行了封装处理,其中方法_dispatch_continuation_init可以理解为一个任务包装器,进入_dispatch_continuation_init方法中查看实现:

    image.png

    其中_dispatch_Block_invoke这个函数很熟悉,也就是同步函数中的func。但是这里有一个判断,见下面代码:

    dispatch_function_t func = _dispatch_Block_invoke(work);
    
    if (dc_flags & DC_FLAG_CONSUME) {
    
    func = _dispatch_call_block_and_release;
    
    }
    复制代码

    dc_flags的初始值就是DC_FLAG_CONSUME,所以&处理后,这里会进入条件语句中,并且func被重新设置为_dispatch_call_block_and_release

    进入_dispatch_continuation_init_f,见下图:

    image.png

    在这里对将workfunc封装到dc中,同时对任务的优先级进行处理,因为异步函数由于线程或者cpu的调用是异步的,所以这里会对任务的调用优先级进行处理。

回到dispatch_async方法实现,完成任务包装后,调用_dispatch_continuation_async方法进行异步函数的处理流程。见下图:

image.png

很显然最后一行使我们索要研究的重点,dx_push函数三个参数分别为:队列、dc(包装的任务)、qos。全局搜索dx_push,找到了宏定义的地方,见下图:

image.png

看到vtable感觉很熟悉,没错就是上面在分析队列的创建初始化时的模板类,也就是队列对应的类。这里会调用vtable这个类的dq_push方法,直接搜索dq_push,找到其实现:

image.png

底层为不同类型的队列提供不同的调用入口,比如全局并发队列会调用_dispatch_root_queue_push方法。依次作为入口,全局搜索_dispatch_root_queue_push方法的实现:

image.png

在此流程中,前面只是做一些判断封装处理,最终会走到最后一行代码_dispatch_root_queue_push_inline中,继续跟踪器源码流程:

image.png

_dispatch_root_queue_push_inline中调用了_dispatch_root_queue_poke方法,_dispatch_root_queue_poke中的核心流程为_dispatch_root_queue_poke_slow,见下图所示:

image.png

  • _dispatch_root_queue_poke_slow实现

    _dispatch_root_queue_poke_slow中有一个关键流程,_dispatch_root_queues_init(),见下图:

    image.png

    进入_dispatch_root_queues_init()方法,在该方法中采用的单例处理,见下图:

    image.png

  • 单例处理_dispatch_root_queues_init_once

    进入_dispatch_root_queues_init_once方法,这里做了什么呢?见下图:

    image.png

    在该方法中进行了线程池的初始化处理、工作队列的配置、工作队列的初始化等工作,这也就是解释了为什么_dispatch_root_queues_init_once是单例的。单例可以避免重复的初始化。

    同时这里有一个关键的设置,执行函数的设置,也就是将任务执行的函数被统一设置成了_dispatch_worker_thread2,见下面代码:

    cfg.workq_cb = _dispatch_worker_thread2;
    复制代码

    我们可以通过bt打印运行堆栈信息,来验证异步函数最终任务是通过_dispatch_worker_thread2调用的。见下图所示:

    image.png

  • 总结

    通过跟踪异步处理流程,系统针对不同的队列类型,执行不同的dq_push的方法,并通过单例的形式完成了线程池的初始化、工作队列的配置等工作,并且底层最终通过_dispatch_worker_thread2完成了异步函数中任务的调用执行。

5.遗留问题

GCD的运用、底层原理、函数执行逻辑等,就分析到这里,但是还有一些问题没有解决,比如一下问题:

  • 对于异步函数,线程在哪里开辟?
  • 底层通过_dispatch_worker_thread2方法完成任务的执行,那么触发调用的位置在哪?
  • GCD中单例的逻辑是怎样的?

_dispatch_root_queue_poke_slow方法中还有一些没有探索的东西!

下篇文章再深入分析!

文章分类
iOS
文章标签