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”調用是 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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章