Swift 中的 Task

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

第一次处理任务时,您可能会认识到调度队列(dispatch queue)和任务(tasks)之间的相识程度。两者都允许在具有特定优先级的不同线程上分派工作。然而,任务通过消除冗长的调度队列代码,使我们的生活变得相当不同且更轻松。

您可以在我的文章 Swift 中的async/await了解有关 async/await 的更多信息。

如何创建然后运行一个 Task

在 Swift 中创建一个basicTask如下所示:

let basicTask = Task {
    return "This is the result of the task"
}

如您所见,我们保留了对返回字符串值的 basicTask 的引用。我们可以使用引用来读出结果值:

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

此示例返回一个字符串,但也可能引发错误:

let basicTask = Task {
    // .. 做一些工作 ..
    throw ExampleError.somethingIsWrong
}

do {
    print(try await basicTask.value)
} catch {
    print("Basic task failed with error: \(error)")
}

// Prints: Basic task failed with error: somethingIsWrong

换句话说,您可以使用任务来产生错误

如何运行任务

好吧,上面的例子已经给出了本节的答案。任务在创建后会立即运行,不需要显式启动。重要的是要了解需要执行的工作是在任务创建后直接执行的,因为它告诉您仅在允许任务内工作开始时才会创建它。

在任务中执行异步方法

除了同步返回值或抛出错误外,任务还可以执行异步方法。我们需要一个任务来在不支持并发的函数中执行任何异步方法。您可能已经熟悉以下错误:

'async' call in a function that does not support concurrency is a common error in Swift.

不支持并发的函数中的“async”调用是 Swift 中的常见错误。

在此示例中,executeTask 方法是另一个任务的简单包装器:

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

我们可以通过在一个新任务中调用executeTask()方法来解决上述错误。

var body: some View {
    Text("Hello, world!")
        .padding()
        .onAppear {
            Task {
                await executeTask()
            }
        }
}

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

该任务创建了一个并发支持环境,我们可以在其中调用异步方法 executeTask()。有趣的是,即使我们没有在 onappear 方法中保留对已创建任务的引用,我们的代码也会执行,这里来到我下一节要说明的内容:取消任务。

处理取消

在想到处理任务取消时,您可能会惊讶地看到您的任务正在执行,即使您没有保留对它的引用。 Combine 中的发布者订阅要求我们保持强引用以确保发出值。与 Combine 相比,您可能希望在释放所有引用后也取消任务。

但是,Task的工作方式不同,因为无论您是否保留引用,它们都会运行。保留引用的唯一原因是让自己能够等待结果或取消任务。

取消一个任务

为了向您解释任务取消是如何工作的,我们将使用一个加载图像的新代码示例:

struct ContentView: View {
    @State var image: UIImage?

    var body: some View {
        VStack {
            if let image = image {
                Image(uiImage: image)
            } else {
                Text("Loading...")
            }
        }.onAppear {
            Task {
                do {
                    image = try await fetchImage()
                } catch {
                    print("Image loading failed: \(error)")
                }
            }
        }
    }

    func fetchImage() async throws -> UIImage? {
        let imageTask = Task { () -> UIImage? in
            let imageURL = URL(string: "https://source.unsplash.com/random")!
            print("Starting network request...")
            let (imageData, _) = try await URLSession.shared.data(from: imageURL)
            return UIImage(data: imageData)
        }
        return try await imageTask.value
    }
}

上面的代码例子获取了一张随机的图片,如果请求成功,就会相应地显示出来。

为了这个演示,我们可以在imageTask创建后立即取消它:

func fetchImage() async throws -> UIImage? {
    let imageTask = Task { () -> UIImage? in
        let imageURL = URL(string: "https://source.unsplash.com/random")!
        print("Starting network request...")
        let (imageData, _) = try await URLSession.shared.data(from: imageURL)
        return UIImage(data: imageData)
    }
    // Cancel the image request right away:
    imageTask.cancel()
    return try await imageTask.value
}

上面的取消调用将会阻止请求,因为 URLSession 实现在执行之前会执行取消检查。因此,上面的代码示例打印出以下内容:

Starting network request...
Image loading failed: Error Domain=NSURLErrorDomain Code=-999 "cancelled"

如您所见,我们的打印语句仍在执行。这个打印语句是演示了如何使用静态取消检查的两种方法的其中一种。另一种是通过在检测到取消时抛出错误来停止执行当前任务:

let imageTask = Task { () -> UIImage? in
    let imageURL = URL(string: "https://source.unsplash.com/random")!

    /// 如果任务已被取消,则抛出错误。
    try Task.checkCancellation()

    print("Starting network request...")
    let (imageData, _) = try await URLSession.shared.data(from: imageURL)
    return UIImage(data: imageData)
}
// Cancel the image request right away:
imageTask.cancel()

上面的代码打印结果为:

Image loading failed: CancellationError()

如您所见,我们的打印语句和网络请求都没有被调用。

我们可以使用的第二种方法给我们一个取消的状态。通过使用这种方法,我们允许自己在取消时执行任何额外的清理工作:

let imageTask = Task { () -> UIImage? in
    let imageURL = URL(string: "https://source.unsplash.com/random")!

    guard Task.isCancelled == false else {
        // Perform clean up
        print("Image request was cancelled")
        return nil
    }

    print("Starting network request...")
    let (imageData, _) = try await URLSession.shared.data(from: imageURL)
    return UIImage(data: imageData)
}
// Cancel the image request right away:
imageTask.cancel()

在这种情况下,我们的代码只打印出取消声明。

执行定期取消检查对于防止您的代码做不必要的工作至关重要。想象一个例子,我们将转换返回的图像;我们可能应该在整个代码中添加多个检查:

let imageTask = Task { () -> UIImage? in
    let imageURL = URL(string: "https://source.unsplash.com/random")!

    // 在网络请求之前检查取消。
    try Task.checkCancellation()
    print("Starting network request...")
    let (imageData, _) = try await URLSession.shared.data(from: imageURL)

    // 网络请求后检查取消,以防止开始我们繁重的图像操作。
    try Task.checkCancellation()

    let image = UIImage(data: imageData)

    // 由于任务未取消,因此执行返回图像操作。
    return image
}

在可以很容易的掌控任务的取消,这使得我们很容易犯错误和进行不必要的工作。在执行任务时,请保持警惕,确保你的代码定期检查取消的状态。

设置优先级

每个任务都可以有它的优先级。我们可以应用的值类似于我们在使用调度队列时可以配置的服务质量级别。低、中、高优先级看起来与操作设置的优先级相似。

我们可以通过设置优先级来管理任务执行的顺序

每个优先级都有其目的,并且可以表明一项工作比其他工作更重要。但是不能保证您的任务一定更早执行。例如,较低优先级的作业可能已经在运行。

配置优先级有助于防止低优先级任务比更高优先级的任务更先执行。

用于执行的线程

默认情况下,一个任务在一个自动管理的后台线程上执行。通过测试,我发现默认的优先级是25。打印出高优先级的原始值,显示其实相匹配的:

(lldb) p Task.currentPriority
(TaskPriority) $R0 = (rawValue = 25)
(lldb) po TaskPriority.high.rawValue
25

您可以设置断点来验证您的方法在哪个线程上运行:

通过使用断点,您可以检查任务正在运行的线程。

继续您的 Swift 并发之旅

并发更改不仅仅是async-await,还包括许多您可以在代码中受益的新功能。现在您已经了解了任务的基础知识,是时候深入了解其他新的并发特性了:

结论

Swift 中的Task允许我们创建一个并发环境来运行异步方法。取消任务需要明确的检查,以确保我们不去执行任何不必要的工作。通过配置我们任务的优先级,我们可以管理执行的顺序。

转自 Tasks in Swift explained with code examples

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,258评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,335评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,225评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,126评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,140评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,098评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,018评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,857评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,298评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,518评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,678评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,400评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,993评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,638评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,801评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,661评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,558评论 2 352

推荐阅读更多精彩内容