async await Task MainActor基础使用方法

235 阅读6分钟

案例一

let randomImageUrl = URL(string: "https://random.imagecdn.app/300/300")!
func downloadImage()  async throws -> UIImage {
    let (data, response) = try await URLSession.shared.data(from: randomImageUrl)

    guard let response = response as? HTTPURLResponse,
          (200...299).contains(response.statusCode) else {
        fatalError()
    }
    return UIImage(data: data)!
}

Task {
    try! await downloadImage()
}
print("sss")

案例二

Task 相当于开了一个异步方法, 下面有两个异步方法,方法里面的sleep会阻塞整个task的执行,也会影响到第二个task执行,要使用Task.sleep方法来处理这个问题
Task {
    print("11111")
    await sleep(2)
    print("22222")
}

Task {
    print("333")
    await sleep(2)
    print("444444")
}
打印结果: 
**11111**
**22222**
**333**
**444444**

Task {
    print("11111")
    await try Task.sleep(nanoseconds: 3000000000)
    print("22222")
}

Task {
    print("333")
    await try Task.sleep(nanoseconds: 1000000000)
    print("444444")
}
**11111**
**333**
**444444**
**22222**

案例三

test方法都是在主线程执行
@MainActor
func test() async {
    Task {
        print(Thread.current)
    }
}

Task {
    await test()
}

案例三

DispatchQueue.main.async {
}

/// 官方建议的新方法
Task { @MainActor in
}

案例四

public func printElapsedTime(from startTime: Date) {
    let endTime = Date.now
    let timePassed = (startTime.distance(to: endTime)).formatted()
    print("完成任務時間經過:\(timePassed) 秒")
}

let totalWorkers = 10
var finishWorking = 0
let startTime = Date.now

func work(name: String) async throws {
    print("\(name): 1⃣️ 开始工作")
    try await Task.sleep(nanoseconds: 2000000000)
    Task {
        print("\(name): 2⃣️ 午休时间")
        try await Task.sleep(nanoseconds: 1000000000)
        print("\(name): 3⃣️ 睡饱了")
    }

    print("\(name): 4⃣️ 继续工作")
    try await Task.sleep(nanoseconds: 2000000000)
    print("\(name) 5⃣️ 下班")
    await MainActor.run {
        finishWorking += 1
        if finishWorking == totalWorkers {
            print("\(printElapsedTime(from: startTime))")
        }
    }
}

for number in 1...totalWorkers {
    Task {
        try! await work(name:"员工 \(number) 号")
    }
}

案例五

public protocol Deliverable: CustomStringConvertible {
    static var deliveryTime: TimeInterval { get }
}

public enum Food: String, CaseIterable, Deliverable {    
    public static var deliveryTime: TimeInterval = 3
    public var description: String { rawValue }   

    case 薯條 = "🍟"
    case 拉麵 = "🍜"
    case 水餃 = "🥟"
    case 披薩 = "🍕"
    case 漢堡 = "🍔"
}

public enum Drink: String, CaseIterable, Deliverable {
    public static var deliveryTime: TimeInterval = 2
    
    case 珍奶 = "🧋"
    case 生啤 = "🍺"
    case 果汁 = "🧃"
    case 牛奶 = "🥛"
    case= "🍵"

    public var description: String { rawValue }
}

public func printElapsedTime(from startTime: Date) {

    let endTime = Date.now
    let timePassed = (startTime.distance(to: endTime)).formatted()
    print("完成任務時間經過:\(timePassed) 秒")
}

extension Task where Success == Never, Failure == Never {

    public static func sleep(seconds: Double) async throws {
        let nanoseconds = UInt64(seconds * 1_000_000_000)
        try await Task.sleep(nanoseconds: nanoseconds)
    }
}

// 1. 以下是一段 Synchronous 的叫外送方式,請修改讓外送變成 Asynchronous 的,讓最後在大約 3 秒初完成所有運送「所有餐點」。
// ⚠️ 下一集會教更便利地一次處理多個任務的方法,但現在請用目前學會的方式來處理,確保掌握了基本概念。

extension Deliverable {

    func order() async {
        guard let _ = try? await Task.sleep(seconds: Self.deliveryTime) else {
            assertionFailure("無法完成送餐(\(self))")
            return
        }
        print("您的餐點已抵達:\(self)")
    }
}

let startTime = Date.now
var itemReceived = 0

let allItems: [Deliverable] = Food.allCases + Drink.allCases
for item in allItems {
    Task {
        await item.order()
        await MainActor.run {
            itemReceived += 1
            if itemReceived == (Food.allCases.count + Drink.allCases.count) {
                printElapsedTime(from: startTime)
            }
        }
    }
}

/* 2. 練習取得網路上的資料,請透過下列網址會取得隨機貓咪知識,收到資料請透過 CatFact(data:) 來啟動 CatFact,並印出其中的 fact。請取得三個貓咪知識,並且滿足以下條件:

 > 三個請求等待不阻擋彼此。

 > 確保網路請求不是在 Main Thread 進行,而最後印出來貓咪知識時是在 Main Thread 進行。

 > 在網路請求和印出貓咪知識的地方都印出是否在 Main Thread。

 */

enum HTTPError: Error {
    case invalidResponse
}

struct CatFact: Codable {
    let fact: String
    let length: Int
    
    init(data: Data) throws {
        self = try JSONDecoder().decode(CatFact.self, from: data)
    }
    static private let requestUrl = URL(string: "https://catfact.ninja/fact")!
    
    static func getRandomFact() async throws  -> String {
        print("> 網路請求是否在 Main Thread?\(Thread.current.isMainThread)")
        let (data, res) = try await URLSession.shared.data(from: requestUrl)
        guard let res = res as? HTTPURLResponse,
              (200...299).contains(res.statusCode) else {
            throw HTTPError.invalidResponse
        }

        let catFact = try CatFact(data: data)
        return catFact.fact
    }
}

for _ in 1...3 {

    Task {
        let fact = (try? await CatFact.getRandomFact()) ?? "Something went wrong..."
        await MainActor.run {
            print("> 印出貓咪知識是否在 Main Thread?\(Thread.current.isMainThread)")
            print("🐈 貓咪知識:\(fact)")
        }
    }
}

案例六 async使用

修改前
func fetchUsername() async -> String {
    try! await Task.sleep(seconds: 2)
    return "老王"
}

func fetchPassword() async -> String {
    try! await Task.sleep(seconds: 2)
    return "123456"
}

func test() async {
    let startTime = Date.now
    let fetchName = await fetchUsername()
    let fetchPassword = await fetchPassword()
    print(fetchName, fetchPassword)
    printElapsedTime(from: startTime)
}

Task {
    await test()
}
**老王** **123456**
**完成任務時間經過:** **4.275235** **秒**

/// 修改后
func fetchUsername() async -> String {
    try! await Task.sleep(seconds: 2)
    return "老王"
}

func fetchPassword() async -> String {
    try! await Task.sleep(seconds: 2)
    return "123456"
}

func test() async {
    let startTime = Date.now
    async let fetchName = fetchUsername()
    async let fetchPassword = fetchPassword()
    print(await fetchName, await fetchPassword)
    printElapsedTime(from: startTime)
}

Task {
    await test()
}

案例七 withTaskGroup

优化前
let startTime = Date.now

func fetchUser(id: Int) async -> String {
    try! await Task.sleep(seconds: 1)
    let names = ["泡沫", "teddy", "秘密", "啵啵", "法兰克"]
    return names[id-1]
}

Task {
    let userIDs = Array(1...5)
    for id in userIDs {
        await fetchUser(id: id)
    }
    printElapsedTime(from: startTime)
}
**完成任務時間經過:** **5.318276** **秒**

优化后
let startTime = Date.now

func fetchUser(id: Int) async -> String {
    try! await Task.sleep(seconds: 1)
    let names = ["泡沫", "teddy", "秘密", "啵啵", "法兰克"]
    return names[id-1]
}

Task {
    await withTaskGroup(of: Void.self, body: { group in
        let userIDs = Array(1...5)
        for id in userIDs {
            group.addTask {
                _ = await fetchUser(id: id)
            }
        }
    })
    printElapsedTime(from: startTime)
}
**完成任務時間經過:** **1.092025** **秒**

再次优化
let startTime = Date.now

func fetchUser(id: Int) async -> String {
    try! await Task.sleep(seconds: 1)
    let names = ["泡沫", "teddy", "秘密", "啵啵", "法兰克"]
    return names[id-1]
}

Task {
    let users = await withTaskGroup(of: String.self, returning: [String].self, body: { group in
        let userIDs = Array(1...5)
        for id in userIDs {
            group.addTask {
               await fetchUser(id: id)
            }
        }

        var users = [String]()
        for await result in group {
            users.append(result)
        }
        return users
    })
    print(users)
    printElapsedTime(from: startTime)
}


/// 添加throws
let startTime = Date.now

func fetchUser(id: Int) async throws -> String {
    try await Task.sleep(seconds: 1)
    let names = ["泡沫", "teddy", "秘密", "啵啵", "法兰克"]
    return names[id-1]
}

Task {
    let users = await withTaskGroup(of: String?.self, returning: [String].self, body: { group in
        let userIDs = Array(1...5)
        for id in userIDs {
            group.addTask {
                try? await fetchUser(id: id)
            }
        }
        var users = [String]()
        for await result in group {
            users.append(result ?? "")
        }
        return users.compactMap { $0 }
    })
    print(users)
    printElapsedTime(from: startTime)
}

async 练习题一

// 1. 請將以下內容改用 async let 寫,讓程式能在 2 秒初完成。

// *如果你的 playground 也無法使用此語法,可以直接對照是否跟答案一樣。

// 1️⃣ 第一題的相關 function

public func getUsername() async -> String {
    try! await Task.sleep(seconds: 1)
    return "Jane"
}

public func getAllMovies() async -> [String] {
    try! await Task.sleep(seconds: 2)
    return ["捍衛戰士:獨行俠", "侏羅紀世界:統霸天下", "雷神索爾:愛與雷霆", "貓王艾維斯", "巴斯光年"]
}

Task {
    let startTime = Date.now
    let username = await getUsername()
    let movies = await getAllMovies()
    print("Hello \(username),現在熱門電影是 \(movies)")
    printElapsedTime(from: startTime)
}

优化后时间为两秒多一点
Task {
    let startTime = Date.now
    async let username = getUsername()
    async let movies = getAllMovies()

    let name = await username
    let movie = await movies

    print("Hello \(name),現在熱門電影是 \(movie)")
    printElapsedTime(from: startTime)
}

async 练习题二

// 2. 根據步驟依序建立一個取得翻譯過的「Hello」API 服務,接著使用這個 API 提供首頁畫面需要的資訊,最後測試獲得首頁畫面資訊的功能是否正常。第二步驟和第三步驟都需要使用 TaskGroup 完成。

// 1️⃣ 寫一個根據使用者位置取得當地語言的「HelloAPIManager」的服務。

// *你可以透過網址「 https://fourtonfish.com/hellosalut/?cc= 」後面加上位置取得對應的當地 Hello。此位置資料和 User 中的 location 屬性一樣。

// *下載到資料後,可以透過 helloDataToString(Data) 這個方法把資料轉成 String。

// *所有錯誤都應該被拋出去讓呼叫端自行決定如何處理。

// *請先打開瀏覽器到「 https://fourtonfish.com/hellosalut/?cc=tw 」 ,確認你能正常連到此網站,如果不行的話則直接使用「getLocalizedHello(of:)」這個方法取得當地 Hello。
import Foundation

// 1️⃣ 第一題的相關 function
public func getUsername() async -> String {
    try! await Task.sleep(seconds: 1)
    return "Jane"
}

public func getAllMovies() async -> [String] {
    try! await Task.sleep(seconds: 2)
    return ["捍衛戰士:獨行俠", "侏羅紀世界:統霸天下", "雷神索爾:愛與雷霆", "貓王艾維斯", "巴斯光年"]
}

// 2️⃣ 第二題的相關 funciton、類型
public func helloDataToString(_ data: Data) -> String? {
    try? JSONDecoder().decode(HelloResult.self, from: data).string
}

enum LoginError: Error {
    case invalidAccountOrPassword
}

public struct User {
    public let name: String
    public let location: String
   
    public init(account: String, password: String) async throws {
        try await Task.sleep(seconds: 1)
        guard let user = User.users[account], password == "pass" else {
            throw LoginError.invalidAccountOrPassword
        }
        self.name = user.0
        self.location = user.1
    }


    private static let users = [
        "chaocode": ("Jane", "tw"),
        "aragakiyui": ("結衣", "jp"),
        "thinkaboutzu": ("子瑜", "kr"),
        "emilyinparis": ("艾蜜莉", "fr"),
        "lepetitprince": ("小王子", "b612"),
    ]
}

enum WeatherAPIError: Error {
    case locationNotFound(location: String)
}

public func getWeather(for location: String) async throws -> Double {
    try await Task.sleep(seconds: 1)
    guard let temperature = ["tw": 33.6, "kr": 24.1, "jp": 26, "fr": 28.2][location] else {
        throw WeatherAPIError.locationNotFound(location: location)
    }
    return temperature
}


/// 只在無法連上網站時使用這個方式取得翻譯好的 Hello。可以上網時請練習使用 URLSession。

public func getLocalizedHello(of location: String) async throws -> String? {
    try await Task.sleep(seconds: 1)
    return ["kr": "안녕하세요", "fr": "Salut", "tw": "你好", "jp": "こんにちは"][location]
}

// 通用 function
public func printElapsedTime(from startTime: Date) {
    let endTime = Date.now
    let timePassed = (startTime.distance(to: endTime)).formatted()
    print("完成任務時間經過:\(timePassed) 秒")
}

extension Task where Success == Never, Failure == Never {

    public static func sleep(seconds: Double) async throws {
        let nanoseconds = UInt64(seconds * 1_000_000_000)
        try await Task.sleep(nanoseconds: nanoseconds)
    }
}

// 以下只是轉換 Hello Data 的 function。不需要閱讀。
struct HelloResult: Codable {

    let code: String
    let hello: String
   
    var string: String {
        if !hello.contains(";") { return hello }
        return hello.replacingOccurrences(of: "&#", with: "").split(separator: ";").compactMap{ String(Int($0)!, radix: 16).unicode }.joined()

    }
}


extension String {

    var unicode: String? {
        if let charCode = UInt32(self, radix: 16),
           let unicode = UnicodeScalar(charCode) {
            let str = String(unicode)
            return str
        }
        return nil
    }
}

enum HelloAPIManager {

    enum HelloAPIError: Error {

        case incorrectURL

        case unableToParseData

    }

  


    static func hello(at location: String) async throws -> String {

        guard let url = URL(string: "https://fourtonfish.com/hellosalut/?cc=\(location)") else {

            throw HelloAPIError.incorrectURL

        }

        let (data, response) = try await URLSession.shared.data(from: url)

        guard let response = response as? HTTPURLResponse,

              (200...299).contains(response.statusCode),

              let hello = helloDataToString(data) else {

            throw HelloAPIError.unableToParseData

        }

        return hello

    }

}

typealias HomePageContent = (username: String, localizedHello: String, localTemperature: Double?)

  


enum HomePageResult {

    case user(User)

    case hello(String)

    case weather(Double?)

}

  


func login(account: String, password: String) async throws -> HomePageContent {

    try await withThrowingTaskGroup(of: HomePageResult.self, returning: HomePageContent.self) { group in

        group.addTask {

            .user(try await User(account: account, password: password))

        }

        

        var content: HomePageContent = ("", "", 0)

        

        for try await result in group {

            switch result {

                case .user(let user):

                    group.addTask {

                        .hello((try? await HelloAPIManager.hello(at: user.location)) ?? "Hello")

                    }

                    

                    group.addTask {

                        .weather(try? await getWeather(for: user.location))

                    }

                    

                    content.username = user.name

                case .weather(let temprature):

                    content.localTemperature = temprature

                case .hello(let hello):

                    content.localizedHello = hello

            }

        }

         

        return content

    }

}

  


// 3️⃣ 用 TaskGroup 測試 testCases 中的六組登入資料,使用你在第二步建立的 function 登入。

// * 最後應照原本的測試順序印出歡迎訊息。

// * 歡迎訊息是:「Hello,OOO。今天的溫度大約是 XX 度。」。Hello 應為當地語言,OOO 是使用者名稱,如果沒有溫度資訊則省略。

// * 無法登入時印出「無法登入帳號 XXX,原因:OOO」。OOO直接使用錯誤名稱即可。

// * 印出測試完六組總共花費多少時間。我的完成時間是兩秒多,不過因為包含網路任務所以這個數字會受到你的網路速度影響。

  


let testCases: [(account: String, password: String)] = [("janechao", "pass"), ("chaocode", "pass"), ("aragakiyui", "pass"), ("thinkaboutzu", "pass"), ("kimkardashian", "1234"), ("emilyinparis", "pass")]

Task {

    let startTime = Date.now

    

    await withTaskGroup(of: (id: String, message: String).self) { group in

        testCases.forEach { test in

            group.addTask {

                do {

                    let result = try await login(account: test.account, password: test.password)

                    let tempratureMessage = result.localTemperature == .none ? "" : "今天的溫度大約是 \(result.localTemperature!) 度。"

                    

                    return (test.account, "\(result.localizedHello), \(result.username)\(tempratureMessage)")

                } catch {

                    return (test.account, "無法登入帳號 \(test.account),原因:\(error)")

                }

                

            }

        }

        

        var results = [String: String]()

        for await result in group {

            results[result.id] = result.message

        }

        

        testCases.forEach { print(results[$0.account]!) }

    }

    

    printElapsedTime(from: startTime)

}