GCD(Grand Central Dispatch)是 iOS 和 macOS 上管理多线程任务的常用框架。它提供了高效的并发处理方式,允许开发者更轻松地管理任务和队列。以下是常用的 GCD 功能和方法:
1、Dispatch Queues
GCD 提供了几种类型的队列,用来控制任务的执行顺序和线程。
Main Queue(主队列)
DispatchQueue.main,这是一个串行队列,用于在主线程执行任务。通常用于 UI 更新,因为所有 UI 相关的操作必须在主线程上完成。
DispatchQueue.main.async {
// 在主线程更新 UI
print("更新 UI")
}
实际的使用场景通常为在 sync 异步方法调取数据,返回数据时 UI 无法监听到数据更新,因此通常使用DispatchQueue.main代码块包裹更新的参数,同时参数还需要用@Published标记。
Global Queues(全局并发队列)
通过 DispatchQueue.global() 创建的并发队列,可以用于后台执行耗时操作。全局队列提供了不同的优先级(例如 .userInitiated、.background 等),帮助开发者控制任务的重要性和紧急程度。
DispatchQueue.global(qos: .background).async {
print("在后台执行任务")
}
全局队列是一种共享的并发队列,系统自动管理。它适合进行耗时操作,例如网络请求、文件读取等。全局队列提供不同的优先级(QOS),可以用于控制任务的紧急程度和系统资源的分配:
DispatchQueue.global(qos: .background).async {
// 在后台执行任务,例如网络请求或数据加载
let data = fetchDataFromNetwork()
DispatchQueue.main.async {
// 在主线程上更新 UI
updateUI(with: data)
}
}
在处理耗时操作时使用/不使用全局队列的区别:
1、主线程阻塞:主线程默认用于UI更新。如果耗时任务在主线程执行(没有用 DispatchQueue.global(qos: .background).async),界面会卡顿,用户体验很差,尤其在进行网络请求或大规模数据处理时。
2、并发执行效率:DispatchQueue.global(qos: .background).async 会把任务派发到全局并发队列,能够提高任务的并发处理效率,而不会影响其他系统任务。
3、线程控制:DispatchQueue.global(qos: .background).async 允许开发者指定任务的重要性(如 .background、.userInitiated 等),确保任务按适当的优先级在后台执行。如果不明确指定队列,任务将在默认主线程或自定义的队列上执行,失去对并发控制的优势。
Custom Queues(自定义队列)
使用 DispatchQueue(label: “your.label”) 创建自定义的串行或并发队列。自定义队列允许你更细粒度地控制任务的执行顺序和队列隔离。
let customQueue = DispatchQueue(label: "com.example.customQueue")
customQueue.async {
print("在自定义串行队列中执行")
}
创建自定义队列:DispatchQueue(label: “com.example.customQueue”) 创建了一个串行队列,队列的标签(label)可以用于调试时辨识这个队列。
添加异步任务:customQueue.async 将闭包添加到 customQueue 中,该闭包将在该队列的任务中按顺序执行。
默认情况下,使用 DispatchQueue(label:) 创建的队列是串行队列,即任务是逐个、按顺序依次执行的。而如果你希望自定义队列是并发的,可以这样指定:
let concurrentQueue = DispatchQueue(label: "com.example.concurrentQueue", attributes: .concurrent)
concurrentQueue.async {
print("在自定义并发队列中执行")
}
自定义队列的优势
控制任务的执行顺序:在串行队列中,可以确保任务按顺序依次执行;在并发队列中,可以指定多个任务同时进行。
任务隔离:可以在不同的自定义队列上分派不同的任务,使任务之间互不干扰,便于对任务分类、调试。
避免阻塞主队列:耗时任务可以安排在自定义队列中执行,避免直接占用主队列,保持应用响应性。
使用场景
后台数据处理:如处理图像、网络请求数据解析等。
独立的任务执行:在自定义队列中执行特定任务,避免影响主线程和其他任务。
控制并发性:在并发场景中自定义控制任务的数量和顺序。
2、同步任务(sync)与异步任务(async)
async(异步执行):将任务放到队列后立即返回,不等待任务完成。常用于耗时任务的异步执行。
DispatchQueue.global().async {
// 异步任务,不会阻塞当前线程
print("异步任务")
}
sync(同步执行):将任务放到队列后等待任务完成,再继续执行当前线程的代码。需要注意避免在主线程上使用 sync,可能导致死锁。
DispatchQueue.global().sync {
// 同步任务,阻塞当前线程直到任务完成
print("同步任务")
}
3、Dispatch Group(调度组)
调度组用于管理一组任务,可以在所有任务完成后执行一个特定的操作。常用于并发执行多个异步任务并在它们完成后执行后续任务。
let group = DispatchGroup()
group.enter()
DispatchQueue.global().async {
print("任务 1 完成")
group.leave()
}
group.enter()
DispatchQueue.global().async {
print("任务 2 完成")
group.leave()
}
group.notify(queue: .main) {
print("所有任务完成,进行下一步操作")
}
1、创建调度组:let group = DispatchGroup() 创建了一个 DispatchGroup 对象,用于管理一组异步任务。
2、管理任务的开始和结束:
使用 group.enter() 表示任务开始,告诉调度组有任务要添加。
在异步任务完成后,调用 group.leave(),通知调度组该任务已经完成。
3、添加异步任务:
使用 DispatchQueue.global().async { … } 在全局并发队列上添加异步任务。group.enter() 和 group.leave() 包围的代码块代表一个任务的开始和结束。
4、在任务全部完成后执行操作:
group.notify(queue: .main) { … } 用于在所有任务完成后执行特定操作。notify 方法会监听调度组中的任务是否全部完成,一旦所有任务都调用了 leave(),就会在指定的 queue 上执行闭包里的代码。在这个例子中,我们使用 queue: .main,所以会在主线程上执行闭包。
运行结果示例
任务 1 完成
任务 2 完成
所有任务完成,进行下一步操作
注意:因为任务是异步的,任务 1 和 任务 2 的输出顺序可能会有所不同,但 所有任务完成,进行下一步操作 始终会在两个任务都完成后才执行。
使用场景
并发网络请求:如果需要并发地执行多个网络请求,并在所有请求完成后继续下一步逻辑,可以使用 DispatchGroup。
文件处理:在多个文件的读写操作完成后进行最终合并或处理。
数据处理与分析:并发执行多个耗时的计算任务,并在它们完成后展示结果。
注意事项
每个 enter 必须有对应的 leave,否则 notify 永远不会被触发。
notify 内的代码在所有任务完成后才会执行。
调度组的作用
在没有调度组的情况下,任务仍然是异步的,但调度组的作用不仅仅是为了异步执行任务,而是为了在多个异步任务都完成后执行特定的收尾操作。这个控制能力在某些并发场景中非常重要,尤其是在需要等待多个并发任务完成后再继续后续流程的情况下。
没有调度组的情况
如果你只是简单地在全局队列上运行多个任务,那么这些任务各自独立完成,彼此之间没有协作或等待关系,也不会有“等所有任务完成后才执行下一步”这种逻辑。我们就无法确保所有任务都执行完后再进行后续操作。
DispatchQueue.global().async {
print("任务 1 完成")
}
DispatchQueue.global().async {
print("任务 2 完成")
}
// 此处不能准确等待上面任务完成后再进行操作
print("所有任务完成,进行下一步操作") // 这个操作可能在任务1和任务2完成之前就执行了
在这种情况下,print(“所有任务完成,进行下一步操作”)可能会在任务 1 和任务 2 完成之前就执行了,因为它并没有等这些异步任务完成再执行。
有调度组的情况
通过使用 DispatchGroup,我们可以确保所有异步任务执行完后再进行下一步操作,这样可以确保执行顺序。
let group = DispatchGroup()
group.enter()
DispatchQueue.global().async {
print("任务 1 完成")
group.leave()
}
group.enter()
DispatchQueue.global().async {
print("任务 2 完成")
group.leave()
}
group.notify(queue: .main) {
print("所有任务完成,进行下一步操作") // 确保任务1和任务2都完成后再执行
}
在这个例子中,group.notify 会等待 group.enter 和 group.leave 成对出现,确保所有任务都完成后才执行。这种模式非常适合处理多个并发任务完成后的“收尾”操作,比如更新 UI、存储数据或者进行最终的总结性处理。
4、Dispatch Semaphore(信号量)
信号量用于控制线程访问资源的数量,适合限流、资源管理等场景。例如,可以限制某些代码块并发执行的数量。
let semaphore = DispatchSemaphore(value: 1)
DispatchQueue.global().async {
semaphore.wait() // 请求资源
print("任务 1 开始")
sleep(1)
print("任务 1 结束")
semaphore.signal() // 释放资源
}
DispatchQueue.global().async {
semaphore.wait()
print("任务 2 开始")
sleep(1)
print("任务 2 结束")
semaphore.signal()
}
1、创建信号量:let semaphore = DispatchSemaphore(value: 1) 创建一个信号量,初始值为 1。信号量的初始值决定了允许多少个线程同时访问某个资源。在这里,设置为 1 表示一次只允许一个线程访问。
2、请求资源:semaphore.wait() 用于请求资源。如果信号量的值大于 0,则信号量减 1,当前任务获得资源。如果信号量为 0,则该任务会阻塞,直到其他任务释放资源。
3、释放资源:semaphore.signal() 用于释放资源,信号量的值加 1,并通知等待的线程可以继续执行。每一个 wait() 都应有对应的 signal(),以避免信号量被耗尽导致死锁。
4、控制任务的并发:在这个例子中,信号量的初始值为 1,因此只允许一个任务同时执行。当任务 1 请求资源并开始执行后,任务 2 会被阻塞,直到任务 1 释放资源。
运行结果示例
任务 1 开始
任务 1 结束
任务 2 开始
任务 2 结束
注意:虽然两个任务是并发执行的,但因为信号量的限制,它们会依次执行,确保同一时间只有一个任务在访问资源。
使用场景
限流:限制某些代码块的并发执行数量,避免资源超负荷。例如,限制并发的网络请求数量。
资源管理:在多线程环境中控制对共享资源的访问,确保只有固定数量的线程能够同时使用该资源。
任务执行顺序:确保多个线程按序执行,避免线程竞争引起的数据不一致问题。
注意事项
信号量的初始值通常设置为资源的数量,如果超出该数量的线程访问,信号量会阻塞多余的线程。
每次 wait 必须有对应的 signal,否则会造成死锁。
常见的信号量初始值选择
信号量的初始值决定了允许多少个并发任务同时访问某一资源或执行特定代码块。选择初始值时,要根据具体需求和场景来确定:
初始值为 1:
如果只允许单线程访问某个资源或执行某个代码块(即互斥锁的效果),设为 1。这种情况下,信号量相当于一个“独占锁”,确保一次只有一个任务进入。
示例:避免两个任务同时写入某个共享资源。
let semaphore = DispatchSemaphore(value: 1) // 初始值为1,实现互斥锁
初始值为 N(N > 1):
如果允许最多 N 个线程并发执行,则可以设置初始值为 N。这种方式适合需要限流的场景,例如下载并发限制或数据库连接池限制。
示例:限制最多 3 个并发下载任务。
let semaphore = DispatchSemaphore(value: 3) // 允许最多3个任务并行
如何选择合适的值
单一资源保护:如果是保护单个资源,如文件、数据库等的写操作,通常用 1。这样多个任务按顺序访问,避免冲突。
多资源限流:如果多个任务共享一些有限的资源(例如网络连接、数据库连接),则信号量可以设置为资源的数量限制,如 3 或 5。
限制并发下载任务
假设我们需要限制并发下载的任务数为 3:
let semaphore = DispatchSemaphore(value: 3) // 允许最多3个下载任务同时进行
for i in 1...10 {
DispatchQueue.global().async {
semaphore.wait() // 请求资源
print("下载任务 \(i) 开始")
sleep(2) // 模拟下载任务
print("下载任务 \(i) 结束")
semaphore.signal() // 释放资源
}
}
在这个例子中,最多允许 3 个下载任务并发进行,超过 3 个的任务会等待,直到有任务释放资源。
5、Dispatch Barrier(栅栏)
栅栏用于确保在并发队列中,某个特定的任务完成后才开始后续任务,常用于读写操作的隔离,确保写操作的线程安全。
let concurrentQueue = DispatchQueue(label: "com.example.concurrentQueue", attributes: .concurrent)
concurrentQueue.async {
print("读取数据 1")
}
concurrentQueue.async {
print("读取数据 2")
}
// 栅栏任务,确保前面的读操作完成后再执行写操作
concurrentQueue.async(flags: .barrier) {
print("写入数据")
}
concurrentQueue.async {
print("读取数据 3")
}
1、并发队列:DispatchQueue(label: “com.example.concurrentQueue”, attributes: .concurrent) 定义了一个并发队列 concurrentQueue。并发队列允许多个任务同时执行,适合执行大量的读操作。
2、读任务:前两个 concurrentQueue.async 是并发读取数据任务,它们会同时执行,不互相阻塞。这种并发读操作非常适合在不改变数据的场景中提高效率。
3、栅栏任务:concurrentQueue.async(flags: .barrier) 定义了一个带有栅栏标志的任务。当这个栅栏任务到达时,会等待队列中之前所有的任务完成(在此示例中,等待前两个读取任务完成),然后执行栅栏任务(写入数据)。在栅栏任务执行期间,后续的并发任务会被阻塞。
4、后续任务:在栅栏任务完成后,后续的任务(读取数据 3)才会继续执行。这样就确保了写任务的隔离性和线程安全。运行结果示例
读取数据 1
读取数据 2
写入数据
读取数据 3
实际上,前两个读任务是可以并行完成的,写任务会在所有读任务完成后独立执行,并确保其完成后,后续任务才继续。
使用场景
读写隔离:在并发环境中,多个线程可以同时读取数据,但写入操作需要单独进行,避免数据不一致。
线程安全的数据访问:避免多个线程对同一数据的并发写操作,通过栅栏确保写操作的唯一性。
资源控制:在需要严格顺序的读写任务中,确保在特定任务(如数据写入、删除等)完成后才允许其他操作。
注意事项
栅栏任务仅在并发队列上有效,在串行队列中没有实际作用。
在全局并发队列(DispatchQueue.global())上使用栅栏不会生效。要使用栅栏特性,需要自己创建自定义并发队列。
6、Dispatch After(延迟执行)
延迟执行允许在指定时间后执行任务,用于实现延迟操作的效果。
DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
print("2 秒后执行")
}
DispatchQueue.main:表示主队列,通常用于处理UI更新。这样可以确保延迟任务在主线程上执行,尤其适合在UI更新的场景下使用。
.asyncAfter(deadline: .now() + 2.0):这个方法表示在当前时间基础上延迟2秒后,执行代码块 { } 中的任务。
延迟时间:这里的 2.0 表示延迟2秒。你可以将这个数字更改为任意你需要的时间间隔(以秒为单位)。
典型使用场景
1、延迟显示消息或提示框:在用户操作后延迟显示一些信息,避免马上出现造成的干扰。
2、动画效果:在动画效果中,延迟执行有助于协调不同动画的起止时间。
3、网络请求后的UI更新:网络请求完成后,有时需要等待一会儿再更新UI,比如显示”操作成功”的提示。
7、Dispatch Once(单次执行)
Swift 已移除了 dispatch_once,但可以通过创建一个 static 变量和一个标记来模拟单次执行的效果。适用于初始化操作。
class MyClass {
static let shared = MyClass()
private init() {
print("初始化只执行一次")
}
}
1、static let shared = MyClass():静态属性 shared 是类的唯一实例。static 保证了该属性在程序的生命周期内只会被初始化一次,并且所有对 shared 的引用都是同一个实例。
2、私有构造方法 private init():将 init 方法标记为 private,可以防止在类外部直接调用构造方法 MyClass() 创建新的实例,确保只能通过 MyClass.shared 来访问实例。
使用方式
每次调用 MyClass.shared,返回的都是同一个 MyClass 实例。例如:
let instance1 = MyClass.shared
let instance2 = MyClass.shared
// 输出 "true",因为 instance1 和 instance2 指向同一个实例
print(instance1 === instance2)
适用场景
配置管理:如应用的配置设置、网络管理类、日志系统等。
共享资源:如数据库连接、文件系统、缓存管理等。
总结
GCD 的常用方式包括:
主队列(DispatchQueue.main)和全局队列(DispatchQueue.global())的异步任务。
同步任务、异步任务、调度组、信号量、栅栏操作、延迟执行等,用于管理任务的执行顺序和队列隔离。
GCD 提供了多种方式来高效地管理并发操作,从主线程 UI 更新、并发任务管理到资源控制和延迟执行等。不同的 GCD 功能模块组合使用,可以有效提高应用性能、响应性和线程安全性。
GCD 的使用建议:
UI 操作使用主队列:确保所有 UI 更新都在主队列上执行。
耗时操作使用全局队列:避免阻塞主线程,以提高用户体验。
调度组 和 信号量:适用于并发任务管理和资源控制,确保任务协调与线程安全。
栅栏 和 延迟执行:确保数据读写的安全性、以及实现定时任务等效果。