告别回调地狱!Swift 并发编程的新时代

1,348 阅读3分钟

在 iOS 开发中,异步编程一直是一个绕不开的话题。从最初的 Block 回调,到 Combine 框架,再到现在的 async/await,Apple 一步步改进异步编程的方式,使代码更易读、更易维护。今天,我们就来聊聊 async/await 的优势,并深入探讨它如何帮助我们解决并发问题。

1. 回调地狱:异步编程的噩梦

在 iOS 早期,我们使用回调(Block)来处理异步任务,比如网络请求:

func fetchData(completion: @escaping (Result<String, Error>) -> Void) {
    DispatchQueue.global().async {
        // 模拟网络请求
        sleep(2)
        let success = Bool.random()
        if success {
            completion(.success("数据加载成功"))
        } else {
            completion(.failure(NSError(domain: "网络错误", code: -1)))
        }
    }
}

虽然这样可以工作,但当任务依赖增多时,就会形成回调地狱

fetchData { result in
    switch result {
    case .success(let data):
        processData(data) { processedData in
            saveToDatabase(processedData) { success in
                if success {
                    print("数据处理完成")
                }
            }
        }
    case .failure(let error):
        print("发生错误: \(error)")
    }
}

层层嵌套的回调不仅使代码可读性变差,还增加了错误处理的复杂度。Apple 需要一种更优雅的方式。

2. Combine 的改进,但仍然复杂

Combine 通过声明式 API 改进了回调问题,使代码更加流畅:

fetchDataPublisher()
    .map { processData($0) }
    .flatMap { saveToDatabase($0) }
    .sink(receiveCompletion: { completion in
        if case .failure(let error) = completion {
            print("发生错误: \(error)")
        }
    }, receiveValue: { success in
        print("数据处理完成: \(success)")
    })
    .store(in: &cancellables)

但 Combine 仍然需要繁琐的操作符,且新手入门成本较高。

3. async/await:更优雅的异步解决方案

Swift 5.5 引入了 async/await,使异步代码看起来像同步代码:

func fetchData() async throws -> String {
    try await Task.sleep(nanoseconds: 2 * 1_000_000_000)
    if Bool.random() {
        return "数据加载成功"
    } else {
        throw NSError(domain: "网络错误", code: -1)
    }
}

func process() async {
    do {
        let data = try await fetchData()
        print("数据处理完成: \(data)")
    } catch {
        print("发生错误: \(error)")
    }
}

代码逻辑清晰、可读性高,没有回调地狱。

4. 并发问题:为什么需要 actor?

在引入 async/await 之前,我们使用 GCD 和 OperationQueue 进行并发处理。但并发访问共享资源时,可能会发生数据竞争,导致难以复现的 bug。例如:

class Counter {
    var count = 0
    
    func increment() {
        count += 1
    }
}

let counter = Counter()
DispatchQueue.concurrentPerform(iterations: 1000) { _ in
    counter.increment()
}
print(counter.count) // 可能不是 1000,出现数据竞争

原因在于值类型存储在栈上,线程独享;引用类型存储在堆上,多个线程共享,如果多个线程同时修改 count,结果可能是不确定的。

Swift 通过actor 解决了这个问题。

5. actor:解决并发数据竞争

actor 是 Swift 引入的一种并发安全的引用类型,确保同一时间只有一个任务可以访问它的内部状态。

actor SafeCounter {
    private var count = 0
    
    func increment() {
        count += 1
    }
    
    func getCount() -> Int {
        return count
    }
}

let counter = SafeCounter()
Task {
    await counter.increment()
    print(await counter.getCount())
}

actor 内部,Swift 自动确保并发访问安全,使代码更易维护。

6. SwiftUI 中的 Task 和 .task 修饰符

在 SwiftUI 中,我们可以使用 .task 修饰符启动异步任务:

struct ContentView: View {
    @State private var data = "加载中..."
    
    var body: some View {
        Text(data)
            .task {
                do {
                    data = try await fetchData()
                } catch {
                    data = "发生错误"
                }
            }
    }
}

这样,视图在加载时会自动调用 fetchData(),避免使用 onAppear {} 处理异步操作。

7. TaskGroup 处理多个异步任务

如果我们有多个并发任务需要执行,比如并行加载多张图片,可以使用 TaskGroup

func fetchMultipleData() async -> [String] {
    await withTaskGroup(of: String.self) { group in
        for i in 1...3 {
            group.addTask {
                return await fetchData() + "\(i)"
            }
        }
        var results = [String]()
        for await result in group {
            results.append(result)
        }
        return results
    }
}

TaskGroup 允许多个任务并行执行,并在所有任务完成后收集结果。

8. 总结

  • 回调容易导致回调地狱,可读性差

  • Combine 解决了部分问题,但学习成本高

  • async/await 让代码更简洁,解决异步回调问题

  • actor 解决并发数据竞争,保证线程安全

  • SwiftUI .task 让异步任务管理更方便

  • TaskGroup 适用于多个异步任务的并发处理

未来的 Swift 可能会继续演进并发模型,但目前 async/await 和 actor 已经是最佳实践,赶快用起来吧!