MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Swift异步编程基础与GCD使用指南

2024-09-295.3k 阅读

Swift异步编程基础

异步编程的概念

在软件开发中,尤其是在处理用户界面、网络请求、文件读写等操作时,异步编程扮演着至关重要的角色。同步编程意味着代码按照顺序依次执行,只有当前一个任务完成后,下一个任务才会开始。这在处理耗时操作时会导致界面卡顿,因为主线程被阻塞,无法响应用户交互。

而异步编程允许程序在执行耗时任务时,不阻塞主线程,从而保持应用的响应性。例如,当进行网络请求获取数据时,异步编程可以让主线程继续处理用户的触摸事件、更新界面等操作,而不是等待网络请求完成。

为什么在Swift中使用异步编程

  1. 提升用户体验:在iOS应用开发中,主线程主要负责处理用户界面的更新和交互。如果在主线程执行长时间运行的任务,如网络请求或复杂的计算,会导致界面冻结,用户无法进行任何操作。通过异步编程,将这些耗时任务放在后台线程执行,主线程可以保持流畅,用户可以继续与应用交互,大大提升了用户体验。
  2. 提高资源利用率:现代计算机通常具有多个核心,异步编程可以充分利用这些多核资源。不同的任务可以并行执行,加快整个应用的处理速度,提高系统资源的利用率。

异步任务的类型

  1. I/O 操作:例如文件读取、网络请求等。这些操作通常需要等待外部设备的响应,耗时较长。在Swift中,处理文件读取可以使用异步方式,避免阻塞主线程。
let fileURL = URL(fileURLWithPath: "path/to/your/file.txt")
let task = FileManager.default.contentsOfDirectory(at: fileURL, includingPropertiesForKeys: nil, options: [], errorHandler: { url, error in
    // 处理错误
}) { urls in
    // 处理读取到的文件URL数组
}
  1. 计算密集型任务:像复杂的数学计算、图像渲染等。虽然这些任务不依赖外部设备,但计算量较大,如果在主线程执行会影响应用的响应性。例如,进行大量数据的排序操作可以放在后台线程执行。
DispatchQueue.global(qos:.userInitiated).async {
    let largeArray = (1...1000000).map { $0 }
    let sortedArray = largeArray.sorted()
    // 将排序结果传递回主线程更新UI
    DispatchQueue.main.async {
        // 更新UI操作
    }
}

GCD(Grand Central Dispatch)基础

GCD 简介

GCD是Apple开发的一种用于在iOS、macOS等系统上进行并发编程的技术。它基于队列(queue)的概念,将任务(block)提交到队列中执行。GCD的优势在于它是基于队列的,简化了多线程编程的复杂性,开发者不需要手动管理线程的创建、销毁和同步等操作。

GCD 队列

  1. 串行队列(Serial Queue):串行队列中的任务按照先进先出(FIFO)的顺序依次执行。同一时间只有一个任务在执行,前一个任务完成后,下一个任务才会开始。可以通过DispatchQueue(label: "com.example.serialQueue")来创建一个自定义的串行队列。
let serialQueue = DispatchQueue(label: "com.example.serialQueue")
serialQueue.async {
    // 任务1
    print("Task 1 in serial queue")
}
serialQueue.async {
    // 任务2
    print("Task 2 in serial queue")
}

在上述代码中,Task 1会先执行,完成后Task 2才会执行。

  1. 并发队列(Concurrent Queue):并发队列中的任务也按照FIFO的顺序进入队列,但多个任务可以同时执行,前提是系统资源允许。系统提供了四个标准的并发队列,根据服务质量(QoS)分为:.userInteractive.userInitiated.default.background。可以通过DispatchQueue.global(qos:.userInitiated)获取一个指定QoS的全局并发队列。
let concurrentQueue = DispatchQueue.global(qos:.userInitiated)
concurrentQueue.async {
    // 任务A
    print("Task A in concurrent queue")
}
concurrentQueue.async {
    // 任务B
    print("Task B in concurrent queue")
}

在上述代码中,Task ATask B可能会同时执行(取决于系统资源)。

  1. 主队列(Main Queue):主队列是一个特殊的串行队列,它与应用的主线程相关联。在主队列中执行的任务会在主线程中运行。由于主线程主要用于处理用户界面更新等操作,所以在主队列中执行的任务应该尽量简短,避免阻塞主线程。可以通过DispatchQueue.main获取主队列。
DispatchQueue.main.async {
    // 更新UI操作
    let label = UILabel(frame: CGRect(x: 100, y: 100, width: 200, height: 50))
    label.text = "Hello, World!"
    view.addSubview(label)
}

GCD 任务提交

  1. 异步提交(async):使用async方法提交任务到队列时,任务会立即返回,不会阻塞当前线程。提交的任务会在指定的队列中异步执行。例如:
DispatchQueue.global(qos:.default).async {
    // 耗时操作,如网络请求
    let data = try? Data(contentsOf: URL(string: "https://example.com/api/data")!)
    // 将数据处理结果传递回主线程
    DispatchQueue.main.async {
        // 更新UI显示数据
    }
}
  1. 同步提交(sync):使用sync方法提交任务到队列时,当前线程会等待任务在指定队列中执行完成后才会继续执行。这可能会导致死锁,尤其是在主队列中同步提交任务到主队列时。例如:
let queue = DispatchQueue(label: "com.example.queue")
queue.sync {
    // 任务
    print("Task in sync")
}
print("After sync task")

在上述代码中,print("After sync task")会在任务完成后才执行。

GCD 在Swift异步编程中的应用

异步网络请求

在iOS应用开发中,网络请求是常见的异步操作。使用GCD可以方便地处理网络请求,避免阻塞主线程。例如,使用URLSession进行网络请求:

let url = URL(string: "https://example.com/api/data")!
let task = URLSession.shared.dataTask(with: url) { data, response, error in
    guard let data = data, error == nil else {
        // 处理错误
        return
    }
    // 在后台线程处理数据
    let json = try? JSONSerialization.jsonObject(with: data, options: [])
    // 将处理结果传递回主线程更新UI
    DispatchQueue.main.async {
        // 更新UI显示数据
    }
}
task.resume()

在上述代码中,网络请求在后台线程执行,获取到数据后,在后台线程进行初步处理(如解析JSON),然后将处理结果传递回主线程更新UI。

异步文件操作

  1. 文件读取:读取大文件时,异步操作可以避免阻塞主线程。
let fileURL = URL(fileURLWithPath: "path/to/your/largeFile.txt")
DispatchQueue.global(qos:.background).async {
    do {
        let data = try Data(contentsOf: fileURL)
        let string = String(data: data, encoding:.utf8)
        // 将读取结果传递回主线程处理
        DispatchQueue.main.async {
            // 更新UI显示文件内容
        }
    } catch {
        // 处理错误
    }
}
  1. 文件写入:同样,写入文件也可以异步进行。
let fileURL = URL(fileURLWithPath: "path/to/your/newFile.txt")
let content = "This is some text to write".data(using:.utf8)!
DispatchQueue.global(qos:.background).async {
    do {
        try content.write(to: fileURL)
        // 写入完成后在主线程提示用户
        DispatchQueue.main.async {
            // 显示写入成功提示
        }
    } catch {
        // 处理错误
    }
}

任务依赖

GCD允许设置任务之间的依赖关系。通过DispatchWorkItem可以创建任务,并设置其依赖于其他任务。例如:

let queue = DispatchQueue(label: "com.example.queue")
let task1 = DispatchWorkItem {
    print("Task 1")
}
let task2 = DispatchWorkItem {
    print("Task 2")
}
task2.addDependency(task1)
queue.async(execute: task1)
queue.async(execute: task2)

在上述代码中,task2会在task1完成后才执行。

组队列(Dispatch Group)

  1. 概念:Dispatch Group用于等待一组任务完成。可以将多个任务添加到组中,然后等待组中的所有任务都执行完毕后再执行后续操作。
  2. 示例:假设有多个网络请求,需要在所有请求都完成后进行数据合并和处理。
let group = DispatchGroup()
let urls = [URL(string: "https://example.com/api/data1"), URL(string: "https://example.com/api/data2"), URL(string: "https://example.com/api/data3")]
var allData: [Data] = []
for url in urls {
    guard let url = url else { continue }
    group.enter()
    URLSession.shared.dataTask(with: url) { data, response, error in
        defer { group.leave() }
        guard let data = data, error == nil else {
            // 处理错误
            return
        }
        allData.append(data)
    }.resume()
}
group.notify(queue: DispatchQueue.main) {
    // 所有请求完成后,在主线程处理合并后的数据
    let combinedData = allData.reduce(Data()) { $0 + $1 }
    // 进一步处理合并后的数据
}

在上述代码中,通过group.enter()将任务加入组中,任务完成后通过group.leave()表示任务完成。group.notify用于在所有任务完成后执行指定的操作。

信号量(Dispatch Semaphore)

  1. 概念:Dispatch Semaphore是一种计数信号量,用于控制对共享资源的访问。它有一个初始计数,当信号量的计数大于0时,可以获取信号量(计数减1),当计数为0时,获取信号量的操作会阻塞,直到其他地方释放信号量(计数加1)。
  2. 示例:假设有一个共享资源(如数据库连接),同时只允许一定数量的线程访问。
let semaphore = DispatchSemaphore(value: 3) // 初始计数为3,允许同时3个线程访问
let queue = DispatchQueue(label: "com.example.concurrentQueue", attributes:.concurrent)
for _ in 0..<10 {
    queue.async {
        semaphore.wait() // 获取信号量
        // 访问共享资源
        print("Accessing shared resource")
        // 模拟访问时间
        Thread.sleep(forTimeInterval: 1)
        semaphore.signal() // 释放信号量
    }
}

在上述代码中,由于信号量初始值为3,所以最多同时有3个任务可以访问共享资源。其他任务会在semaphore.wait()处等待,直到有信号量被释放。

高级GCD技术

调度源(Dispatch Source)

  1. 概念:Dispatch Source用于监听系统事件,如文件描述符的变化、定时器事件、信号等。当事件发生时,Dispatch Source会将一个任务提交到指定的队列中执行。
  2. 定时器示例:使用Dispatch Source创建一个定时器。
let timer = DispatchSource.makeTimerSource(flags: [], queue: DispatchQueue.main)
timer.schedule(deadline:.now() + 1, repeating: 1) // 1秒后开始,每隔1秒重复
timer.setEventHandler {
    print("Timer fired")
}
timer.resume()

在上述代码中,DispatchSource.makeTimerSource创建了一个定时器源,schedule方法设置了定时器的起始时间和重复间隔,setEventHandler设置了定时器触发时执行的任务。

栅栏函数(Dispatch Barrier)

  1. 概念:在并发队列中,栅栏函数可以确保在它之前提交的任务都执行完毕后,才执行栅栏函数中的任务,并且在栅栏函数执行完毕后,才继续执行后续提交的任务。这对于处理共享资源的读写操作非常有用,例如在进行数据写入时,防止其他线程同时进行读取操作。
  2. 示例:假设有一个并发队列用于处理数据读写,使用栅栏函数保证数据写入的原子性。
let concurrentQueue = DispatchQueue(label: "com.example.concurrentQueue", attributes:.concurrent)
var data = [Int]()
// 读取数据任务
concurrentQueue.async {
    print("Reading data: \(data)")
}
// 栅栏函数进行数据写入
concurrentQueue.async(flags:.barrier) {
    data.append(1)
    print("Data written")
}
// 读取数据任务
concurrentQueue.async {
    print("Reading data: \(data)")
}

在上述代码中,栅栏函数保证了数据写入操作的原子性,避免了读取操作在写入未完成时获取到不一致的数据。

自定义队列优先级

虽然系统提供了不同QoS的全局并发队列,但有时我们需要自定义队列的优先级。可以通过设置队列的qualityOfService属性来实现。

let highPriorityQueue = DispatchQueue(label: "com.example.highPriorityQueue", qos:.userInteractive)
let lowPriorityQueue = DispatchQueue(label: "com.example.lowPriorityQueue", qos:.background)
highPriorityQueue.async {
    // 高优先级任务
    print("High priority task")
}
lowPriorityQueue.async {
    // 低优先级任务
    print("Low priority task")
}

在上述代码中,highPriorityQueue的任务会优先于lowPriorityQueue的任务执行(在系统资源允许的情况下)。

通过深入理解Swift异步编程基础以及GCD的各种技术,开发者可以编写出高效、响应性强的应用程序,充分利用现代多核设备的性能,为用户提供更好的体验。无论是处理网络请求、文件操作还是复杂的任务调度,GCD都提供了强大而灵活的工具来满足各种异步编程需求。在实际开发中,根据具体的应用场景选择合适的队列、任务提交方式以及GCD技术,是实现优秀并发编程的关键。