Asynchronous - result0924/iosLearning GitHub Wiki

Swift Concurrency

Created by: justin Created time: May 14, 2023 2:14 PM

語法說明

  • import _Concurrency
func download() async {
    let data = await startSync()
    process(data)
}

- async: 表示這段closure會用asynchronous的方式執行
- await: 表示暫停點,後面接著的內容可以被暫停、稍後再來執行
  await會搭配一個asynchronous的closure
  await不代表一定會暫停、只是一個暫停點

要從synchronous建立一個asynchronous的code
可以用Task

Task {
    await download()
}

- async、Task都能用來建立一段asynchronous的空間
- Task:在synchronous的空間建立一段Asynchronous code

我們要建立一個 asynchronous Code 的空間、 我們可以用async這個關鍵字把它放在closure的宣告 就可以讓這整個closure的空間變asynchronous的 或者我們可以在任何一個地方新增一個Task 那Task裡面就可以使用asynchronous的code 然後呼叫asynchronous的方法一定要搭配await awiait並不代表現在馬上暫停、它只是明確告訴你、這裡可能會被暫停 但實際到底會不會被暫停或暫停多久我們都不知道 在等待asynchronous的code時要非常小心、 因為可能有不同thread正在存取相同的變數


===========================
MainActor

@MainActor
func uiRefresh() {

}

Task { @MainActor in
    uiRefresh()
}

- 在function前面加上@MainActor這樣子function裡面的code都會在main thread上面執行
- 也可以在Task建立一個@MainActor這樣也可以讓Task裡面的code在main thread上面執行
- 上面二個寫法其實是一樣的

如果要針對Task某一段code單獨把它執行在mian thread上面的話可以用下面這種寫法
因為main thread不一定能馬上執行、所以要加上await

Task {
    await MainActor.run {
        uiRefresh()
    }
}

=================

DispatchQueue.main.async {

}
Apple希望以後改為
Task.detached { @MainActor in

}
.detached可加可不加
=============
TaskGroup:會等待整個task的gruop任務都完成才會往下

Continuations

讓不支援async/await的舊library or third party去支援async/await
convert non-asynchronous code in to asynchronous code

sendable:

@sendable: 可以安全地傳遞的內容 意思是這裡面不會發生、一起被存取或修改的奇怪情況 這個closure就算被多個thread同時執行也沒有關系 在標了@sendable後、如果做了任何不安全的行為、 XCode都是會發出警告的 ps. 在swift 5.7後才可以使用

打開concurrency checking預設是minimal、在swift 6.0後預設就會是complet https://www.avanderlee.com/swift/sendable-protocol-closures/#preparing-your-code-for-swift-6-with-strict-concurrency-checking

如何讓class confirm sendable

  • 一個可發送(sendable)的類別必須是 final,表示它不能被繼承。
  • 類別的所有屬性必須是可發送和不可變的,也就是使用 let 來聲明。
  • 這個類別不能有任何除了 NSObject 以外的超類(superclass)。
  • 使用 @MainActor 註解標記的類別由於透過主要演算器(main actor)進行同步,因此它們隱式地可發送。這點不論類別的儲存屬性是否可變或可發送。

@Sendable and @escaping would be marked as @Sendable

當你確定已經採取了必要的步驟,確保你的類別是線程安全的且完全沒有資料競爭時,你可以通過將類別標記為「@unchecked Sendable」,強制編譯器接受你的「Sendable」遵從性,而無需實際驗證你的遵從性。

ex.

final class Movie: @unchecked Sendable {
// ...
}

名詞介紹

  • Parallel(平行執行):在不同thread上同時執行
  • Asynchronous(非同步執行):透過暫停在切換不同工作,讓等待時間也能進行工作
  • Concurrency:(Parallel + Asynchronous) 同一段時間中處理了多件事情
  • Concurrency 的語法都是建立在「 Task 」之上。(async let, Task, TaskGroup)

Task 可以設定優先度、local變數和Actor、也可以被取消

  • Global Actor: 預設直接繼承
  • Actor: 如果有用到local變數才繼承
  • 不想繼承的話要使用Task.detached

Structured Concurrency

  • 當整段 block 可以透過由上往下的閱讀就知道實際被執行時的順序,就是結構化的(Structured)
建立方式 可建立的地方 取消方式 結束時間
結構化 async let Task Group 非同步的區塊中 自動 可判斷
非結構化 Task 任何地方 手動 無法判斷
  • 結構化的容易從閱讀理解、能自動管理錯誤和取消。
  • 非結構化的更有彈性,可以從任何地方啟動,也能透過Task從其他地方取消任務

actor

  • 確保一次只有一件事情發生、它幫我們避免的事情是data race
  • 沒有辦法確保執行的順序、沒辦法確保reentrancy
  • reentrancy這件事、是我們自已在設計程式碼時要注意的

What is an actor and why does Swift have them? - a free Swift Concurrency by Example tutorial

如果作為開發人員絕對確定某個給定的函數是可以安全地並行調用而不需要隔離的,我們可以將該函數標記為nonisolated,以免該函數受到actor隔離的影響。

在 Swift Concurrency 中,如果 nonisolated 的 methods 在 actor 中沒有存取任何 mutable,則可以直接調用它們而無需等待它們,因為它們不會被處理為 actor 郵箱中的訊息,而是立即執行。

Data Race

  • 有多個地方、存取同一筆資料、而且其中一個地方正在嘗試修改它、這樣的狀況導致資料不正確

Reentrancy

  • 這段程式碼是不是可以安全的暫停、然後在任何的時間點、重新回來
  • 所謂安全的意思、無論暫停了多久、重新回來的時候、這段程式碼所代表的意義都不會被影響
  • Actor 的可重入性、基本上意味着每當一個 actor 在等待某件事情時,它可以繼續從郵箱中選取其他訊息,直到等待的事情完成並且被暫停的函數可以繼續執行。 可重入性的結果是,在 await 之前我們所做的任何假設都應在 await 之後重新驗證。

thread explosion

  • GCD 會發生但 Swift Concurrency的機制可以避免.

group.addTask group.next

let indices = Array(0...10)
try await withThrowingTaskGroup(of: Void.self, body: { group in
    let maxConcurrentTaskCount = 4
    var concurrentTaskCount = 0

    for i in indices {
        concurrentTaskCount += 1

        if concurrentTaskCount >= maxConcurrentTaskCount {
            _ = try await group.next()
            concurrentTaskCount -= 1
        }

        group.addTask {
            try await Task.sleep(for: .seconds(3))
            print(i)
        }
    }

})

Task.yield()是一個 async 函式,它告訴編譯器現在可以讓別的任務佔用 CPU 資源。
這樣做是為了避免當前操作長時間佔用 CPU,導致其他任務無法執行或者系統變得非常緩慢。

總體來說,使用 await Task.yield() 可以幫助我們更好地管理非同步操作,避免長時間佔用 CPU 資源,提高應用程序的效率和性能。

func veryLongLoop() async {
  for item in veryLongList {
    // process the item
    await Task.yield()
  }
}

  • Combine + asynchronous的範例、利用floatMap和Future特性
example1:
$searchText
    .debounce(for: 0.5, scheduler: DispatchQueue.main)
    .flatMap { query in
        return Future { [weak self] promise in
            guard let self = self else {
                promise(.success([]))
                return
            }

            Task {
                do {
                    let results = try await self.search(for: query)
                    promise(.success(results))
                } catch {
                    // instead of failing, complete with empty result
                    promise(.success([]))
                }
            }
        }

example2:
class Networking {
    func fetchHomepage() -> AnyPublisher<String, Error> {

    }

    func fetchHomepage() async throws -> String {
        let asyncSequence = fetchHomepage().values

        for try await value in asyncSequence {
            return value
        }

        // we really shouldn't ever get here...
        return ""
    }

    or

    func fetchHomepage() async throws -> [String] {
        let asyncSequence = fetchHomepage().values
        var values = [String]()

        for try await value in asyncSequence {
            values.append(value)
        }
        return values
    }
}

在 for 迴圈中使用 try await 可以保證按照迴圈中的順序處理每個非同步操作的結果。
這意味著迴圈的執行會等待前一次的非同步操作完成,然後才進行下一次的非同步操作。

這種方式有助於確保程式的可靠性,同時允許我們以非同步的方式處理並行任務。
我們可以在非同步操作完成後立即處理結果,或者使用 await 在需要的時候暫停執行緒,
直到非同步操作完成。

ex.
// We can await values to become available
for try await line in csvURL.lines {

===
非同步的 for 迴圈具有與一般 for 迴圈相同的規則,第一個迴圈必須完成後,第二個迴圈才能開始。
因此下面的範例二個for loop不會同時執行
ex.
var employees = [Employee]()
let csvURLPartOne = URL(string: "path/to/csv/part-1.csv")
let csvURLPartTwo = URL(string: "path/to/csv/part-2.csv")

for try await line in csvURLPartOne.lines {
  // ...
}

for try await line in csvURLPartTwo.lines {
  // ...
}

但如果想讓二個for loop同時執行、可以利用
let task1 = Task {
    for try await _ in csvURL.lines {
        print("Loop one received line...")
    }
}

let task2 = Task {
    for try await _ in csvURL.lines {
        print("Loop two received line...")
    }
}
try await (task1.value, task2.value)
去讓二個for loop同時執行

combine可以利用Future和switchToLatest和async/await相結合

ex.
$query
    .debounce(for: 0.3, scheduler: DispatchQueue.main)
    .map({ query in
        Future { promise in
            Task {
                do {
                    let results = try await self.network.getResults(forQuery: query)
                    promise(.success(results))
                } catch {
                    promise(.success([]))
                }
            }
        }
    })
    .switchToLatest()
// 在async/await使用combine要小心沒釋放的問題、因為Task要等裡面的for loop處理完才會釋放

func fixedSubscribe() {
    Task { [subject] in
        for await value in subject.values {
            print("received \(value)")
        }
    }
}

func actuallyFixedSubscribe() {
    task = Task { [subject] in
        for await value in subject.values {
            print("received \(value)")
        }
    }
}

Swift Concurrency 有二個工具去幫忙處理子任務流程

  • Async let
使用`Async let`相比`Task`的好處是可以確保從父類別取消後、可以一起取消、不像`Task`
  • Task groups(當Async let太多時的好幫手)
`Task groups`可以用來生成未知數量的子任務
雖然`async let`是一種很棒的工具,可以作為單個父任務或異步函數的一部分並行執行少量任務,
但我們需要不同的機制來創建(和等待)任意數量的任務。

在 Swift 中,`withThrowingTaskGroup` 和 `withTaskGroup` 
是用於處理並行任務(concurrent tasks)的兩種不同方法。它們的主要區別在於錯誤處理的方式。

1. `withThrowingTaskGroup`:
   - `withThrowingTaskGroup` 允許您執行並行任務並處理錯誤。
   - 它接受一個泛型的錯誤類型,並使用 `Result` 類型來表示任務的成功或失敗。
   - 在 `withThrowingTaskGroup` 中,
		 您可以使用 `try` 關鍵字來捕獲和處理任務執行過程中的錯誤。

2. `withTaskGroup`:
   - `withTaskGroup` 也允許您執行並行任務,但不處理錯誤。
   - 它只返回一個非泛型的 `TaskGroup`,並使用 `Void` 類型表示任務的結果。
   - 在 `withTaskGroup` 中,
     您無法使用 `try` 關鍵字來捕獲和處理任務的錯誤。
     任務的錯誤會被忽略,並在後續的處理中無法取得。

總結來說,`withThrowingTaskGroup` 提供了處理並行任務並捕獲錯誤的能力,
而 `withTaskGroup` 僅提供了執行並行任務的能力,並將錯誤忽略。
選擇使用哪種方法取決於您是否需要處理任務執行期間的錯誤。

you can obtain all results from child tasks in a group using nextResult 
to avoid any errors from being thrown out of your Task Group closure.

What’s the Diff: Programs, Processes, and Threads

參考