Swift中的异步编程方式
Swift中的异步编程方式
引
说到异步编程,我们很容易想到的编译回调。无论是需要并行的耗时任务,还是允许串行的简单任务,都通过回调的方式返回结果。回调也是在开发中使用最为广泛的一种异步编程方式。回想一下,通常的网络请求,文件操作等函数都会提供一个回调参数。回调使用起来虽然方便,但其并不利于进行程序流程的控制,仅仅从代码层面看,也很难组织清楚代码的执行顺序和逻辑。
Swift从代码层面提供了结构化的方式来支持异步编程,在Swift5.5中引入了async和await相关的关键字。需要注意,异步和并行本身是两个概念,在Swift中,异步编程模型已经建立在线程调度之上,这也就是说,我们无需关心其中线程的调用,异步的函数本身就是在子线程中并行执行的,线程切换和调度全有语言本身控制。但是Swift不会保证函数会在哪个特定的线程上执行。
异步函数
在尝试Swift中提供的异步编程方式外,可以先回想下对于异步并行的场景,之前是如何处理的,例如下面的代码:
func test(callback: @escaping (_ success: Bool)->Void) { DispatchQueue.global().async { print("Test任务完成", Thread.current) callback(true) } } print("Begin", Thread.current) test { success in print("EndTest") } print("End", Thread.current)
其中test函数所执行的任务是手动放在全局队列中执行的,DispatchQueue会自动的进行线程的调度,将其分配到空闲的子线程来执行。打印结果如下:
Begin <_NSMainThread: 0x600002310100>{number = 1, name = main} End <_NSMainThread: 0x600002310100>{number = 1, name = main} Test任务完成 <NSThread: 0x600002300300>{number = 5, name = (null)} EndTest
上面的示例代码比较简单,如果有更多的异步任务是依赖test任务的,则闭包回调的写法就会变得非常丑陋,代码难以阅读和维护。
在Swift5.5之后,我们可以使用async关键字来定义异步函数,编程模型会自动分配线程执行,例如:
func test1() async -> Bool { print("ts1", Thread.current) return true } func test2() async -> Bool { print("ts2", Thread.current) return true } print("Begin", Thread.current) async let a = test1() async let b = test2() print("End", Thread.current)
上面代码中,async关键字将函数标记为异步的,异步函数是一种特殊的函数,其支持在执行过程中被暂时的挂起,即暂停。对于普通的函数来说,会有3种状态:
1. 执行完成
2. 抛出异常
3. 永不返回
异步函数对应的也会有这3种状态,不同的是,当需要做某些等待操作时,其可以暂时的挂起。运行上面的代码,打印效果如下:
Begin <_NSMainThread: 0x60000329c3c0>{number = 1, name = main} End <_NSMainThread: 0x60000329c3c0>{number = 1, name = main} ts1 <NSThread: 0x60000328cc40>{number = 4, name = (null)} ts2 <NSThread: 0x600003282d40>{number = 6, name = (null)}
可以看到,test1和test2两个任务是异步执行,且被分配在了不同的线程。需要注意,理论上在异步函数中是不允许使用Thread相关接口的,因为任务的挂起和恢复所在线程都是由系统调度的,逻辑上开发者无需关心线程问题,在Swift6版本中继续这样使用将会报错。
上面演示的代码中,test1和test2任务的执行并不依赖关系,如果test2和执行是需要test1执行结束的,则可以直接使用await关键字来将运行挂起,直到结果返回。例如:
func test1() async -> Bool { print("ts1", Thread.current) return true } func test2() async -> Bool { print("ts2", Thread.current) return true } print("Begin", Thread.current) let a = await test1() let b = await test2() print("End", Thread.current)
打印效果如下:
Begin <_NSMainThread: 0x600002180140>{number = 1, name = main} ts1 <NSThread: 0x600002198100>{number = 6, name = (null)} ts2 <NSThread: 0x6000021accc0>{number = 8, name = (null)} End <_NSMainThread: 0x600002180140>{number = 1, name = main}
使用await关键字标记的地方为程序的挂起点,此时会停止当前线程上代码的执行,并等待异步函数的返回,在程序中,支持await进行挂起的场景包括:
1.异步的方法,属性或函数中。
2.main代码块中。
3.非结构化的子任务代码块中。
通常,我们直接使用await调用异步函数时,当前执行会被挂起,更多时候可以使用如下方式来同时执行多个异步函数,使用await来最终获得结果:
func test1() async -> Bool { print("ts1", Thread.current) return true } func test2() async -> Bool { print("ts2", Thread.current) return true } print("Begin", Thread.current) async let a = test1() async let b = test2() let res = await [a ,b] print(res) print("End", Thread.current)
异步序列
Swift中的迭代也支持异步返回,通过AsyncIteratorProtocol协议来定义异步的迭代器,示例如下:
struct Group: AsyncSequence { typealias Element = Int let limit: Int struct AsyncIterator : AsyncIteratorProtocol { let limit: Int var current = 1 mutating func next() async -> Int? { guard !Task.isCancelled else { return nil } guard current <= limit else { return nil } let result = current current += 1 return result } } func makeAsyncIterator() -> AsyncIterator { return AsyncIterator(limit: limit) } } print("Begin") let group = Group(limit: 10) for await i in group { print(i) } print("End")
在对异步迭代器进行遍历时,需要使用for await in的语法方式。
任务组与任务
当有多个异步任务需要执行时,可以将其添加到一个任务组中,当任务组所有任务完成后再进行统一的返回。例如:
let res = await withTaskGroup(of: Bool.self, returning: [Bool].self, body: { taskGroup in taskGroup.addTask { let a = await test1() return a } taskGroup.addTask { let b = await test1() return b } var datas:[Bool] = [] for await data in taskGroup { datas.append(data) } return datas }) print(res)
其中,withTaskGroup方法将创建一个异步的父任务,其中可以添加多个子任务,任务组之间有非常明确的关系,这种编程方式也被称为结构化编程,当然,Swift也提供了非结构化的编程方式,即需要开发者处理任务之间的关系。这非常有用,有时我们需要在非并发的环境中调用异步函数,例如在iOS Application中ViewController的viewDidLoad方法中调用一个异步的函数,此时就需要为其包装一个并发环境,使用Task来创建任务即可:
class ViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() Task(priority: .background) { await test() self.view.backgroundColor = .red print("Finish") } // 上面task的执行不会影响当前线程 print("Continue") } func test() async { try? await Task.sleep(for: .seconds(10)) } override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) { print("touch") print(Thread.current) } }
这里再强调一下,所谓执行任务的挂起和线程的阻塞完全不同,当并发环境中当前任务被挂起时,线程资源会被释放去执行其他任务,直到异步任务有结果后,在恢复执行。上面代码中并没有记录Task实例,其实此实例可以控制任务的取消,获取任务返回值等操作,例如:
override func viewDidLoad() { super.viewDidLoad() let task = Task(priority: .background) { await test() self.view.backgroundColor = .red print("Finish") return "result" } // 上面task的执行不会影响当前线程 print("Continue") // 取消任务 task.cancel() }

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
实时数仓混沌演练实践
一、背景介绍 目前实时数仓提供的投放实时指标优先级别越来越重要,不再是单独的报表展示等功能,特别是提供给下游规则引擎的相关数据,直接对投放运营的广告投放产生直接影响,数据延迟或者异常均可能产生直接或者间接的资产损失。 从投放管理平台的链路全景图来看,实时数仓是不可或缺的一环,可以快速处理海量数据,并迅速分析出有效信息,同时支持投放管理平台的手动控盘。实时节点事故,将可能导致整个投放链路无法正常运行,另外,投放规则引擎是自动化操作,服务需要24小时运行,所以需要配置及时有效的数据质量监控预警,能快速识别到波动异常或者不符合业务的数据,从而计划引入混沌工程,希望可以通过主动注入故障的方式、尽可能提前感知风险、发现潜在问题,并针对性地进行防范、加固,避免故障发生时所带来的严重后果,提高实时数仓整体抗风险能力。 二、演练范围 为了能更细致反应出混沌演练情况,根据演练的内容不同,将实时数仓混沌分为两部分:技术侧和业务侧。 技术侧混沌:基于中间件、数据库、JVM、基础资源、网络、服务等注入常见的异常,根据实际业务中梳理的应用核心场景进行混沌演练,检验系统的脆弱性和应急响应能力,从而提升团队的稳定性...
- 下一篇
openGemini开源一周年,打造极具影响力的时序数据库技术社区
时间转眼来到9月,openGemini社区迎来了一个重要的里程碑:openGemini开源一周年。 openGemini是华为云数据库创新Lab团队自主设计、研发的一款面向全球开源的云原生分布式时序数据库。 作为一款新兴的时序数据库,openGemini在短短一年内取得了令人瞩目的成就,为开发者和企业提供了强大的时序数据存储和分析解决方案。 在这篇文章中,我们将回顾openGemini的发展历程,探讨其对相关应用领域的影响,并展望未来的发展方向。 openGemini诞生记 openGemini发展时间轴 openGemini的诞生源于华为云对海量时序数据处理需求的日益增长。截至2023年,华为云已上线240多个服务,覆盖了29个地理区域的75个可用区,用户数量数十倍增长。华为云发展驶入快车道的同时,运维压力也随之增加,实时监控告警、故障排查、根因定位、容量规划和性能优化等业务场景每天采集云服务和基础设施的监控指标数据超过20TB,每秒入库数据超过4000万条,每秒查询次数超过5万,时间线规模超过10亿,这对时序数据库的性能和数据压缩率都提出了极高的要求。 openGemini团队在技...
相关文章
文章评论
共有0条评论来说两句吧...