GCD
任务+队列
主队列: 任务在主线程执行,主队列是一个串行队列,它主要处理 UI 相关任务,也可以处理其他类型任务,但为了性能考虑,尽量让主队列执行 UI 相关或少量不耗时间和资源的操作。
系统全局并发队列:全局并发队列,存在 5 个不同的 QoS 级别,可以使用默认优先级,也可以单独指定
并行队列: 先进先出,多个任务可以并行执行
串行队列 : 先进先出,同一时间只能执行一个任务
基本操作:
sync 同步
任务一经提交就会阻塞当前线程(当前线程可以理解为下方代码示例中执行 sync
方法所在的线程 thread0
),并请求队列立即安排其执行,执行任务的线程 thread1
默认等于 thread0
,即同步任务直接在当前线程运行,任务完成后恢复线程原任务。
async 异步
任务提交后不会阻塞当前线程,会由队列安排另一个线程执行。
1. 同步(Synchronous)
• 定义:同步意味着任务是按顺序执行的,一个任务完成后,才会开始下一个任务。在执行过程中,程序会阻塞,等待当前任务完成之后才能继续执行下一步。
• 特征:
• 阻塞:程序在执行某个任务时,会等待该任务完成,然后才会继续执行其他任务。
• 执行顺序:任务按照书写的顺序依次执行。
• 简单直观:容易理解,执行顺序清晰,但当任务较慢时会浪费时间,因为程序需要等待任务完成。
• 示例:
假设你需要从数据库中获取数据,读取数据的过程是同步的:
func fetchData() {
let data = loadDataFromDatabase() // 阻塞直到数据加载完成
processData(data) // 数据加载完成后才会执行
}
这意味着,如果 loadDataFromDatabase() 操作需要很长时间(比如网络请求、文件读取等),那么程序在此期间会被阻塞,不能执行其他任务。
2. 异步(Asynchronous)
• 定义:异步意味着任务的执行不阻塞主线程。任务在后台执行时,程序可以继续执行其他操作,直到任务完成时,通知程序来处理结果。
• 特征:
• 非阻塞:程序不会等待当前任务完成,允许其他操作继续执行。
• 回调:当异步任务完成时,通常会通过回调函数(比如闭包、代理等)来通知主程序,处理结果。
• 并发:多个任务可以并行执行,从而提高效率,避免长时间的阻塞。
• 示例:
假设你需要从数据库中获取数据,但希望不阻塞主线程,使用异步方法来加载数据:
func fetchData() {
loadDataFromDatabaseAsync { data in // 异步加载数据,回调处理
processData(data) // 数据加载完成后执行回调
}
}
在这个例子中,loadDataFromDatabaseAsync 会在后台加载数据,主线程不受阻塞,其他操作可以继续进行。当数据加载完成时,回调函数会被调用,随后执行 processData(data)。
同步和异步的区别总结
特性 | 同步 (Synchronous) | 异步 (Asynchronous) |
---|---|---|
执行方式 | 按顺序执行,任务完成后再执行下一个 | 任务在后台执行,程序可以继续执行其他操作 |
程序等待 | 程序会等待任务完成,任务执行时会阻塞 | 程序不等待任务完成,可以继续执行其他任务 |
阻塞与非阻塞 | 阻塞,当前任务未完成无法继续执行其他任务 | 非阻塞,任务在后台执行,主线程不受阻塞 |
适用场景 | 适合任务简单、快速,或必须按顺序执行的场景 | 适合任务需要较长时间、可以并发处理的场景 |
举个例子:下载文件
假设你需要从互联网上下载一个文件:
1. 同步下载:如果你使用同步方式下载文件,程序会等待文件下载完成后,才会继续执行后续的操作。在文件下载的过程中,程序会被"卡住",无法做其他事情,直到下载完成。
2. 异步下载:如果你使用异步方式下载文件,程序会立即开始下载并跳转到下一步操作,文件下载则在后台进行。下载完成后,程序会通过回调或通知来处理下载结果,这样你可以在下载文件的同时执行其他任务。
优缺点对比
• 同步:
• 优点:简单、直观,适合任务简单且必须顺序执行的场景。
• 缺点:如果一个任务耗时较长,会阻塞整个程序,导致性能瓶颈。
• 异步:
• 优点:不会阻塞主线程,适合长时间运行的任务,可以提高程序效率和响应性。
• 缺点:实现较为复杂,通常需要回调、闭包、Promise、异步/并发框架等机制来处理。
实际应用
• 同步:适合计算密集型操作或那些不需要等待的任务(如简单的计算)。
• 异步:适合网络请求、文件 I/O、数据库查询等耗时操作,这样可以避免程序界面卡顿或响应迟缓。
理解同步与异步的差异有助于提高程序的效率,尤其是在面对网络请求或处理大规模数据时。
创建队列:
提供了两种队列,系统队列(全局并发队列),自定义队列
对于自定义队列,默认是串行的
-
串行队列同一时间只会使用同一线程、运行同一任务,并严格按照任务顺序执行。
-
并行队列同一时间可以使用多个线程、运行多个任务,执行顺序不分先后。
-
同步任务会阻塞当前线程,并在当前线程执行。
-
异步任务不会阻塞当前线程,并在与当前线程不同的线程执行。
-
如何避免死锁:不要在串行或主队列中嵌套执行同步任务。
栏栅任务
栅栏任务的主要特性是可以对队列中的任务进行阻隔,执行栅栏任务时,它会先等待队列中已有的任务全部执行完成,然后它再执行,在它之后加入的任务也必须等栅栏任务执行完后才能执行。
这个特性更适合并行队列,而且对栅栏任务使用同步或异步方法效果都相同。
- 创建方式,先创建
WorkItem
,标记为:barrier
,再添加至队列中:
swift
let queue = DispatchQueue(label: "queueName", attributes: .concurrent)
let task = DispatchWorkItem(flags: .barrier) {
// do something
}
queue.async(execute: task)
queue.sync(execute: task) // 与 async 效果一样
迭代任务
并行队列利用多个线程执行任务,可以提高程序执行的效率。而迭代任务可以更高效地利用多核性能,它可以利用 CPU 当前所有可用线程进行计算(任务小也可能只用一个线程)。如果一个任务可以分解为多个相似但独立的子任务,那么迭代任务是提高性能最适合的选择。
使用 concurrentPerform
方法执行迭代任务,迭代任务的后续任务需要等待它执行完成才会继续。本方法类似于 Objc 中的 dispatch_apply
方法,创建方式如下:
DispatchQueue.concurrentPerform(iterations: 10) {(index) -> Void in // 10 为迭代次数,可修改。
// do something
}
迭代任务可以单独执行,也可以放在指定的队列中:
swift
let queue = DispatchQueue.global() // 全局并发队列
queue.async {
DispatchQueue.concurrentPerform(iterations: 100) {(index) -> Void in
// do something
}
//可以转至主线程执行其他任务
DispatchQueue.main.async {
// do something
}
}
延迟加入队列
swift
class AsyncAfter {
typealias ExchangableTask = (_ newDelayTime: TimeInterval?,
_ anotherTask:@escaping (() -> ())
) -> Void
/// 延迟执行一个任务,并支持在实际执行前替换为新的任务,并设定新的延迟时间。
///
/// - Parameters:
/// - time: 延迟时间
/// - yourTask: 要执行的任务
/// - Returns: 可替换原任务的闭包
static func delay(_ time: TimeInterval, yourTask: @escaping ()->()) -> ExchangableTask {
var exchangingTask: (() -> ())? // 备用替代任务
var newDelayTime: TimeInterval? // 新的延迟时间
let finalClosure = { () -> Void in
if exchangingTask == nil {
DispatchQueue.main.async(execute: yourTask)
} else {
if newDelayTime == nil {
DispatchQueue.main.async {
print("任务已更改,现在是:\(Date())")
exchangingTask!()
}
}
print("原任务取消了,现在是:\(Date())")
}
}
dispatch_later(time) { finalClosure() }
let exchangableTask: ExchangableTask =
{ delayTime, anotherTask in
exchangingTask = anotherTask
newDelayTime = delayTime
if delayTime != nil {
self.dispatch_later(delayTime!) {
anotherTask()
print("任务已更改,现在是:\(Date())")
}
}
}
return exchangableTask
}
}
delay
方法接收两个参数,并返回一个闭包:
- TimeInterval:延迟时间
- @escaping () -> (): 要延迟执行的任务
- 返回:可替换原任务的闭包,我们去了一个别名:
ExchangableTask
ExchangableTask
类型定义的闭包,接收一个新的延迟时间,和一个新的任务。
如果不执行返回的闭包,则在delay
方法内部,通过 dispatch_later
方法会继续执行原任务。
如果执行了返回的 ExchangableTask
闭包,则会选择执行新的任务。
线程安全和线程同步
读写操作
swift
let queue = DispatchQueue(label: "com.example.queue", attributes: .concurrent)
var array = [Int]()
// 写操作
queue.async(flags: .barrier) {
array.append(1)
array.append(2)
}
// 读操作
queue.async {
print(array)
}
什么时候用串行队列?什么时候用并发队列 + 栅栏?
• 用串行队列:
• 当你的任务量不大,或对性能要求不高时。
• 任务的顺序性更重要,所有操作按严格顺序执行。
• 用并发队列 + 栅栏:
• 当你的任务中有大量的读操作,并且需要同时保证写操作的安全性时。
• 适用于性能要求较高的场景,例如数据库、多线程缓存读写操作。
所以,串行队列的实现虽然简单,但并发队列 + 栅栏在性能和灵活性上更有优势,特别是在读多写少的场景下。
多线程典型场景
-
一个页面有三个网络请求,需要在三个网络请求都返回的时候刷新页面
使用dispatch group
-
实现一个线程安全的Array的读和写
swiftimport Foundation class ThreadSafeArray<T> { private var array: [T] = [] private let queue = DispatchQueue(label: "com.example.ThreadSafeArray", attributes: .concurrent) // 写操作:追加元素 func append(_ item: T) { queue.async(flags: .barrier) { self.array.append(item) } } // 写操作:移除元素 func remove(at index: Int) { queue.async(flags: .barrier) { guard index < self.array.count else { return } self.array.remove(at: index) } } // 读操作:通过索引访问 func get(at index: Int) -> T? { var result: T? queue.sync { guard index < self.array.count else { return } result = self.array[index] } return result } // 读操作:获取数组的所有内容 func getAll() -> [T] { var result: [T] = [] queue.sync { result = self.array } return result } // 读操作:获取数组的大小 var count: Int { var result = 0 queue.sync { result = self.array.count } return result } }
代码的线程安全性
• 写操作:
• 使用 async(flags: .barrier) 保证写操作互斥。
• 当有一个写操作在执行时,队列会阻止其他读和写操作,确保数据一致性。
• 读操作:
• 通过 queue.sync 保证读操作线程安全。
• 即使多个读操作同时进行,也不会干扰,因为并发队列允许多个 sync 操作并行。
代码执行逻辑示例
1. 写操作的独占性
假设同时有以下两个任务:
• array.append(1)
• array.remove(at: 0)
因为写操作使用了 .barrier,它们不会同时执行,队列会保证第一个写操作完成后再执行下一个写操作。
2. 读写互斥
假设同时有以下任务:
• 读取元素 array.getAll()
• 添加新元素 array.append(1)
在这种情况下:
1. 读操作通过 sync 提交到队列中,可以立即并发执行。
2. 写操作通过 barrier 提交时,会等待所有正在进行的读操作完成后再执行。
3. 并发读
假设同时有以下任务:
• array.getAll()
• array.get(at: 0)
因为读操作是通过 sync 提交到并发队列中,它们可以并行执行,不会互相阻塞。
同步(Synchronous)和异步(Asynchronous)是编程中两种不同的执行方式,主要区别在于任务的执行顺序和程序的等待行为。
信号量
DispatchSemaphore
,通常称作信号量,顾名思义,它可以通过计数来标识一个信号,这个信号怎么用呢,取决于任务的性质。通常用于对同一个资源访问的任务数进行限制。
例如,控制同一时间写文件的任务数量、控制端口访问数量、控制下载任务数量等。
信号量的使用非常的简单:
- 首先创建一个初始数量的信号对象
- 使用
wait
方法让信号量减 1,再安排任务。如果此时信号量仍大于或等于 0,则任务可执行,如果信号量小于 0,则任务需要等待其他地方释放信号。 - 任务完成后,使用
signal
方法增加一个信号量。 - 等待信号有两种方式:永久等待、可超时的等待。
基础源码解析
队列和线程之间的关系
一个重要概念是overcommit,overcommit的队列在队列创建时会新建一个线程,非overcommit队列创建队列则未必创建线程。另外width=1意味着是串行队列,只有一个线程可用,width=0xffe则意味着并行队列,线程则是从线程池获取,可用线程数是64个。
可以看到全局队列是非overcommit的(flat保留字只能传0,如果默认优先级则是com.apple.root.default-qos,但是width=0xffe是并行队列);主队列是overcommit的com.apple.root.default-qos.overcommit,不过它是串行队列,width=1,并且运行的这个线程只能是主线程;自定义串行队列是overcommit的,默认优先级则是 com.apple.root.default-qos.overcommit,并行队列则是非overcommit的。
串行并行的判断
首先通过width判定是串行队列还是并发队列,如果是并发队列则调用_dispatch_sync_invoke_and_complete
,串行队列则调用_dispatch_barrier_sync_f
死锁的判断
_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;
}
通过判断队列的状态和线程的状态,把这两个进行异或操作,如果状态相同的话,就返回yes,产生死锁。
如何执行调度
DISPATCH_NOINLINE
static void
_dispatch_sync_invoke_and_complete_recurse(dispatch_queue_class_t dq,
void *ctxt, dispatch_function_t func, uintptr_t dc_flags
DISPATCH_TRACE_ARG(void *dc))
{
_dispatch_sync_function_invoke_inline(dq, ctxt, func);
_dispatch_trace_item_complete(dc);
_dispatch_sync_complete_recurse(dq._dq, NULL, dc_flags);
}
// 看一下 _dispatch_sync_function_invoke_inline
DISPATCH_ALWAYS_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);
}
// 看一下 _dispatch_client_callout
void
_dispatch_client_callout(void *ctxt, dispatch_function_t f)
{
@try {
return f(ctxt);
}
@catch (...) {
objc_terminate();
}
}
典型流程示例
假设通过 dispatch_sync(queue, ^{ ... })
提交任务:
- 任务入队 :
- 如果队列空闲,直接调用
_dispatch_sync_invoke_and_complete_recurse
。
- 如果队列空闲,直接调用
- 执行任务 :
_dispatch_sync_function_invoke_inline
调用用户代码。- 线程帧压栈,记录当前队列。
- 异常处理 :
- 用户代码若抛出异常,进程终止。
- 清理资源 :
- 线程帧弹栈,递归更新队列状态,唤醒其他任务。
异步操作:
static void
_dispatch_root_queue_poke_slow(dispatch_queue_global_t dq, int n, int floor)
{
int remaining = n;
int r = ENOSYS;
_dispatch_root_queues_init();
_dispatch_debug_root_queue(dq, __func__);
_dispatch_trace_runtime_event(worker_request, dq, (uint64_t)n);
#if !DISPATCH_USE_INTERNAL_WORKQUEUE
#if DISPATCH_USE_PTHREAD_ROOT_QUEUES
if (dx_type(dq) == DISPATCH_QUEUE_GLOBAL_ROOT_TYPE)
#endif
{
_dispatch_root_queue_debug("requesting new worker thread for global "
"queue: %p", dq);
r = _pthread_workqueue_addthreads(remaining,
_dispatch_priority_to_pp_prefer_fallback(dq->dq_priority));
(void)dispatch_assume_zero(r);
return;
}
#endif // !DISPATCH_USE_INTERNAL_WORKQUEUE
#if DISPATCH_USE_PTHREAD_POOL
dispatch_pthread_root_queue_context_t pqc = dq->do_ctxt;
if (likely(pqc->dpq_thread_mediator.do_vtable)) {
while (dispatch_semaphore_signal(&pqc->dpq_thread_mediator)) {
_dispatch_root_queue_debug("signaled sleeping worker for "
"global queue: %p", dq);
if (!--remaining) {
return;
}
}
}
bool overcommit = dq->dq_priority & DISPATCH_PRIORITY_FLAG_OVERCOMMIT;
if (overcommit) {
// 串行队列
os_atomic_add2o(dq, dgq_pending, remaining, relaxed);
} else {
if (!os_atomic_cmpxchg2o(dq, dgq_pending, 0, remaining, relaxed)) {
_dispatch_root_queue_debug("worker thread request still pending for "
"global queue: %p", dq);
return;
}
}
// floor 为 0,remaining 是根据队列任务的情况处理的
int can_request, t_count;
// 获取线程池的大小
t_count = os_atomic_load2o(dq, dgq_thread_pool_size, ordered);
do {
// 计算可以请求的数量
can_request = t_count < floor ? 0 : t_count - floor;
if (remaining > can_request) {
_dispatch_root_queue_debug("pthread pool reducing request from %d to %d",
remaining, can_request);
os_atomic_sub2o(dq, dgq_pending, remaining - can_request, relaxed);
remaining = can_request;
}
if (remaining == 0) {
// 线程池满了,就会报出异常的情况
_dispatch_root_queue_debug("pthread pool is full for root queue: "
"%p", dq);
return;
}
} while (!os_atomic_cmpxchgvw2o(dq, dgq_thread_pool_size, t_count,
t_count - remaining, &t_count, acquire));
pthread_attr_t *attr = &pqc->dpq_thread_attr;
pthread_t tid, *pthr = &tid;
#if DISPATCH_USE_MGR_THREAD && DISPATCH_USE_PTHREAD_ROOT_QUEUES
if (unlikely(dq == &_dispatch_mgr_root_queue)) {
pthr = _dispatch_mgr_root_queue_init();
}
#endif
do {
_dispatch_retain(dq);
// 开辟线程
while ((r = pthread_create(pthr, attr, _dispatch_worker_thread, dq))) {
if (r != EAGAIN) {
(void)dispatch_assume_zero(r);
}
_dispatch_temporary_resource_shortage();
}
} while (--remaining);
#else
(void)floor;
#endif // DISPATCH_USE_PTHREAD_POOL
}
这段代码是 GCD(Grand Central Dispatch)中用于管理全局队列(Global Queue)线程池 的核心函数 _dispatch_root_queue_poke_slow
,其主要职责是根据任务需求动态创建或唤醒线程。以下是对代码的分层解析:
一、函数背景与核心目标
- 调用场景 :当全局队列(
dispatch_queue_global_t
)中有新任务提交,但当前线程池中没有足够空闲线程处理任务时,触发此函数。 - 核心目标:通过创建新线程或唤醒休眠线程,确保任务能够被及时执行。
- 关键参数 :
dq
:目标全局队列(可能是并发队列或特定优先级的队列)。n
:需要唤醒或创建的线程数量。floor
:线程池的最小保留线程数(避免过度销毁)。
二、代码逻辑分层解析
1. 初始化与调试(Initialization & Debugging)
c
_dispatch_root_queues_init(); // 确保全局根队列初始化
_dispatch_debug_root_queue(dq, __func__); // 调试日志
_dispatch_trace_runtime_event(worker_request, dq, (uint64_t)n); // 性能追踪
- 作用:确保根队列已初始化,记录调试信息和性能事件。
2. 内核级 Workqueue 处理(XNU Kernel Workqueue)
c
#if DISPATCH_USE_PTHREAD_ROOT_QUEUES
r = _pthread_workqueue_addthreads(remaining, _dispatch_priority_to_pp_prefer_fallback(dq->dq_priority));
- 条件 :当使用
pthread
工作队列(如 macOS 或 iOS 的默认配置)。 - 行为 :直接通过
_pthread_workqueue_addthreads
向 XNU 内核请求线程。 - 机制 :
- 内核根据优先级(
dq_priority
转换为pthread_priority_t
)动态调度线程。 - 内核管理的线程生命周期更高效,避免用户态频繁创建/销毁。
- 内核根据优先级(
3. 用户态线程池管理(User-Level Thread Pool)
当不使用内核 Workqueue 时(如某些定制化配置),进入用户态线程池管理逻辑:
3.1 唤醒休眠线程
c
while (dispatch_semaphore_signal(&pqc->dpq_thread_mediator)) {
if (!--remaining) return;
}
- 目的 :尝试通过信号量唤醒线程池中休眠的线程。
- 信号量机制 :休眠线程通过
dispatch_semaphore_wait
挂起,信号量signal
会唤醒一个线程。 - 优化:避免创建新线程的开销,优先复用休眠线程。
3.2 处理 Overcommit(超发线程)
c
if (overcommit) {
os_atomic_add2o(dq, dgq_pending, remaining, relaxed);
} else {
os_atomic_cmpxchg2o(dq, dgq_pending, 0, remaining, relaxed);
}
- Overcommit 标志:表示队列允许超出发起线程数限制(如串行队列的特殊处理)。
- 原子操作 :
os_atomic_add2o
:直接增加挂起的线程请求数。os_atomic_cmpxchg2o
:通过 CAS(Compare-and-Swap)确保线程请求数正确。
3.3 动态调整线程请求数
c
do {
can_request = t_count < floor ? 0 : t_count - floor;
if (remaining > can_request) {
remaining = can_request;
}
} while (!os_atomic_cmpxchgvw2o(dq, dgq_thread_pool_size, t_count, t_count - remaining, &t_count, acquire));
- 目标 :根据当前线程池大小(
dgq_thread_pool_size
)和floor
值,计算实际可创建的线程数。 - 策略 :
- 若当前线程数
t_count
小于floor
,不允许创建新线程。 - 否则,可创建
t_count - floor
个线程。
- 若当前线程数
- 原子操作 :通过
os_atomic_cmpxchgvw2o
更新线程池大小,避免竞态条件。
3.4 创建新线程
c
while ((r = pthread_create(pthr, attr, _dispatch_worker_thread, dq))) {
if (r != EAGAIN) (void)dispatch_assume_zero(r);
_dispatch_temporary_resource_shortage();
}
- 入口函数 :
_dispatch_worker_thread
是线程的入口,负责从队列中获取并执行任务。 - 错误处理 :
- 若
pthread_create
返回EAGAIN
(资源不足),调用_dispatch_temporary_resource_shortage
等待后重试。 - 其他错误直接触发断言(
dispatch_assume_zero
)。
- 若
到了这里可以清楚的看到对于全局队列使用_pthread_workqueue_addthreads
开辟线程,对于其他队列使用pthread_create
开辟新的线程。
三、代码流程图
plaintext
开始
│
├─ 初始化根队列 & 记录调试信息
│
├─ 如果使用内核 Workqueue:
│ ├─ 调用 _pthread_workqueue_addthreads 请求内核创建线程
│ └─ 返回
│
├─ 否则(用户态线程池):
│ ├─ 尝试通过信号量唤醒休眠线程
│ ├─ 处理 Overcommit 标志
│ ├─ 动态调整线程请求数(基于线程池大小和 floor)
│ ├─ 循环创建新线程(处理 EAGAIN 错误)
│ └─ 更新线程池状态
│
└─ 结束