在 Swift 中使用 async let 并發(fā)運行后臺任務(wù)

Async/await語法是在Swift 5.5 引入的赔桌,在 WWDC 2021中的 Meet async/await in Swift 對齊進行了介紹在辆。它是編寫異步代碼的一種更可讀的方式工闺,比調(diào)度隊列和回調(diào)函數(shù)更容易理解。Async/await 語法與其他編程語言(如C#或JavaScript)中使用的語法類似温峭。使用 "async let "是為了并行的運行多個后臺任務(wù)猛铅,并等待它們的綜合結(jié)果。

Swift異步編程是一種編寫允許某些任務(wù)并發(fā)運行而不是按順序運行的代碼的方法凤藏。這可以提高應(yīng)用程序的性能奸忽,允許它同時執(zhí)行多個任務(wù)堕伪,但更重要的是,它可以用來確保用戶界面對用戶輸入的響應(yīng)栗菜,同時任務(wù)在后臺線程上執(zhí)行欠雌。

長期運行的任務(wù)阻塞了UI

在一個同步的程序中,代碼以線性的疙筹、從上到下的方式運行富俄。程序等待當前任務(wù)完成后再進入下一任務(wù)。這在用戶界面(UI)方面會產(chǎn)生問題而咆,因為如果一個長期運行的任務(wù)被同步執(zhí)行霍比,程序就會阻塞,UI就會變得沒有反應(yīng)暴备,直到任務(wù)完成悠瞬。

下面的代碼模擬了一個長期運行的任務(wù),如以同步方式下載一個文件涯捻,其結(jié)果是UI 變得沒有反應(yīng)浅妆,直到任務(wù)完成。這樣的用戶體驗是不可接受的汰瘫。

Model:

struct DataFile : Identifiable, Equatable {
    var id: Int
    var fileSize: Int
    var downloadedSize = 0
    var isDownloading = false
    
    init(id: Int, fileSize: Int) {
        self.id = id
        self.fileSize = fileSize
    }
    
    var progress: Double {
        return Double(self.downloadedSize) / Double(self.fileSize)
    }
    
    mutating func increment() {
        if downloadedSize < fileSize {
            downloadedSize += 1
        }
    }
}

ViewModel:

class DataFileViewModel: ObservableObject {
    @Published private(set) var file: DataFile
    
    init() {
        self.file = DataFile(id: 1, fileSize: 10)
    }
    
    func downloadFile() {
        file.isDownloading = true

        for _ in 0..<file.fileSize {
            file.increment()
            usleep(300000)
        }

        file.isDownloading = false
    }
    
    func reset() {
        self.file = DataFile(id: 1, fileSize: 10)
    }
}

View:

struct TestView1: View {
    @ObservedObject private var dataFiles: DataFileViewModel
    
    init() {
        dataFiles = DataFileViewModel()
    }
    
    var body: some View {
        VStack {
            /// 從文末源代碼獲取其實現(xiàn)
            TitleView(title: ["Synchronous"])
            
            Button("Download All") {
                dataFiles.downloadFile()
            }
            .buttonStyle(BlueButtonStyle())
            .disabled(dataFiles.file.isDownloading)
            
            HStack(spacing: 10) {
                Text("File 1:")
                ProgressView(value: dataFiles.file.progress)
                    .frame(width: 180)
                Text("\((dataFiles.file.progress * 100), specifier: "%0.0F")%")

                ZStack {
                    Color.clear
                        .frame(width: 30, height: 30)
                    if dataFiles.file.isDownloading {
                        ProgressView()
                            .progressViewStyle(CircularProgressViewStyle(tint: .blue))
                    }
                }
            }
            .padding()
            
            Spacer().frame(height: 200)

            Button("Reset") {
                dataFiles.reset()
            }
            .buttonStyle(BlueButtonStyle())

            Spacer()
        }
        .padding()
    }
}
模擬同步下載一個文件--沒有實時更新UI

使用 async/await 在后臺執(zhí)行任務(wù)

將 ViewModel 中的downloadFile方法修改為異步的狂打。請注意,由于DataFile模型是被視圖監(jiān)聽的混弥,對模型的任何改變都需要在UI線程上執(zhí)行。這是通過使用 MainActor 隊列來完成的对省,即用MainActor.run包裹所有的模型更新蝗拿。

ViewModel

class DataFileViewModel2: ObservableObject {
    @Published private(set) var file: DataFile
    
    init() {
        self.file = DataFile(id: 1, fileSize: 10)
    }
    
    func downloadFile() async -> Int {
        await MainActor.run {
            file.isDownloading = true
        }
        
        for _ in 0..<file.fileSize {
            await MainActor.run {
                file.increment()
            }
            usleep(300000)
        }
        
        await MainActor.run {
            file.isDownloading = false
        }
        
        return 1
    }
    
    func reset() {
        self.file = DataFile(id: 1, fileSize: 10)
    }
}

View:

struct TestView2: View {
    @ObservedObject private var dataFiles: DataFileViewModel2
    @State var fileCount = 0
    
    init() {
        dataFiles = DataFileViewModel2()
    }
    
    var body: some View {
        VStack {
            TitleView(title: ["Asynchronous"])
            
            Button("Download All") {
                Task {
                    let num = await dataFiles.downloadFile()
                    fileCount += num
                }
            }
            .buttonStyle(BlueButtonStyle())
            .disabled(dataFiles.file.isDownloading)
            
            Text("Files Downloaded: \(fileCount)")
            
            HStack(spacing: 10) {
                Text("File 1:")
                ProgressView(value: dataFiles.file.progress)
                    .frame(width: 180)
                Text("\((dataFiles.file.progress * 100), specifier: "%0.0F")%")
                
                ZStack {
                    Color.clear
                        .frame(width: 30, height: 30)
                    if dataFiles.file.isDownloading {
                        ProgressView()
                            .progressViewStyle(CircularProgressViewStyle(tint: .blue))
                    }
                }
            }
            .padding()
            
            Spacer().frame(height: 200)
            
            Button("Reset") {
                dataFiles.reset()
            }
            .buttonStyle(BlueButtonStyle())
            
            Spacer()
        }
        .padding()
    }
}
使用 async/await 來模擬下載一個文件,同時更新UI

在后臺執(zhí)行多個任務(wù)

現(xiàn)在我們有一個文件在后臺下載蒿涎,UI顯示進度哀托,讓我們把它改為多個文件。ViewModel被改為持有一個DataFiles數(shù)組劳秋,而不是一個單一的文件仓手。添加一個downloadFiles方法來遍歷所有文件并下載每一個。

視圖被綁定到DataFiles數(shù)組玻淑,并更新顯示每個文件的下載進度嗽冒。下載按鈕被綁定到異步的downloadFiles中。

ViewModel:

class DataFileViewModel3: ObservableObject {
    @Published private(set) var files: [DataFile]
    @Published private(set) var fileCount = 0
    
    init() {
        files = [
            DataFile(id: 1, fileSize: 10),
            DataFile(id: 2, fileSize: 20),
            DataFile(id: 3, fileSize: 5)
        ]
    }
    
    var isDownloading : Bool {
        files.filter { $0.isDownloading }.count > 0
    }
    
    func downloadFiles() async {
        for index in files.indices {
            let num = await downloadFile(index)
            await MainActor.run {
                fileCount += num
            }
        }
    }
    
    private func downloadFile(_ index: Array<DataFile>.Index) async -> Int {
        await MainActor.run {
            files[index].isDownloading = true
        }
        
        for _ in 0..<files[index].fileSize {
            await MainActor.run {
                files[index].increment()
            }
            usleep(300000)
        }
        await MainActor.run {
            files[index].isDownloading = false
        }
        return 1
    }
    
    func reset() {
        files = [
            DataFile(id: 1, fileSize: 10),
            DataFile(id: 2, fileSize: 20),
            DataFile(id: 3, fileSize: 5)
        ]
    }
}

View:

struct TestView3: View {
    @ObservedObject private var dataFiles: DataFileViewModel3
    
    init() {
        dataFiles = DataFileViewModel3()
    }
    
    var body: some View {
        VStack {
            TitleView(title: ["Asynchronous", "(multiple Files)"])
            
            Button("Download All") {
                Task {
                    await dataFiles.downloadFiles()
                }
            }
            .buttonStyle(BlueButtonStyle())
            .disabled(dataFiles.isDownloading)
            
            Text("Files Downloaded: \(dataFiles.fileCount)")
            
            ForEach(dataFiles.files) { file in
                HStack(spacing: 10) {
                    Text("File \(file.id):")
                    ProgressView(value: file.progress)
                        .frame(width: 180)
                    Text("\((file.progress * 100), specifier: "%0.0F")%")
                    
                    ZStack {
                        Color.clear
                            .frame(width: 30, height: 30)
                        if file.isDownloading {
                            ProgressView()
                                .progressViewStyle(CircularProgressViewStyle(tint: .blue))
                        }
                    }
                }
            }
            .padding()
            
            Spacer().frame(height: 150)
            
            Button("Reset") {
                dataFiles.reset()
            }
            .buttonStyle(BlueButtonStyle())
            
            Spacer()
        }
        .padding()
    }
}
使用async await來模擬按順序下載多個文件

使用 "async let "來模擬并發(fā)下載多個文件的情況

上面的代碼可以被改進补履,以并行地執(zhí)行多個下載添坊,因為每個任務(wù)都是獨立于其他任務(wù)的。在Swift并發(fā)中箫锤,這是用async let實現(xiàn)的贬蛙,它用一個承諾立即給一個變量賦值雨女,允許代碼執(zhí)行下一行代碼。然后阳准,代碼等待這些承諾氛堕,等待最終結(jié)果的完成。

async/await:

    func downloadFiles() async {
        for index in files.indices {
            let num = await downloadFile(index)
            await MainActor.run {
                fileCount += num
            }
        }
    }

async let

    func downloadFiles() async {
        async let num1 = await downloadFile(0)
        async let num2 = await downloadFile(1)
        async let num3 = await downloadFile(2)
        
        let (result1, result2, result3) = await (num1, num2, num3)
        await MainActor.run {
            fileCount = result1 + result2 + result3
        }
    }

ViewModel

class DataFileViewModel4: ObservableObject {
    @Published private(set) var files: [DataFile]
    @Published private(set) var fileCount = 0
    
    init() {
        files = [
            DataFile(id: 1, fileSize: 10),
            DataFile(id: 2, fileSize: 20),
            DataFile(id: 3, fileSize: 5)
        ]
    }
    
    var isDownloading : Bool {
        files.filter { $0.isDownloading }.count > 0
    }
    
    func downloadFiles() async {
        async let num1 = await downloadFile(0)
        async let num2 = await downloadFile(1)
        async let num3 = await downloadFile(2)
        
        let (result1, result2, result3) = await (num1, num2, num3)
        await MainActor.run {
            fileCount = result1 + result2 + result3
        }
    }
    
    private func downloadFile(_ index: Array<DataFile>.Index) async -> Int {
        await MainActor.run {
            files[index].isDownloading = true
        }
        
        for _ in 0..<files[index].fileSize {
            await MainActor.run {
                files[index].increment()
            }
            usleep(300000)
        }
        await MainActor.run {
            files[index].isDownloading = false
        }
        return 1
    }
    
    
    func reset() {
        files = [
            DataFile(id: 1, fileSize: 10),
            DataFile(id: 2, fileSize: 20),
            DataFile(id: 3, fileSize: 5)
        ]
    }
}

View

struct TestView4: View {
    @ObservedObject private var dataFiles: DataFileViewModel4
    
    init() {
        dataFiles = DataFileViewModel4()
    }
    
    var body: some View {
        VStack {
            TitleView(title: ["Parallel", "(multiple Files)"])
            
            Button("Download All") {
                Task {
                    await dataFiles.downloadFiles()
                }
            }
            .buttonStyle(BlueButtonStyle())
            .disabled(dataFiles.isDownloading)
            
            Text("Files Downloaded: \(dataFiles.fileCount)")
            
            ForEach(dataFiles.files) { file in
                HStack(spacing: 10) {
                    Text("File \(file.id):")
                    ProgressView(value: file.progress)
                        .frame(width: 180)
                    Text("\((file.progress * 100), specifier: "%0.0F")%")
                    
                    ZStack {
                        Color.clear
                            .frame(width: 30, height: 30)
                        if file.isDownloading {
                            ProgressView()
                                .progressViewStyle(CircularProgressViewStyle(tint: .blue))
                        }
                    }
                }
            }
            .padding()
            
            Spacer().frame(height: 150)
            
            Button("Reset") {
                dataFiles.reset()
            }
            .buttonStyle(BlueButtonStyle())
            
            Spacer()
        }
        .padding()
    }
}

使用 "async let "來模擬并行下載多個文件的情況
使用 "async let "來模擬并行下載多個文件的情況

結(jié)論

在后臺執(zhí)行長期運行的任務(wù)并保持UI的響應(yīng)是很重要的野蝇。async/await提供了一個干凈的機制來執(zhí)行異步任務(wù)岔擂。有的時候,一個方法在后臺調(diào)用多個方法浪耘,默認情況下是按順序進行這些調(diào)用乱灵。async 讓其立即返回,允許代碼進行下一個調(diào)用七冲,然后所有返回的對象可以一起等待痛倚。這使得多個后臺任務(wù)可以并行進行。

GitHub 上提供了 AsyncLetApp 的源代碼澜躺。

譯自 https://swdevnotes.com/swift/2023/use-async-let-to-run-background-tasks-in-parallel-in-swift/

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末蝉稳,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子掘鄙,更是在濱河造成了極大的恐慌耘戚,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,525評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件操漠,死亡現(xiàn)場離奇詭異收津,居然都是意外死亡,警方通過查閱死者的電腦和手機浊伙,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,203評論 3 395
  • 文/潘曉璐 我一進店門撞秋,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人嚣鄙,你說我怎么就攤上這事吻贿。” “怎么了哑子?”我有些...
    開封第一講書人閱讀 164,862評論 0 354
  • 文/不壞的土叔 我叫張陵舅列,是天一觀的道長。 經(jīng)常有香客問我卧蜓,道長帐要,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,728評論 1 294
  • 正文 為了忘掉前任烦却,我火速辦了婚禮宠叼,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己冒冬,他們只是感情好伸蚯,可當我...
    茶點故事閱讀 67,743評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著简烤,像睡著了一般剂邮。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上横侦,一...
    開封第一講書人閱讀 51,590評論 1 305
  • 那天挥萌,我揣著相機與錄音,去河邊找鬼枉侧。 笑死引瀑,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的榨馁。 我是一名探鬼主播憨栽,決...
    沈念sama閱讀 40,330評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼翼虫!你這毒婦竟也來了屑柔?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,244評論 0 276
  • 序言:老撾萬榮一對情侶失蹤珍剑,失蹤者是張志新(化名)和其女友劉穎掸宛,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體招拙,經(jīng)...
    沈念sama閱讀 45,693評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡唧瘾,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,885評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了迫像。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片劈愚。...
    茶點故事閱讀 40,001評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖闻妓,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情掠械,我是刑警寧澤由缆,帶...
    沈念sama閱讀 35,723評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站猾蒂,受9級特大地震影響均唉,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜肚菠,卻給世界環(huán)境...
    茶點故事閱讀 41,343評論 3 330
  • 文/蒙蒙 一舔箭、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦层扶、人聲如沸箫章。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,919評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽檬寂。三九已至,卻和暖如春戳表,著一層夾襖步出監(jiān)牢的瞬間桶至,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,042評論 1 270
  • 我被黑心中介騙來泰國打工匾旭, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留镣屹,地道東北人。 一個月前我還...
    沈念sama閱讀 48,191評論 3 370
  • 正文 我出身青樓价涝,卻偏偏與公主長得像女蜈,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子飒泻,可洞房花燭夜當晚...
    茶點故事閱讀 44,955評論 2 355

推薦閱讀更多精彩內(nèi)容