• 作者:老汪软件技巧
  • 发表时间:2024-09-04 04:02
  • 浏览量:

在上一篇文章中提到,如果在不支持并发的同步调用环境中调用一个异步方法,会造成编译错误。今天这篇文章中,我们将介绍如何通过任务和任务组来创建异步函数的使用环境。

任务

Swift 中的任务是 WWDC 2021 引入的并发框架的一部分。任务允许我们从非并发方法创建并发环境,使用async/await调用方法。

当首次使用任务时,你可能会注意到调度队列和任务之间的相似之处。两者都允许在不同线程上以特定优先级调度工作。然而,任务有着明显的不同之处,并通过简化调度队列的繁琐性使我们的开发变得更加轻松。

任务的创建和执行

创建一个任务:

let basicTask = Task {
    return "This is the result of the task"
}
print(await basicTask.value)

任务也可以抛出错误:

let basicTask = Task {
    // .. perform some work
    throw ExampleError.somethingIsWrong
}
do {
    print(try await basicTask.value)
} catch {
    print("Basic task failed with error: (error)")
}

任务在创建后立即运行,不需要显式启动。因此,只有在需要开始它的工作时才创建任务。

在上一篇文章中描述的 async_let 语法会隐式创建一个子任务。

取消任务

使用 cancel() 来取消任务:

basicTask.cancel()

处理取消任务

checkCancellation() 和 isCancelled 都可以用于检查任务取消,但它们的作用方式有所不同:

Task.checkCancellation():

例子:

Task {
    // ... 一些异步操作
    try Task.checkCancellation() // 检查是否被取消,如果被取消则抛出错误
    // ... 继续执行其他操作 (如果任务没有被取消)
}

2. Task.isCancelled:

例子:

Task {
    while !Task.isCancelled { // 循环执行,直到任务被取消
        // ... 执行一些操作
        try await Task.sleep(nanoseconds: 1_000_000_000) // 模拟 1 秒的延迟
    }
    // ... 任务被取消后执行一些清理操作
}

3. 总结:

监听取消任务

_并发模型的优缺点_并发模型有哪些

let task = await Task.withTaskCancellationHandler {
    // ...
} onCancel: {
    print("Canceled!")
}
// ... some time later...
task.cancel()  // Prints "Canceled!"

设置优先级

每个任务都有其优先级,类似于使用任务队列时的QOS,代码如下:

任务状态

Swift 中的 Task 有多种状态,反映了其生命周期中的不同阶段。 可以使用 Task.State 枚举类型来访问这些状态。

任务组

Swift 中的任务组用来组合多个并行任务,并等待所有任务完成后返回结果。可以将任务组看作一个包含多个动态添加的子任务的容器。子任务可以并行或串行执行,但是只有在其子任务完成后,Task Group才会被标记为完成。

如何使用任务组

使用withTaskGroup(of:returning:body:)或withThrowingTaskGroup(of:returning:body:)函数来创建任务组。添加到任务组中的所有子任务都会自动并发的开始执行。

使用任务组,返回最终的结果集,例子如下:

let images = await withTaskGroup(of: UIImage.self, returning: [UIImage].self) { taskGroup in
    let photoURLs = await listPhotoURLs(inGallery: "Amsterdam Holiday")
    for photoURL in photoURLs {
        taskGroup.addTask { await downloadPhoto(url: photoURL) }
    }
    return await taskGroup.reduce(into: [UIImage]()) { partialResult, name in
        partialResult.append(name)
    }
}

2. 错误处理, 例子如下:

let images = try await withThrowingTaskGroup(of: UIImage.self, returning: [UIImage].self) { taskGroup in
    let photoURLs = try await listPhotoURLs(inGallery: "Amsterdam Holiday")
    for photoURL in photoURLs {
        taskGroup.addTask { try await downloadPhoto(url: photoURL) }
    }
    return try await taskGroup.reduce(into: [UIImage]()) { partialResult, name in
        partialResult.append(name)
    }
}

在上面的例子中,如果子任务失败,任务组不会失败;为了在子任务失败时,任务组也失败,需要在迭代的时候使用next()来代替while reduce(),代码如下:

let images = try await withThrowingTaskGroup(of: UIImage.self, returning: [UIImage].self) { taskGroup in
    let photoURLs = try await listPhotoURLs(inGallery: "Amsterdam Holiday")
    for photoURL in photoURLs {
        taskGroup.addTask { try await downloadPhoto(url: photoURL) }
    }
    var images = [UIImage]()
    /// Note the use of `next()`:
    while let downloadImage = try await taskGroup.next() {
        images.append(downloadImage)
    }
    return images
}

next()方法接收来自各个任务的错误,让你可以相应地处理这些错误。在这种情况下,我们将错误传递到组闭包,使整个任务组失败。此时,任何其他正在运行的子任务都将被取消。

避免并发改变

不要在创建 TaskGroup 的任务之外的地方修改它。具体来说,以下行为是不被允许的:

因为并发会导致数据竞争,当多个任务同时修改一个 TaskGroup 时,可能会导致数据不一致,甚至程序崩溃。

取消任务组

你可以使用cancelAll()方法显式地取消任务组中的所有子任务。使用addTaskUnlessCancelled()方法添加的任务,只有在任务组没有被取消的情况下才会开始执行。如果任务组已经被取消,这个任务就不会被添加到任务组中,也不会开始执行。

let photos = await withTaskGroup(of: Optional<Data>.self) { group in
    let photoNames = await listPhotos(inGallery: "Summer Vacation")
    for name in photoNames {
        let added = group.addTaskUnlessCancelled {
            guard !Task.isCancelled else { return nil }
            return await downloadPhoto(named: name)
        }
        guard added else { break }
    }
    var results: [Data] = []
    for await photo in group {
        if let photo { results.append(photo) }
    }
    return results
}

关于如何监听取消的任务,请看任务相关部分的讲解。

参考……