Swift并发编程总结:async await Task

148 阅读6分钟
  • 本文适合有一定Swift并发编程经验的同学
  • 这里的并发不包括GCD、操作队列,仅仅包含Swift特有的并发技术
  • 要学习Swift的并发模型,首先要理解Swift并发编程的哲学思想,其次要俯瞰整个Swift并发编程的技术模型。
  • 这里“哲学”我们可以理解为在解决什么问题。这里的技术模型是指它在技术上的设计思路是什么。

一、简单回顾Swift并发用法

异步编程的例子

func test() async -> Int {
    try? await Task.sleep(nanoseconds: 1_000_000_000 * 1)
    return 100
}

Task {
    let result = await test() // 在Task内可以使用await
    print(result)
}

func test2() async -> Int {
    try? await Task.sleep(nanoseconds: 1_000_000_000 * 1)
    return await test() // 在async函数中可以使用await
}

// let r = await test() 编译器报错,没有在Task或异步上下文中不能调用await

回忆起并发用法了吗?

二、Swift并发哲学(问题)

1. 代码复杂度高,可读性差。

1.1. 典型代表回调地狱。只看下面红色代码部分

// 模拟一个网络请求,返回用户数据
func fetchUserData(completion: @escaping (String) -> Void) {
    DispatchQueue.global().async {
        sleep(1)  // 模拟延迟
        completion("User123")  // 返回用户ID
    }
}

// 获取用户设置
func fetchUserSettings(userID: String, completion: @escaping (String) -> Void) {
    DispatchQueue.global().async {
        sleep(1)  // 模拟延迟
        completion("Settings for (userID)")  // 返回设置
    }
}

// 获取用户偏好设置
func fetchUserPreferences(settings: String, completion: @escaping (String) -> Void) {
    DispatchQueue.global().async {
        sleep(1)  // 模拟延迟
        completion("Preferences for (settings)")  // 返回偏好设置
    }
}

// 回调地狱:调用这些函数并处理回调
fetchUserData { userID in
    fetchUserSettings(userID: userID) { settings in
        fetchUserPreferences(settings: settings) { preferences in
            print("User preferences: (preferences)")
        }
    }
}

1.2. Swift怎么解决的?

// 模拟一个异步任务,获取用户数据
func fetchUserData() async -> String {
    try? await Task.sleep(nanoseconds: 1 * 1_000_000_000)  // 模拟延迟
    return "success"
}

// 获取用户设置
func fetchUserSettings(userID: String) async -> String {
    try? await Task.sleep(nanoseconds: 1 * 1_000_000_000)  // 模拟延迟
    return "success"
}

// 获取用户偏好设置
func fetchUserPreferences(settings: String) async -> String {
    try? await Task.sleep(nanoseconds: 1 * 1_000_000_000)  // 模拟延迟
    return "success"
}

// 使用 async/await 执行这些任务并简化错误处理
Task {
    let userID = await fetchUserData()
    let settings = await fetchUserSettings(userID: userID)
    let preferences = await fetchUserPreferences(settings: settings)
}

用串行代码的书写方式执行异步任务

2. 错误处理冗长

2.1. 在回调地狱中处理错误。要想学习Swift并发,就必须看懂下面有问题的代码。如果不知道新的并发架构在解决什么问题,也就不能很好的理解后面的模型为什么会设计成这样

// 模拟一个异步任务,获取用户数据
func fetchUserData(completion: @escaping (Result<String, Error>) -> Void) {
    DispatchQueue.global().async {
        sleep(1)  // 模拟延迟
        let success = true  // 假设成功
        if success {
            completion(.success("User123"))
        } else {
            completion(.failure(MyError.networkError))
        }
    }
}

// 获取用户设置
func fetchUserSettings(userID: String, completion: @escaping (Result<String, Error>) -> Void) {
    DispatchQueue.global().async {
        sleep(1)  // 模拟延迟
        let success = true  // 假设成功
        if success {
            completion(.success("Settings for (userID)"))
        } else {
            completion(.failure(MyError.networkError))
        }
    }
}

// 获取用户偏好设置
func fetchUserPreferences(settings: String, completion: @escaping (Result<String, Error>) -> Void) {
    DispatchQueue.global().async {
        sleep(1)  // 模拟延迟
        let success = true  // 假设成功
        if success {
            completion(.success("Preferences for (settings)"))
        } else {
            completion(.failure(MyError.networkError))
        }
    }
}

// 错误类型
enum MyError: Error {
    case networkError
}

// 调用这些函数并处理回调,层层嵌套
fetchUserData { result in
    switch result {
    case .success(let userID):
        fetchUserSettings(userID: userID) { result in
            switch result {
            case .success(let settings):
                fetchUserPreferences(settings: settings) { result in
                    switch result {
                    case .success(let preferences):
                        print("User preferences: (preferences)")
                    case .failure(let error):
                        print("Failed to fetch preferences: (error)")
                    }
                }
            case .failure(let error):
                print("Failed to fetch settings: (error)")
            }
        }
    case .failure(let error):
        print("Failed to fetch user data: (error)")
    }
}

2.2. Swift怎么解决的?

// 模拟一个异步任务,获取用户数据
func fetchUserData() async throws -> String {
    await Task.sleep(1 * 1_000_000_000)  // 模拟延迟
    let success = true  // 假设成功
    if success {
        return "User123"
    } else {
        throw MyError.networkError
    }
}

// 获取用户设置
func fetchUserSettings(userID: String) async throws -> String {
    await Task.sleep(1 * 1_000_000_000)  // 模拟延迟
    let success = true  // 假设成功
    if success {
        return "Settings for (userID)"
    } else {
        throw MyError.networkError
    }
}

// 获取用户偏好设置
func fetchUserPreferences(settings: String) async throws -> String {
    await Task.sleep(1 * 1_000_000_000)  // 模拟延迟
    let success = true  // 假设成功
    if success {
        return "Preferences for (settings)"
    } else {
        throw MyError.networkError
    }
}

// 使用 async/await 执行这些任务并简化错误处理
Task {
    do {
        let userID = try await fetchUserData()
        let settings = try await fetchUserSettings(userID: userID)
        let preferences = try await fetchUserPreferences(settings: settings)
        print("User preferences: (preferences)")
    } catch {
        print("Error: (error)")
    }
}

用串行代码的书写方式执行异步任务,代码复杂度低,便于阅读和维护。

2.3. 数据竞争问题频发出现

可以理解为数据的线程安全问题,老的解决方案是加锁,但容易造成死锁,新的解决方案是actor。

但我认为新的解决方案也不完美,而且在实战中很难得到应用,因为它依赖于async await。如果要用,改造成本太高。

2.4. 小结

最主要的是几个个问题,其他问题都不大,就不再讲了。

这里还要说一下,Swift并发解决了这两个问题,并且提供了新的能力

如果你没有看明白Swift是怎么解决问题的,说明你对await不够理解:

await:表示在这个点 可能会非阻塞式 挂起 当前任务,等待结果,并在之后继续执行

很多人对上面提到的“非阻塞”,还“挂起”有疑问?

  • 不阻塞怎么挂起?
  • 挂起了当然就阻塞了,怎么可能挂起了还非阻塞

要回答这个问题要从一个现象上说起:在await调用前和调用后分别打印当前线程的id,你会发现线程id有可能不一样,说明任务有可能在两个线程里完成。为什么会出现这样的现象?原因是:

  • 怎么挂起:await调用函数,就导致暂停执行当前任务A(非暂停线程),Swift并发框架将任务A和任务A的上下文打包到一个队列里。
  • 非阻塞:任务A放到队列后,当前线程继续处理其他任务。当任务A返回(可能是io),空闲线程拿出任务A,恢复任务的上下文,任务A的代码继续执行。

如何还是不明白可以可以看看前面我发的文章:await是怎么实现非阻塞式挂起的

三、Swift并发技术模型

上面聊了Swift并发解决的问题,但一个完整的并发框架只解决问题不行,还要提供完整的能力。所以在讨论模型之前我们首先聊一下并发框架需要提供哪些能力

3.1 并发能力

任务组(TaskGroup):用于并行执行多个相关联的异步任务,并收集它们的结果。

3.2 模型

未完待续,没有写下去的动力,有想看的,下面留言,人多了,再继续总结。