Swift中使用async let并发运行后台任务

Viewed 0

Swift 异步编程允许任务并发运行而非顺序执行,从而提升应用性能并保持UI响应。本文介绍如何在 Swift 中使用 async let 并发运行后台任务。

前言

Async/await 语法于 Swift 5.5 引入,提供了一种更可读的异步代码编写方式,比调度队列和回调更易于理解。它与其他语言如 C# 或 JavaScript 的语法类似。async let 用于并行运行多个后台任务并等待它们的结果。

长期运行的任务阻塞了UI

在同步程序中,代码线性执行,当前任务完成前会阻塞后续任务。当长期运行的任务(如下载文件)同步执行时,UI 会变得无响应,直到任务完成,这导致糟糕的用户体验。

以下代码模拟同步下载文件,UI 在任务完成前无法响应:

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 {
            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()
    }
}

使用 async/await 在后台执行任务

将 ViewModel 的 downloadFile 方法改为异步,确保模型更新在 UI 线程上执行(使用 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()
    }
}

这允许后台下载文件,同时 UI 保持响应并显示进度。

在后台执行多个任务

扩展以处理多个文件,ViewModel 持有一个 DataFile 数组:

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 let" 下载多个文件

使用 async let 并行下载多个文件,提升效率。它立即给变量赋值承诺,允许代码继续执行,然后等待所有承诺完成。

原异步代码:

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: 与先前类似,但标题改为 "Parallel" 以指示并行执行。

结论

在后台执行长期运行任务以保持UI响应至关重要。async/await 提供了清晰的异步任务执行机制。默认情况下,方法按顺序调用后台任务,但 async let 允许立即返回并并行执行多个任务,然后一起等待结果,从而提升并发性能。

0 Answers