18-Explore los principios subyacentes de iOS | Tecnología de subprocesos múltiples [Análisis de código fuente GCD 1: dispatch_get_global_queue y dispatch_(a)sync, singleton, thread deadlock]

prefacio

Antes, cuando estábamos explorando los principios de la animación y el renderizado, publicamos varios artículos y les respondimos iOS动画是如何渲染,特效是如何工作的疑惑. Sentimos profundamente que los diseñadores de sistemas tienen una mente tan abierta al crear estos marcos de sistemas, y también深深意识到了解一门技术的底层原理对于从事该方面工作的重要性。

Así que decidimos 进一步探究iOS底层原理的任务. Siguiendo la exploración correcta , && , GCDdel artículo anterior , este artículo continuará explorando los principios subyacentes de subprocesamiento múltiple GCD主队列串行队列并行队列全局并发队列

1. dispatch_get_global_queue cola concurrente global + función de sincronización dispatch_sync

dq->dq_width == 1 Para las colas en serie, ¿qué pasa con las colas concurrentes? Como se muestra en la figura a continuación, se sigue el siguiente proceso de marco, _dispatch_sync_f_inlinepero tantas ramas, ¿a cuál va? Rastree y depure a través de puntos de interrupción simbólicos en los métodos _dispatch_sync_f_slow, _dispatch_sync_recurse, _dispatch_introspection_sync_beginy ._dispatch_sync_invoke_and_complete

  • Depuración de puntos de interrupción simbólicos

Depuración de puntos de interrupción simbólicosA través del siguiente seguimiento del punto de interrupción del símbolo, se encuentra que se ha ido _dispatch_sync_f_slow, como se muestra en la siguiente figura:

El punto de interrupción está en _dispatch_sync_f_slowAl leer el código fuente, encontré algo interesante, que es el _dispatch_sync_invoke_and_completemétodo

_dispatch_sync_invoke_and_complete

  • _dispatch_sync_invoke_and_complete

_dispatch_sync_invoke_and_complete

_dispatch_sync_invoke_and_completeEl tercer parámetro de este método func también es la tarea que debe ejecutarse, pero func el todo detrás también es un parámetro, es decir, el DISPATCH_TRACE_ARG( _dispatch_trace_item_sync_push_pop(dq, ctxt, func, dc_flags))todo es un parámetro, lo cual es interesante, no hay una coma en el medio. Amigo, eres tan especial! ¡Tiempo suficiente!

那么去DISPATCH_TRACE_ARG定义看看 DISPATCH_TRACE_ARGDISPATCH_TRACE_ARG的宏定义里面,你们有没有发现,这里居然把逗号放在了里面,好家伙,宏定义里面还可以这么玩,苹果工程师还真有意思哈! DISPATCH_TRACE_ARG 通过全局的搜索,发现这个宏定义有两处,一个有逗号,一个没有逗号,这就是根据不同的条件,进行设置,相当于是一个可选的参数,这一波操作又是非常的细节了!

既然下符号断点会走_dispatch_sync_f_slow方法,现在就去看看这个方法

  • _dispatch_sync_f_slow

_dispatch_sync_f_slow 这里又是很多的分支,又通过下符号断点,发现走的是_dispatch_sync_function_invoke方法里面

  • _dispatch_sync_function_invoke
static void
_dispatch_sync_function_invoke(dispatch_queue_class_t dq, void *ctxt,
		dispatch_function_t func)
{
	_dispatch_sync_function_invoke_inline(dq, ctxt, func);
}
  • _dispatch_sync_function_invoke_inline
static inline void
_dispatch_sync_function_invoke_inline(dispatch_queue_class_t dq, void *ctxt,
		dispatch_function_t func)
{
	dispatch_thread_frame_s dtf;
	_dispatch_thread_frame_push(&dtf, dq);
	_dispatch_client_callout(ctxt, func);
	_dispatch_perfmon_workitem_inc();
	_dispatch_thread_frame_pop(&dtf);
}
  • push 之后调用callout执行,最后再 pop,所以可以同步的执行任务

二、 dispatch_async异步函数

dispatch_async异步函数的任务,是包装在 qos里面的,那么现在跟踪流程,去看看

  • dispatch_async

dispatch_async

  • _dispatch_continuation_async

_dispatch_continuation_async

  • dx_push

dx_push 搜索dx_push调用的地方 inserte la descripción de la imagen aquí 这里就先去看看并发队列里面的dq_push吧,

  • _dispatch_lane_concurrent_push

_dispatch_lane_concurrent_push 这里if里面有对栅栏函数(_dispatch_object_is_barrier)的判断,栅栏函数这里就不分析了,后续的博客里面会分析的。

_dispatch_lane_concurrent_push里面会去调用_dispatch_lane_push方法,在上面搜索dx_push的图里面,可以看到,在串行队列里面是直接调用了_dispatch_lane_push,也就是说串行并发都会走这个方法。

  • _dispatch_lane_push

_dispatch_lane_push 最后去调用dx_wakeup,再去搜索看看 dx_wakeup dx_wakeup 是一个宏定义,看看dq_wakeup哪里调用了 lugar de llamada dx_wakeup 如上图可以发现,串行和并发都是_dispatch_lane_wakeup,全局的是_dispatch_root_queue_wakeup _dispatch_lane_wakeup

  • _dispatch_queue_wakeup

_dispatch_queue_wakeup

通过下符号断点会走_dispatch_lane_class_barrier_complete _dispatch_lane_class_barrier_complete _dispatch_lane_class_barrier_complete里面循环递归一些操作,还看到了一个系统的函数os_atomic_rmw_loop2o,在这个方法里面要么返回dx_wakeup或者做其他的一些处理。

Información de cola simultánea _dispatch_lane_concurrent_push _dispatch_continuation_redirect_push

#define dx_push(x, y, z) dx_vtable(x)->dq_push(x, y, z)

通过跟流程和下符号断点,会走全局并发队列的_dispatch_root_queue_push方法。通过下符号断点,跟踪源码,最终定位到一个重要的方法_dispatch_root_queue_poke_slow

dispatch_root_queue_push_inline(dispatch_queue_global_t dq,
		dispatch_object_t _head, dispatch_object_t _tail, int n)
{
	struct dispatch_object_s *hd = _head._do, *tl = _tail._do;
	if (unlikely(os_mpsc_push_list(os_mpsc(dq, dq_items), hd, tl, do_next))) {
		return _dispatch_root_queue_poke(dq, n, 0);
	}
}
  • _dispatch_root_queue_poke

_dispatch_root_queue_poke

  • _dispatch_root_queue_poke_slow

_dispatch_root_queue_poke_slow _dispatch_root_queues_init方法使用了单例。

static inline void
_dispatch_root_queues_init(void)
{
	dispatch_once_f(&_dispatch_root_queues_pred, NULL,
	_dispatch_root_queues_init_once);
}

在该方法中,采用单例的方式进行了线程池的初始化处理、工作队列的配置、工作队列的初始化等工作。同时这里有一个关键的设置,执行函数的设置,也就是将任务执行的函数被统一设置成了_dispatch_worker_thread2。见下图: _dispatch_root_queues_init_once

  • 调用堆栈验证

apilar información

调用执行是通过workloop工作循环调用起来的,也就是说并不是及时调用的,而是通过os完成调用,说明异步调用的关键是在需要执行的时候能够获取对应的方法,进行异步处理,而同步函数是直接调用。

在上面的流程中_dispatch_root_queue_poke_slow 方法,还没有继续分析,现在就去分析,如果是全局队列,此时会创建线程进行执行任务 procesamiento de cola global 对线程池进行处理,从线程池中获取线程,执行任务,同时判断线程池的变化 Grupo de subprocesos para procesamiento remaining可以理解为当前可用线程数,当可用线程数等于0时,线程池已满pthread pool is full,直接return。底层通过pthread完成线程的开辟 inserte la descripción de la imagen aquí 就是_dispatch_worker_thread2是通过pthread完成oc_atmoic原子触发

那么我们的线程可以开辟多少线程条呢?

inicialización del grupo de subprocesos

队列线程池的大小为:dgq_thread_pool_sizedgq_thread_pool_size = thread_pool_size ,默认大小如下: DISPATCH_WORKQ_MAX_PTHREAD_COUNT 255表示理论上线程池的最大数量。但是实际能开辟多少呢,这个不确定。在苹果官方完整Thread Management中,有相关的说明,辅助线程的最小允许堆栈大小为 16 KB,并且堆栈大小必须是4KB 的倍数。见下图: Thread Management 也就是说,一个辅助线程的栈空间是512KB,而一个线程所占用的最小空间是16KB,也就是说栈空间一定的情况下,开辟线程所需的内存越大,所能开辟的线程数就越小。针对一个4GB内存的iOS真机来说,内存分为内核态和用户态,如果内核态全部用于创建线程,也就是1GB的空间,也就是说最多能开辟1024KB / 16KB个线程。当然这也只是一个理论值。

三、 单例

上面提到了单例,那么接下来就去分析一下单例 来看看简单的单例使用:

   static dispatch_once_t token;

   dispatch_once(&token, ^{
       // 代码执行
   });
  • 单例的定义如下:

单例定义

void
_dispatch_once(dispatch_once_t *predicate,
		DISPATCH_NOESCAPE dispatch_block_t block)
{
	if (DISPATCH_EXPECT(*predicate, ~0l) != ~0l) {
		dispatch_once(predicate, block);
	} else {
		dispatch_compiler_barrier();
	}
	DISPATCH_COMPILER_CAN_ASSUME(*predicate == ~0l);
}
#undef dispatch_once
#define dispatch_once _dispatch_once
#endif
#endif // DISPATCH_ONCE_INLINE_FASTPATH

针对不同的情况作了一些特殊处理,比如栅栏函数等,这里只分析dispatch_once,进入dispatch_once实现 dispatch_once 单例是只会执行一次,那么这里就是利用 val参数来进行控制的,接着去dispatch_once_f里面看看 在这里插入图片描述l的底层原子性进行关联,关联到uintptr_t v的一个变量,通过os_atomic_load从底层取出,关联到变量v上。如果v这个值等于DLOCK_ONCE_DONE,也就是已经处理过一次了,就会直接return返回

  • _dispatch_once_gate_tryenter
static inline bool
_dispatch_once_gate_tryenter(dispatch_once_gate_t l)
{
	return os_atomic_cmpxchg(&l->dgo_once, DLOCK_ONCE_UNLOCKED,
			(uintptr_t)_dispatch_lock_value_for_self(), relaxed);
}

_dispatch_once_gate_tryenter里面是进行原子操作,就是锁的处理,如果之前没有执行过,原子处理会比较它状态,进行解锁,最终会返回一个bool值,多线程情况下,只有一个能够获取锁返回yes

if (_dispatch_once_gate_tryenter(l)) {
     return _dispatch_once_callout(l, ctxt, func);
}

通过_dispatch_lock_value_for_self上了一把锁,保证多线程安全。如果返回yes,就会执行_dispatch_once_callout方法,执行单例对应的任务,并对外广播

  • _dispatch_once_callout
static void
_dispatch_once_callout(dispatch_once_gate_t l, void *ctxt,
		dispatch_function_t func)
{
	_dispatch_client_callout(ctxt, func);
	_dispatch_once_gate_broadcast(l);
}
  • _dispatch_client_callout执行任务
  • _dispatch_once_gate_broadcast对外广播,标记为 done
  • _dispatch_once_gate_broadcast广播

_dispatch_once_gate_broadcasttoken通过原子比对,如果不是done,则设为done。同时对_dispatch_once_gate_tryenter方法中的锁进行处理。

  • _dispatch_once_mark_done

_dispatch_once_mark_done os_atomic_cmpxchg 是一个宏定义,先进行比较再改变,先比较 dgo,在设置标记为DLOCK_ONCE_DONE也就是 doneos_atomic_cmpxchg

token标记为done之后,就会直接返回,如存在多线程处理,没有获取锁的情况,就会调用_dispatch_once_wait,如下下: 单例执行方法 _dispatch_once_wait,进行等待,这里开启了自旋锁,内部进行原子处理,在loop过程中,如果发现已经被其他线程设置once_done了,则会进行放弃处理 _dispatch_once_wait 那么任务的执行交给谁了呢? 堆栈信息 通过打印堆栈信息,发现是交给了下层的线程,通过一些包装,给了底层的pthread 在这里插入图片描述 这就可以说 GCD底层是封装了pthread ,不管是 iOS还是 Java都是封装了底层的通用线程机制pthread

这里的执行是通过工作循环workloop,工作循环的调起受 OS(受 CPU调度执行的。)管控的,异步线程的异步体现在哪里呢?就是体现在是否可以获得,而不是立即执行,而同步函数是直接调用执行的,而这里并没有看到异步的直接调用执行。

四、 sync 和 async 的区别

  • 是否可以开启新的线程执行任务
  • 任务的回调是否具有异步行、同步性
  • 是否产生死锁问题

五、 死锁 源码分析

在前面篇幅的分析中,我们得知,同步 sync函数的流程是:

  • _dispatch_sync_f -- >
  • _dispatch_sync_f_inline -- >
  • _dispatch_barrier_sync_f

_dispatch_sync_f_inline 走到_dispatch_barrier_sync_f流程中,这与上篇博客的分析是一致的,因为这里dq_width=1,所以是串行队列,如果是并发队列,则会走到_dispatch_sync_f_slow,现在去_dispatch_barrier_sync_f方法里面看看

  • _dispatch_barrier_sync_f
static void
_dispatch_barrier_sync_f(dispatch_queue_t dq, void *ctxt,
		dispatch_function_t func, uintptr_t dc_flags)
{
	_dispatch_barrier_sync_f_inline(dq, ctxt, func, dc_flags);
}

这个方法又会调用_dispatch_barrier_sync_f_inline方法

_dispatch_barrier_sync_f_inline 在这个方法里面,会对队列进行判断,是否存在等待或者挂起状态

//判断是否挂起、等待
if (unlikely(!_dispatch_queue_try_acquire_barrier_sync(dl, tid))){
    // 添加任务
    return _dispatch_sync_f_slow(dl, ctxt, func, DC_FLAG_BARRIER, dl,
				DC_FLAG_BARRIER | dc_flags);
}

在之前的博客里面也提到了死锁相关的内容,出现死锁会报和_dispatch_sync_f_slow相关的错误,如下:

死锁 虽然死锁会走_dispatch_sync_f_slow方法,但是死锁的报错不是_dispatch_sync_f_slow这个报错,而是如下图中所示的0处报错了

死锁报错

真报错的是__DISPATCH_WAIT_FOR_QUEUE__,那么现在去验证一下

  • _dispatch_sync_f_slow

_dispatch_sync_f_slow_dispatch_sync_f_slow方法内部,我们发现了刚刚死锁报错的__DISPATCH_WAIT_FOR_QUEUE__,现在去内部看看

  • __DISPATCH_WAIT_FOR_QUEUE__

DISPATCH_WAIT_FOR_QUEUE

__DISPATCH_WAIT_FOR_QUEUE__内部,发现了和死锁报错信息基本一样,意思是:

dispatch_sync 在当前线程已经拥有的队列上调用 ,对不起兄弟,我已经拥有她了,你来晚一步了

if (unlikely(_dq_state_drain_locked_by(dq_state, dsc->dsc_waiter))) {
		DISPATCH_CLIENT_CRASH((uintptr_t)dq_state,
				"dispatch_sync called on queue "
				"already owned by current thread");
}

这个dsc_waiter是由前面_dispatch_sync_f_slow方法里面传过来来的

dsc_waiter

_dispatch_tid_self()是线程 id,定义如下

_dispatch_tid_self()

_dispatch_thread_port是线程的通道,现在再去看看线程状态的匹配

//状态
uint64_t dq_state = _dispatch_wait_prepare(dq);
if (unlikely(_dq_state_drain_locked_by(dq_state, dsc->dsc_waiter))) {
        DISPATCH_CLIENT_CRASH((uintptr_t)dq_state,
                "dispatch_sync called on queue "
                "already owned by current thread");
}
  • _dq_state_drain_locked_by
static inline bool
_dq_state_drain_locked_by(uint64_t dq_state, dispatch_tid tid)
{
	return _dispatch_lock_is_locked_by((dispatch_lock)dq_state, tid);
}
  • _dispatch_lock_is_locked_by
static inline bool
_dispatch_lock_is_locked_by(dispatch_lock lock_value, dispatch_tid tid)
{
	// equivalent to _dispatch_lock_owner(lock_value) == tid
	return ((lock_value ^ tid) & DLOCK_OWNER_MASK) == 0;
}
  • DLOCK_OWNER_MASK
#define DLOCK_OWNER_MASK			((dispatch_lock)0xfffffffc)

这里就是死锁的判断:异或再作操作,也就是结果为0就是死锁。翻译一下就是dq_state ^ dsc->dsc_waiter 的结果为 0再和DLOCK_OWNER_MASK操作等于0

那么dq_state ^ dsc->dsc_waiter 的结果什么情况下会为 0呢?异或是相同为0,因为DLOCK_OWNER_MASK是一个非常大的整数,所以dq_statedsc->dsc_waiter 都是为0

El subproceso id para es el mismo que llamo, ya estoy en 等待状态, ahora tiene una nueva tarea que debo ejecutar, lo que crea una contradicción, ingresa al 相互等待estado y luego genera 死锁. ¡Es por eso que las colas en serie ejecutan tareas síncronas y provocan interbloqueos!

Supongo que te gusta

Origin juejin.im/post/7116878578091819045
Recomendado
Clasificación