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

news2025/1/9 2:17:48

在这里插入图片描述

概览

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 修饰的“隐式”异步方法也有可能不在主线程上下文中执行这一隐藏的陷阱,并对其原因和解决办法做了详细的说明。

感谢观赏,再会!😎

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Axure原型设计工具怎么样?有替代软件吗?

Axurerp是一种快速原型设计工具&#xff0c;可以制作高度互动的HTML原型。设计师不仅可以使用Axure绘制线框图和原型&#xff0c;还可以在Axurerp中完成一系列用户体验设计。在本文中&#xff0c;我们将根据用户体验设计师的真实经验&#xff0c;触发用户体验设计师的实际工作&…

绕过PPL机制窃取凭证

Mimikatz窃取凭证攻击 正常权限cmd情况下mimikatz是无法直接提权的,所以窃取密码更是不行的。 但管理员权限下的mimikatz是可以获取到主机登陆密码的。 PPL安全机制 在此之前,用户只需要使用SeDebugPrivilege令牌权限即可获取任意进程的所有访问权限;随后Windows8.1 在此…

阿里云 OSS使用介绍

1、什么是阿里云 OSS&#xff1f; OSS 为 Object Storage Service&#xff0c;即对象存储服务。是阿里云提供的海量、安全、低成本、高可靠的云存储服务。 OSS 具有与平台无关的 RESTful API 接口&#xff0c;可以在任意应用、任意时间、任意地点 存储与访问 任何类型的数据。…

算法的时间复杂度!!!很懵逼吧~

度量一个程序的执行时间通常有两种方法&#xff1a;事后统计的方法、事前估算法 1.事后统计的方法 这种方法可行,但是有两个问题: 一是要想对设计的算法的运行性能进行评测,需要实际运行该程序;二是所得时间的统计量依赖于计算机的硬件、软件等环境因素,这种方式&#xff0c;…

PostGIS学习教程五:数据

教程的数据是有关纽约市的四个shapefile文件和一个包含社会人口经济数据的数据表。在前面一节我们已经将shapefile加载为PostGIS表&#xff0c;在后面我们将添加社会人口经济数据。 下面描述了每个数据集的记录数量和表属性。这些属性值和关系是我们以后分析的基础。 要在pgAdm…

2021年09月 Scratch(一级)真题解析#中国电子学会#全国青少年软件编程等级考试

一、单选题(共25题,每题2分,共50分) 第1题 如下图所示,小明想要做一个文字逐字出现的动画效果,他画出了程序的流程图,以下哪个程序可以实现? A: B: C: D: 答案&#

PP-YOLO: An Effective and Efficient Implementation of Object Detector(2020.8)

文章目录 Abstract1. Introduction先介绍了一堆前人的work自己的workexpect 2. Related Work先介绍别人的work与我们的区别 3.Method3.1. ArchitectureBackboneDetection NeckDetection Head 3.2. Selection of TricksLarger Batch SizeEMADropBlockIoULossIoU AwareGrid Sensi…

Elasticsearch:ES|QL 快速入门

警告&#xff1a;此功能处于技术预览阶段&#xff0c;可能会在未来版本中更改或删除。 Elastic 将努力解决任何问题&#xff0c;但技术预览版中的功能不受官方 GA 功能的支持 SLA 的约束。目前的最新发行版为 Elastic Stack 8.11。 Elasticsearch 查询语言 (ES|QL) 提供了一种强…

MySQL8.0学习笔记

1. CMD命令 1.1 数据库启动与停止 (1) 启动数据库&#xff1a;net start mysql80 (2) 停止数据库&#xff1a;net stop mysql80 1.2 数据库连接与退出 (1) 连接数据库&#xff1a;mysql [-hlocalhost -P3306] -uroot -p[123456] // 本地数据库可省略-h -P (2) 退出数据库…

Opencv!!在树莓派上安装Opencv!

一、更新树莓派系统 sudo apt-get update sudo apt-get upgrade二、安装python-opencv sudo apt-get install libopencv-dev sudo apt-get install python3-opencv三、查看是否安装成功 按以下命令顺序执行&#xff1a; python import cv2 cv2.__version__如果出现版本号&a…

【luckfox】2、添加lcd spi屏st7735和gc9306

前言 本章使用fbtft添加spi lcd st7735/gc9306。 fbtft生成fb0设备&#xff0c;后续通过lvgl可以实现自定义界面绘制。 代码参考 https://gitee.com/openLuat/LuatOS/blob/master/components/lcd/luat_lcd_gc9306x.c 硬件是合宙的&#xff0c;合宙esp32有支持&#xff0c;仿…

Linux内存问题排查

目录 概念工具 概念 工具 vmstat&#xff1a;查看内存变化情况 通过vmstat&#xff0c;可以看到空闲列是否一直是减少的趋势&#xff0c;而缓冲和缓存一直不变&#xff0c;说明存在内存泄漏 top/htop ps pmap&#xff1a;查看进程的内存分布 bcc工具&#xff1a; memlea…

实战Leetcode(五)

Practice makes perfect&#xff01; 实战一&#xff1a; 思路&#xff1a;我们要用复制的节点来组成一个新的链表&#xff0c;而原链表的节点随机指向其中一个节点&#xff0c;我们首先给每一个节点都复制并且插入到原来节点的后面&#xff0c;然后用复制的节点指向我们原来节…

http接口测试—自动化测试框架设计

一、测试需求描述 对服务后台一系列的http接口功能测试。 输入&#xff1a;根据接口描述构造不同的参数输入值&#xff08;Json格式&#xff09; 输出&#xff1a;字符串&#xff08;传入的方式传入的字符串&#xff09; http://localhost:8090/lctest/TestServer 二、程序设计…

manim更新

manim升级18.0 # 1 更新pip&#xff0c;推荐轮子下载 python -m pip install --upgrade pip 推荐方式下载轮子安装 首先尝试在中断更新pip&#xff0c;通过命令python -m pip install --upgrade pip 可能遇到以下情况 记录最新的pip轮子名 记录下上面pip的名称&#xff0c;去…

【开源】基于Vue和SpringBoot的校园失物招领管理系统

项目编号&#xff1a; S 006 &#xff0c;文末获取源码。 \color{red}{项目编号&#xff1a;S006&#xff0c;文末获取源码。} 项目编号&#xff1a;S006&#xff0c;文末获取源码。 目录 一、摘要1.1 项目介绍1.2 项目录屏 二、研究内容2.1 招领管理模块2.2 寻物管理模块2.3 系…

nacos集群配置(超完整)

win配置与linux一样&#xff0c;换端口或者换ip&#xff0c;文章采用的 linux不同IP&#xff0c;同一端口 节点ipportnacos1192.168.253.168848nacos2192.168.253.178848nacos3192.168.253.188848 单IP多个端口 1.复制两个&#xff0c;重命名 2.修改 conf目录下的 application…

教务必备:php+Mysql多条件都输对版万用查分系统

查分吧PHP多条件都输对版已有表万用查询系统 V1.8 极简单文件实现一至多条件都输对成绩录取分班等通用查询。 支持隐藏指定列、支持网址列显示为图片或链接、支持验证码开关。 适合学校或教育机构信息中心技术员使用&#xff0c;快速部署并用于已有数据表查询。 无后台管理…

python爬虫 之 JavaScript 简单基础

文章目录 在网页使用JavaScript 代码的方式常用的JavaScript 事件常用的JavaScript 对象 在网页使用JavaScript 代码的方式 在网页中使用 JavaScript 代码的方式主要有三种&#xff1a; 内联方式&#xff08;Inline&#xff09;&#xff1a; 在 HTML 文件中直接嵌入 JavaScrip…

【C++代码】最接近的三数之和,括号生成,合并两个有序链表,合并 K 个升序链表

题目&#xff1a;最长公共前缀 编写一个函数来查找字符串数组中的最长公共前缀。如果不存在公共前缀&#xff0c;返回空字符串 ""。 class Solution { public:string longestCommonPrefix(vector<string>& strs) {string res"";int index 0; f…