Swift 警惕“隐式异步(implicitly asynchronous)”方法的执行陷阱

在这里插入图片描述

概览

actor 是 Swift 5.5+ 中一个“不可思议”的新类型,可以把它看做成一个数据同步器。actor 中所有属性和方法都会被自动“串行”(serializes)访问和执行,从而有效避免了数据竞争的发生。

不过,在一些微妙的情境下使用 actor 仍然可能出现数据竞争的潜在风险,这得从“隐式异步”方法谈起了…

在本篇博文中,您将学到以下内容:

  • 概览
  • 1. 编译器的神助攻!
  • 2. 谁说 actor 就不会发生数据竞争?
  • 3. 没有 async 修饰的也可能是“异步”方法?
  • 4. 如何让“异步”代码在同步上下文中执行?
  • 5. 让暴风雨来的更猛烈些:使用更严格的并发检查
  • 总结

1. 编译器的神助攻!

首先,我们创建一个简单的 Asyncor 累加器 actor,可以看到它的 value 属性和 inc() 方法都被要求在 MainActor 上执行,这样貌似可以确保数据的同步行为(真的吗?)。

actor Asyncor {
    @MainActor
    var value = 0
    
    @MainActor
    func inc() {
        value += 1
    }
}

如果我们尝试在同步上下文中调用它,编译器则会立即发现其中的“不和谐”:

struct Invoker {
    static func invoke() {
        let asyncor = Asyncor()
        asyncor.inc()
    }
}

在这里插入图片描述

这时,有两种修复方法:

  1. 在 Task 环境中使用 await 调用 Asyncor#inc() 方法;
  2. 或者将 Invoker.invoke() 方法也用 @MainActor 来修饰;
struct Invoker {
    // 解决方法1:
    static func invoke1() {
        Task {
            let asyncor = Asyncor()
            await asyncor.inc()
        }
    }
    
    // 解决方法2:
    @MainActor
    static func invoke2() {
        let asyncor = Asyncor()
        asyncor.inc()
    }
}

有了编译器的“火眼金睛”,我们可以立即找到异步代码中的问题,并迅速修正它们。

那么,编译器能否始终保持“滴水不漏”、“明察秋毫”呢?

2. 谁说 actor 就不会发生数据竞争?

我们再来看一个“栗子”,还拿上面的 Asyncor 说事。

现在,我们在两个后台队列中累加 Asyncor 的值 10000 次:

let group = DispatchGroup()

let queue_0 = DispatchQueue.global()
let queue_1 = DispatchQueue.global()

// 共累加 5000 * 2 = 10000 次
for i in 0..<5000 {
    queue_0.async(group: group) {
        print("q0: \(i)")
        asyncor.inc()
    }
    
    queue_1.async(group: group) {
        print("q1: \(i)")
        asyncor.inc()
    }
}

group.notify(queue: DispatchQueue.global()) {
    Task {@MainActor in
        print("10000 次累加的总和为:\(asyncor.value)")
    }
}

默认情况下,以上代码在编译时不会有任何错误。按道理来说,在 Asyncor actor 中的 value 属性和 inc() 方法都受 @MainActor 的保护,所以上面代码最后累加的结果一定是 10000!

真是这样么?

理想很丰满,现实却啪啪打脸!

在这里插入图片描述

从上面运行结果可以看到:最终的累加结果并不等于 10000。What’s wrong with it!?

我们经过分析发现 inc() 方法竟然不是在主线程而是在其它线程中执行的,这不禁令人“大跌眼镜”:难怪会出现数据竞争!

在这里插入图片描述

那么问题来了:为什么 @MainActor 修饰的 inc() 方法没有在主线程上执行呢?

3. 没有 async 修饰的也可能是“异步”方法?

细心的小伙伴们可能发现了一些蛛丝马迹:Asyncor actor 中的 inc() 方法并没有被 async 修饰!那么它到底是不是异步方法呢?

虽然 Asyncor#inc() 实例方法没有被 async 所修饰,但它前面赫然在列的 @MainActor 却强烈暗示我们它需要在主线程上执行。

我们称这种在 actor 内却未被 async 修饰的方法称为“隐式”异步方法。

何谓“隐式”呢?在 actor 中的方法如果未用 async 修饰,当从 actor 外部调用该方法时,它就变成一个“隐式(implicitly asynchronous)”异步方法。

“隐式”异步方法有如下特点:

  • 若它在异步上下文中调用需要加上 await 修饰,否则无法通过编译;
  • 若它在某些非异步环境(比如 DispatchQueue )中调用,可以通过编译但其异步约束(@MainActor)实际不会起作用;

相反的,对于一个“显式”异步方法(即被 async 修饰的方法),不用 await 修饰是无法过编译器这一关的:

actor Asyncor {
    @MainActor
    var value = 0
    
    @MainActor
    func inc() {
        value += 1
    }
    
    // async_inc() 是一个“显式”异步方法
    @MainActor
    func async_inc() async {
        value += 1
    }
}

Text("Hello Swift")
    .onAppear {
        DispatchQueue.global().async {
            let asyncor = Asyncor()
            
            // “隐式”异步方法可以不加 await 修饰,但结果可能不是我们想要的
            asyncor.inc()
            
            // “显式”异步方法必须要 await 修饰,以下一行代码无法通过编译
            asyncor.async_inc()
        }
    }

在这里插入图片描述


所以小伙伴们知道上面代码中产生数据竞争的原因了吗?虽然 Asyncor#inc() 被 @MainActor 修饰,但无奈何它是一个“隐式”异步方法且在非异步的上下文中执行,所以 @MainActor 没有起到实际的作用。

因为没有用 await 修饰,所以另一个隐含的意味是它的执行不会被挂起。

要想修复这个问题很简单,我们只需将 Asyncor#inc() 方法放在异步上下文中就可以遵守 @MainActor 的约束了:

actor Asyncor {
    @MainActor
    var value = 0
    
    @MainActor
    func inc() {
        value += 1
    }
}

let asyncor = Asyncor()

let group = DispatchGroup()

let queue_0 = DispatchQueue.global()
let queue_1 = DispatchQueue.global()

// 共累加 5000 * 2 = 10000 次
for i in 0..<5000 {
    queue_0.async(group: group) {
        print("q0: \(i)")
        Task {
            await asyncor.inc()
        }
    }
    
    queue_1.async(group: group) {
        print("q1: \(i)")
        Task {
            await asyncor.inc()
        }
    }
}

group.notify(queue: DispatchQueue.global()) {
    Task {@MainActor in
        print("10000 次累加的总和为:\(asyncor.value)")
    }
}

在这里插入图片描述

4. 如何让“异步”代码在同步上下文中执行?

如果是真·异步方法则是无论如何也不能在同步上下文中执行的。不过对于“隐式异步”方法来说,我们可以让其绕过编译器的检查。

比如,在下面的 invoke() 方法闭包中默认是无法执行“隐式”异步方法的:

func invoke(_ block: () -> Void) {
    block()
}

struct Invoke {
    func test() {
        invoke {
            let asyncor = Asyncor()
            asyncor.inc()
        }
    }
}

在这里插入图片描述

不过,我们可以用 @preconcurrency 修饰器来“骗取”Swift 编译器的信任:

@preconcurrency
func invoke(_ block: () -> Void) {
    block()
}

@preconcurrency 修饰的 invoke() 方法闭包中可以直接调用“隐式”异步方法。


在 SwiftUI 视图中也可以直接调用 invoke() 方法而无需 @preconcurrency 的修饰:

func invoke(_ block: () -> Void) {
    block()
}

struct ContentView: View {
    var body: some View {
        Text("Hello Swift")
            .onAppear {
                invoke {
                    let asyncor = Asyncor()
                    asyncor.inc()
                }
            }
    }
}

这是因为 SwiftUI 视图的 body 本身就被 @MainActor 修饰着:

@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
public protocol View {
    
    associatedtype Body : View

    @ViewBuilder @MainActor var body: Self.Body { get }
}

不过强烈不推荐大家这样做!

因为这样一来,那些“隐式”异步方法的执行环境可能不是我们想要的。

5. 让暴风雨来的更猛烈些:使用更严格的并发检查

其实,在最新的 Xcode 中使用如上伎俩还是会被编译器有所察觉:

在这里插入图片描述

如果我们希望让编译器变得更加“严厉”,可以在项目的编译设置中选择更加严格的并发代码检查选项:

在这里插入图片描述

如果大家喜欢自我挑战,可以选择最严格的 Complete 并发检查选项,这是 Swift 6 中默认的“味道”。

所以,这样我们就可以提前感受和拥抱 Swift 6 的降临了,棒棒哒!💯。


更多 Swift 语言中并发编程的相关知识,请小伙伴们移步到我的专题专栏中系统的学习吧:

  • Swift 语言开发精讲(文章平均质量分 97)

总结

在本篇博文中,我们讨论了被 @MainActor 修饰的“隐式”异步方法也有可能不在主线程上下文中执行这一隐藏的陷阱,并对其原因和解决办法做了详细的说明。

感谢观赏,再会!😎

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/145215.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

笔记51:循环神经网络入门

本地笔记地址&#xff1a;D:\work_file\DeepLearning_Learning\03_个人笔记\3.循环神经网络\循环神经网络 a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a

29.第三方登录

1►第三方登录 当今社会&#xff0c;微信登录、QQ登录、抖音登录等等三方登录已经层出不穷&#xff0c;学会三方登录势在必行。 微信登录要认证开发者&#xff0c;必须为企业&#xff0c;个人不行&#xff0c;而且还要交300块钱。 QQ登录也要申请、微博登录也要申请。 还好…

ppt画思路图 流程图 医学药学生画图素材

关注微信&#xff0c;回复: 素材 &#xff0c;即可领取

基于 React 的 HT for Web ,由厦门图扑团队开发和维护 - 用于 2D/3D 图形渲染和交互

本心、输入输出、结果 文章目录 基于 React 的 HT for Web &#xff0c;由厦门图扑团队开发和维护 - 用于 2D/3D 图形渲染和交互前言什么是 HT for WebHT for Web 的特点如何使用 HT for Web相关链接弘扬爱国精神 基于 React 的 HT for Web &#xff0c;由厦门图扑团队开发和维…

基于闪电搜索算法优化概率神经网络PNN的分类预测 - 附代码

基于闪电搜索算法优化概率神经网络PNN的分类预测 - 附代码 文章目录 基于闪电搜索算法优化概率神经网络PNN的分类预测 - 附代码1.PNN网络概述2.变压器故障诊街系统相关背景2.1 模型建立 3.基于闪电搜索优化的PNN网络5.测试结果6.参考文献7.Matlab代码 摘要&#xff1a;针对PNN神…

任意文件读取漏洞 (Arbitrary File Read/Download Vulnerability)

任意文件读取漏洞 文章目录 任意文件读取漏洞漏洞场景漏洞危害漏洞分类任意文件读取重要函数readfile()file_get_contents()fread()$_GET任意文件读取 任意文件下载html实现a标签PHP实现任意文件下载 任意⽂件读取攻防过滤../防守绕过 任意文件读取挖掘漏洞防御 ​ 一些网站的需…

十年测试告诉你35岁测试程序员,互联网技术岗,何去何从?

今年的就业情形&#xff0c;想必大家都深有感触。企业裁员&#xff0c;求职市场岗位大幅减少&#xff1b;薪资降低&#xff0c;岗位能力要求越来越高&#xff1b;好像一瞬间大家都从万米高空坠落&#xff0c;失重带来的眩晕和迷茫&#xff0c;让求职者和招聘企业都显得有点手忙…

路由器ipsec|vpn实验分析

AR1 和 AR2代表两个公司的出口&#xff0c;R2模拟互联&#xff0c;两个公司通信&#xff0c;通过ipsec vpn 加密隧道进行业务通信 切记&#xff1a;ipsec 路由器一定用AR系列&#xff0c;千万别用R&#xff0c;否则会给你惊喜 R2只有接口配ip&#xff0c;无任何配置&#xff…

【Err】jetBrains远程开发报错:Failed to exec spawn helper: pid: 18310, exit value: 1

最近双11阿里云打折&#xff0c;买了台服务器做了下远程开发环境&#xff0c;在IDEA远程开发时遇到了个问题&#xff0c;导致项目启动失败&#xff0c;报错如下&#xff1a; JetBrains远程开发报错 Failed to exec spawn helper: pid: 18310, exit value: 1 &#xff08;我改好…

解析SQL 获取表、字段及SQL查询参数

解析SQL 获取表、字段及SQL查询参数 1. 执行效果2. 使用2.1 引入依赖2.2 相关实体2.3 工具类 1. 执行效果 2. 使用 2.1 引入依赖 <!-- sql 解析处理--><dependency><groupId>com.github.jsqlparser</groupId><artifactId>jsqlparser</artifa…

Potrace:一个基于多边形的跟踪算法

Potrace算法通过几个步骤将位图转换为矢量轮廓。 第一步&#xff0c;将位图分解为若干条路径&#xff0c;在黑白区域间形成边界。 在第二步中&#xff0c;每条路径由一个最优多边形逼近。 在第三步中&#xff0c;每个多边形被转换成光滑的轮廓。 在可选的第四步中&#xff0c;通…

【管理运筹学】运筹学“背诵手册”(二) | 对偶理论与灵敏度分析

二、对偶理论与灵敏度分析 用矩阵形式表示原问题和对偶问题&#xff1a; max ⁡ z C X s . t . { A X ≤ b X ≥ 0 \max z\pmb{CX}\\ s.t.\begin{cases} \pmb{AX\leq b} \\ \pmb{X}\geq\pmb{0} \end{cases} maxzCXs.t.{AX≤bX≥0​ 其中 C ( c 1 , c 2 , ⋯ , c n ) , X (…

Java入门篇 之 继承

本篇碎碎念&#xff1a;最近的课程遇到瓶颈了&#xff0c;看的时候感觉自己会了&#xff0c;但是结束仔细一回顾还是一知半解&#xff0c;一点一点来吧&#xff0c;基础必须要打好(自己给自己好的心里暗示&#xff0c;结局一定是好的) 今日份励志文案:慢慢改变&#xff0c;慢慢…

四、Ribbon负载均衡

目录 一、负载均衡流程 1、我通过浏览器直接访问userservice/user/1&#xff0c;无法访问&#xff0c;说明是负载均衡做了相应的处理 2、我们来看一下代码中负载均衡的流程是怎样的 3、图像流程 二、负载均衡策略 1、修改负载均衡策略 &#xff08;方式一&#xff09; &a…

Spring面试题:(七)Spring AOP思想及实现

AOP思想的概念 AOP的实现&#xff1a;动态代理技术 通过spring容器获取目标对象和增强对象&#xff0c;通过动态代理生产代理对象&#xff0c;在目标对象的目标方法执行增强方法&#xff0c;返回生成代理对象给spring容器&#xff0c;在获取bean时则获取代理对象。 JDK代理和…

【源码运行打包】kkFileView 下载与安装

目录导航 1、源码下载2、IDEA部署2.1、克隆代码2.2、配置maven2.3、下载依赖报错2.4、执行maven打包 3、Centos7.9部署启动3.1、环境要求3.2、部署jdk环境3.3、上传部署包3.4、解压部署包3.5、访问测试3.6、解决乱码 4、使用指南5、部署包下载 文件预览服务 kkFileView &#x…

【Spring进阶系列丨第一篇】初识Spring开发

前言 小伙伴们大家好&#xff0c;我是陈橘又青&#xff0c;今天起 《Spring进阶系列》 开始更新。本专栏将涵盖Spring框架的核心概念、配置管理、Web开发、AOP、Boot、Security、Data、Integration和Batch等多个主题。通过理论讲解和实际案例的剖析&#xff0c;帮助读者深入理解…

【Linux】Ubuntu16.04下完美安装python高版本及对应版本的pip

Ubuntu16.04下完美安装python高版本及对应版本的pip 方法一:直接用命令安装python3.6&#xff08;但我没安装成功&#xff09; 好像是因为Ubuntu16.04的软件仓库&#xff08;源&#xff09;中python的最高版本就是python3.5&#xff0c;所以无法直接用apt来安装 #方法一 sudo…

金财数科无代码开发平台:轻松实现电商、CRM、广告推广系统的集成连接

连接与集成&#xff1a;挖掘电商平台的潜力 金财数科是一家领先的信息技术公司&#xff0c;专注于利用前沿技术如互联网、人工智能、大数据和区块链等&#xff0c;为传统财税信息化方案和产品提供升级改造&#xff0c;并打造新一代智能财税SaaS平台。我们的目标是帮助企业通过…

Nodejs操作缓存数据库-Redis

Hi I’m Shendi Nodejs专栏 Nodejs操作缓存数据库-Redis 在服务端开发中&#xff0c;缓存数据库也是不可或缺的&#xff0c;可以提高程序并发以及方便后续扩展&#xff0c;而目前最常用的莫过于Redis了 安装依赖 和之前的mysql一样&#xff0c;redis的依赖最常用的就是redis …