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

2023-06-25 10:11:44

前言

Async/await 语法是在 Swift 5.5 引入的,在 WWDC 2021中的 Meet async/await in Swift 对齐进行了介绍。它是编写异步代码的一种更可读的方式,比调度队列和回调函数更容易理解。Async/await 语法与其他编程语言(如 C# 或 JavaScript)中使用的语法类似。使用 "async let "是为了并行的运行多个后台任务,并等待它们的综合结果。

Swift 异步编程是一种编写允许某些任务并发运行而不是按顺序运行的代码的方法。这可以提高应用程序的性能,允许它同时执行多个任务,但更重要的是,它可以用来确保用户界面对用户输入的响应,同时任务在后台线程上执行。

长期运行的任务阻塞了UI

在一个同步的程序中,代码以线性的、从上到下的方式运行。程序等待当前任务完成后再进入下一任务。这在用户界面(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()
    }
}

模拟同步下载一个文件--没有实时更新UI

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

将 ViewModel 中的downloadFile方法修改为异步的。请注意,由于DataFile模型是被视图监听的,对模型的任何改变都需要在UI线程上执行。这是通过使用 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

在后台执行多个任务

现在我们有一个文件在后台下载,UI显示进度,让我们把它改为多个文件。ViewModel被改为持有一个DataFiles数组,而不是一个单一的文件。添加一个downloadFiles方法来遍历所有文件并下载每一个。

视图被绑定到DataFiles数组,并更新显示每个文件的下载进度。下载按钮被绑定到异步的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 " 下载多个文件

使用 "async let "来模拟并发下载多个文件的情况

上面的代码可以被改进,以并行地执行多个下载,因为每个任务都是独立于其他任务的。在Swift并发中,这是用async let实现的,它用一个承诺立即给一个变量赋值,允许代码执行下一行代码。然后,代码等待这些承诺,等待最终结果的完成。

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 "来模拟并行下载多个文件的情况

结论

在后台执行长期运行的任务并保持UI的响应是很重要的。async/await提供了一个干净的机制来执行异步任务。有的时候,一个方法在后台调用多个方法,默认情况下是按顺序进行这些调用。async 让其立即返回,允许代码进行下一个调用,然后所有返回的对象可以一起等待。这使得多个后台任务可以并行进行。

更多推荐

Java安全入门笔记(持续更新)

之前陆陆续续学过一点Java安全,笔记一直都没没有系统的写过,现在重新深入学一下之前的知识,会把笔记持续更新过来Java反射反射是java得一个重要特性,它可以获取一个类的所有信息,还可以执行类中的方法反射赋予Java动态特性我个人感觉静态语言的安全性是比较高的,因为一个供给使用的静态语言的程序的结构时固定的,能给攻击

Linux内核源码分析 (B.11) 从内核世界透视 mmap 内存映射的本质(原理篇)

Linux内核源码分析(B.11)从内核世界透视mmap内存映射的本质(原理篇)文章目录Linux内核源码分析(B.11)从内核世界透视mmap内存映射的本质(原理篇)1\.详解内存映射系统调用mmap2\.私有匿名映射3\.私有文件映射4\.共享文件映射5\.共享匿名映射6\.参数flags的其他枚举值7\.大页内存

说说Object类下面有几种方法呢?

今天说一道基础题型,不过很多人会忽略或者至少说不完整,但是面试时被问到的几率还是很大的。面试题Object有几种方法呢?Java语言是一种单继承结构语言,Java中所有的类都有一个共同的祖先。这个祖先就是Object类。如果一个类没有用extends明确指出继承于某个类,那么它默认继承Object类。Object的方法

【数据结构】—从直接插入排序升级到希尔排序究极详解(含C语言实现)

食用指南:本文在有C基础的情况下食用更佳🔥这就不得不推荐此专栏了:C语言♈️今日夜电波:透明で透き通って何にでもなれそうで—HaKU2:05━━━━━━️💟────────5:38🔄◀️⏸▶️☰💗关注👍点赞🙌收藏您的每一次鼓励都是对我莫大的支持😍目录♉️一、前置知识—什么是插入排序♊️二、直接插入排序直接

开源媒体浏览器Kyoo

什么是Kyoo?Kyoo是一款开源媒体浏览器,可让您流式传输电影、电视节目或动漫。它是Plex、Emby或Jellyfin的替代品。Kyoo是从头开始创建的,它不是一个分叉。一切都将永远是免费和开源的。软件特性:管理您的电影、电视剧和动漫自动下载元数据Transmux/Transcode文件以使它们在每个平台上可用具有

Redis常用数据类型及其应用场景

文章目录StringListHashSetSortedSetBitmapString字符串(String):可以存储任意类型的数据,比如字符串、整数、浮点数等。Redis中String数据类型的常用命令包括:设置值:SETkeyvalue,设置键key的值为value。获取值:GETkey,获取键key的值。获取值的长

产品新闻稿件怎么写?纯干货

写产品新闻稿件需要遵循一定的结构和写作规范,接下来伯乐网络传媒来给大家分享一下,以下是一些建议和步骤,供您参考:标题:使用简洁、准确的语言来概括产品的核心信息,吸引读者的兴趣。导语/导览:在开篇部分,以简洁明了的语句引起读者的兴趣,概述产品的主要特点和亮点。产品背景:介绍产品推出的背景和动机,例如市场需求、行业趋势或竞

【教程】AERMOD高斯稳态扩散模型

查看原文>>>基于AERMOD模型在大气环境影响评价中的实践应用随着我国经济快速发展,我国面临着日益严重的大气污染问题。近年来,严重的大气污染问题已经明显影响国计民生,引起政府、学界和人们越来越多的关注。大气污染是工农业生产、生活、交通、城市化等方面人为活动的综合结果,同时气象因素是控制大气污染的关键自然因素。大气污染

基础概念回顾:云原生应用交付

原文链接:基础概念回顾:云原生应用交付转载来源:NGINX开源社区NGINX唯一中文官方社区,尽在nginx.org.cn尽管云原生应用开发诞生于21世纪初,但是在术语使用方面还是非常混乱。本文将带您了解常见的术语和问题。云原生云原生计算基金会(CNCF)对“云原生”的定义如下:云原生技术允许企业在公有云、私有云和混合

数据结构-----栈(栈的初始化、建立、入栈、出栈、遍历、清空等操作)

目录前言栈1.定义2.栈的特点3.栈的储存方式3.1数组栈3.2链栈4.栈的基本操作(C语言)4.1初始化4.2判断是否满栈4.3判断空栈4.4入栈4.5出栈4.6获取栈顶元素4.7遍历栈4.8清空栈完整代码示例前言大家好呀!今天我们开始学习新的线性表结构----栈,前面我们学习了链表以及链表的相关操作,那么栈跟链表有

java高级:注解

目录认识注解&自定义注解元注解解析注解注解的应用场景认识注解&自定义注解注解和反射一样,都是用来做框架的,我们这里学习注解的目的其实是为了以后学习框架或者做框架做铺垫的。先来认识一下什么是注解?Java注解是代码中的特殊标记,比如@Override、@Test等,作用是:让其他程序根据注解信息决定怎么执行该程序。比如:

热文推荐