Swift 现代并发模型解析

2,009 阅读6分钟
  • 前言

Swift 提供了异步和并行代码的结构化支持。异步代码可以暂停并稍后恢复,使程序在执行长时间任务时仍能处理短期操作,如更新 UI。并行代码则允许多段代码同时运行,比如在四核处理器上同时执行四个任务。

如果你以前编写过并发代码,可能会习惯于使用线程。Swift 的并发模型基于线程,但你不需要直接与线程交互。在 Swift 中,异步函数可以释放它正在运行的线程,这样另一个异步函数就可以在该线程上运行,而第一个函数则处于阻塞状态。当异步函数恢复执行时,Swift 不保证在哪个线程运行

  • 异步函数

    异步函数是一种特殊的函数,它在执行过程中可以被挂起(不会阻塞当前线程)。它可以在等待某些事情发生时暂停。在异步函数体内,你需要标记可能发生暂停的地方。

func saveImageFromNetwork() async throws {
    let urlString = "https://cdn.arstechnica.net/wp-content/uploads/2018/06/macOS-Mojave-Dynamic-Wallpaper-transition.jpg"
    guard let url = URL(string: urlString) else {
      return
    }
    let data = try await fetchImageFromNetwork(url: url)
    guard let fileURL = getPicturesFilePath(fileName: "download.jpg") else {
      return
    }
    guard !checkIfTheFileExist(filePath: fileURL.path) else {
      return
    }
    try await saveImageToDisk(fileURL: fileURL, data: data)
}

考察例程saveImageFromNetwork() async throws, 这是一个可以抛出异常的异步函数。

这个异步函数可能得执行过程如下

  1. 首先这段代码会从开始执行到第一个 await 。 这里会调用另外一个异步函数 fetchImageFromNetwork 。此时当前异步函数会中断,等待 fetchImageFromNetwork 返回。

  2. 当前执行点被挂起的时候,其他的并发代码会运行。

  3. fetchImageFromNetwork 返回时,代码会从中断点处继续顺序往下执行。它将从网络获取的ImageData 赋值给局部变量 data

  4. getPicturesFilePath 和 checkIfTheFileExist 是同步函数,这里没有中断点。

  5. 下一个 await 标记了对 downloadPhoto(named:) 函数的调用。此代码再次暂停执行,直到该函数返回,从而给其他并发代码一个运行的机会。

代码中标记为 await 的可能挂起点表示当前代码可能在等待异步函数返回时暂停执行。这时Swift 会暂停当前线程上的代码执行,并在该线程上运行其他代码。这又被称为让出线程。使用Task.yield()可以主动让出线程。

注意: 可以调用异步函数的地方

  1. 异步函数可以调用异步函数
  2. 非结构化任务
  3. 标记为 @main 的结构体、类或枚举的静态 main() 方法中的代码。
  • 异步序列

Swift 的异步序列(Asynchronous Sequences)是 Swift Concurrency 模型的一部分,用于处理异步数据流。异步序列允许我们逐步接收异步数据,类似于同步序列,但数据的生成是异步的。

struct Counter: AsyncSequence {
    typealias Element = Int
    let howHigh: Int


    struct AsyncIterator: AsyncIteratorProtocol {
        let howHigh: Int
        var current = 1


        mutating func next() async throws -> Int? {
            guard current <= howHigh else {
                return nil
            }
            if Task.isCancelled {
                throw CancellationError()
            }

            try await Task.sleep(nanoseconds: 1_000_000_000)


            let result = current
            current += 1
            return result
        }
    }

    func makeAsyncIterator() -> AsyncIterator {
        return AsyncIterator(howHigh: howHigh)
    }
}

//调用
let counter = Counter(howHigh: 10)
Task {
  for try await value in counter {
      print(value)
  }
}

  • 并发调用异步函数

await: 当后续代码依赖于异步函数的结果时,可以使用 await 调用异步函数。这种方式创建的工作是顺序执行的。

async let: 当你不需要立即获取结果时,可以使用 async-let 调用异步函数。这种方式创建的工作可以并行进行。

  • 串行版本
let data1 = await fetchData(from: url1)
let data2 = await fetchData(from: url2)
let data3 = await fetchData(from: url3)
let dataArray = [data1, data2, data3]
  • 并发版本
async let data1 = fetchData(from: url1)
async let data2 = fetchData(from: url2)
async let data3 = fetchData(from: url3)
// 这里需要等待这三个异步任务的结果
let dataArray = await [data1, data2, data3]
  • 结构化并发

Task: 任务是程序中可以异步运行的工作单元。所有的异步代码都是作为某个任务的一部分运行的。一个任务本身一次只做一件事,但当你创建多个任务时,Swift 可以调度它们同时运行。

除了使用async let隐式使用并发,你还可以显示创建TaskGroup并添加子任务到这个组,它赋予你更多的优先级和取消控制,并让你创建动态数量的任务。

给定任务组中的每个任务都有相同的父任务,每个任务都可以有子任务。由于任务和任务组之间的明确关系,这种方法被称为结构化并发

改造上节例程

let dataArray = await withTaskGroup(of: String.self) { group in
    //添加父任务
    let urls = [url1, url2, url3]
    for url in urls {
        //添加子任务
        group.addTask {
            return await self.fetchData(from: url)
        }
    }
    var results: [String] = []
    for await data in group {
        results.append(data)
    }
    return results
}

以上代码创建了一个TaskGroup,并给这个组添加了三个获取数据的子任务,这三个子任务会并行执行,然后从group(异步序列)收集数据存入数组返回。

  • 非结构化并发

非结构化任务没有父任务。我们可以使用Task.detachedTask.init创建非结构化任务。你可以自己多次调用非结构化任务来实现非结构化并发。

创建一个非结构化任务:

//创建任务
let data = Task {
    await fetchData(from:"www.apple.com")
}
//等待任务执行完毕
let result = await data.value
  • 取消任务

TaskTaskGroup支持用户取消,这使得用户不用等所有任务完成,任务需要做检查任务取消状态并且暂停当前任务。我们有两个方法检查取消状态,在任务中间调用Task.checkCancellation()Task.isCancelled。如果任务被取消,使用Task.checkCancellation()会使任务抛出异常停止任务,并且能够停止所有任务。你如果想在取消的时候做一些清理工作使用Task.isCancelled

func cancelTask() async {
    let task = Task { ()-> String? in
        let data = await fetchData(from: url)
        if Task.isCancelled {//如果任务被取消返回空
            return nil
        }
        return data
    }
    //模拟取消
    task.cancel()
    let data = await task.value
    print(data ?? "empty")
}
let dataArray = await withTaskGroup(of: Optional<String>.self) { group in
    for url in [url1,url2,url3] {
        //如果任务没有被取消,添加成功
        let added = group.addTaskUnlessCancelled {
            guard !Task.isCancelled else {//任务被取消,返回空值
                return nil
            }
            return await self.fetchData(from: url)
        }
        guard added else { break }
    }

    var results: [String] = []
    for await data in group {
        if let data {// 过滤空值
            results.append(data)
        }
    }
    return results
}
  • 结语

Swift 提供了强大的异步和并行编程模型,简化了在现代应用程序中编写高效并发代码的过程。通过异步函数和并行执行,程序可以在处理长时间任务的同时保持对用户界面的响应。Swift 的并发模型使得我们可以专注于任务逻辑,而无需直接处理线程的复杂性。

通过这些特性,我们可以更轻松地编写高性能、高可靠性的应用程序,确保在处理繁重任务时,仍能保持流畅的用户体验。随着 Swift 并发特性的不断完善和发展,我们可以期待在未来的项目中实现更高效、更安全的并发编程。

希望本文对您理解和应用 Swift 的并发模型有所帮助。有疑问请不吝笔墨。