7、Swift协程详解:异步函数的调度和GlobalActor

什么是 GlobalActor

前面我们为了保护特定的状态,就把这些状态包装到一个特定的 actor 实例当中,保护的方式就是将对于这些状态的访问调度到相应的 actor 的调度器当中串行执行。

那么问题来了,如果我有很多分散到不同类甚至不同模块的状态,希望统一调度,该怎么办?最典型的例子就是将 UI 操作调度到主线程,UI 本身就分散在不同的组件当中,对于 UI 的操作更是如此。为了应对这种场景,Swift 在提供了 actor 的基础上又进一步提供了 GlobalActor,旨在提供全局统一的执行调度。

GlobalActor 是一个协议,我们来看一下它的定义:

  • ActorType 是实现 GlobalActor 协议的类型需要提供的全局唯一的 actor 的类型
  • shared 是上述全局唯一的 actor 的实例
  • sharedUnownedExecutor 是上述全局唯一的 actor 实例的调度器,它的值要求与 shared.unownedExecutor 一致

所以只要确定了 shared 是谁,那么这个 GlobalActor 也就确定了。

此外,一个类型在实现 GlobalActor 时,我们还可以用 @globalActor 来修饰它,这样我们就可以用这个实现类去修饰需要使用该 GlobalActor 的实现类来隔离的函数或者属性了。这么说比较抽象,我们接下来看看官方目前提供的唯一一个 GlobalActor 实现是怎么定义的。

探索 MainActor

MainActor 是目前唯一一个 GlobalActor 的实现,它用来将对属性或者函数的访问隔离到主线程上执行。我们来看看它的定义:

我们看到 MainActor 是一个 actor 类型,这对于 GlobalActor 协议来说不是必须的,我们完全可以定义一个 class 来实现 GlobalActor,并且把一个 actor 类型关联到 GlobalActor 上即可。当然,如果条件允许,直接用 actor 类型来实现 GlobalActor 自然更方便一些。

我们在前面提到过, sharedUnownedExecutor 要与 shared.unownedExecutor 一致,这里很显然二者本质上都是 Builtin.buildMainActorExecutorRef()

此外, MainActor@globalActor 修饰之后,自己就可以被用于修饰属性、函数或者类型,我们给出几个简单的 MainActor 的例子:

修饰属性:

修饰函数:

修饰闭包:

修饰类:

@MainActor 修饰的函数在调用时,如果当前不在主线程,则必须异步调度到主线程上执行;同样地,被修饰的属性在被其他线程访问时,也必须异步调度到主线程上处理。

@MainActor 修饰的类的构造器、属性、函数都需要调度到主线程上执行。需要注意的是,为了保证继承的一致性,被修饰的类需要满足或没有父类、或同样被 @MainActor 修饰、或父类是 NSObject;被修饰的类的子类也将会隐式获得 @MainActor 上的状态隔离。

这里的异步访问逻辑实际上与 actor 类型的状态和函数的关系相同,即被 @MainActor 修饰的函数内部访问同样被 @MainActor 修饰的属性时则不需要异步执行,就好像它们都被定义到 MainActor 这个 actor 类型当中一样。

以上使用方法和细节同样适用于其他 GlobalActor 的实现。

自定义 GlobalActor 的实现

了解了 MainActor 的定义之后,我们就可以试着给出自定义的 GlobalActor 实现了,例如:

其中自定义的调度器 MyExecutor 的定义如下:

大家可以简单阅读代码的注释来了解他们的作用。注意到 MyActor 也实现了 GlobalActor 协议,我们也使用 @globalActor 来修饰 MyActor,这样我们就可以用 @MyActor@MainActor 那样去修饰函数、属性和类,并让它调度到我们自己实现的调度器上了。

有关 MyActor 的使用示例,我们将在下一节进一步讨论。

注意 截至本文撰写时,Swift 的最新版本为 5.5.1。当前 Swift 协程对于自定义调度器的支持还在提案阶段,细节可参见:Custom Executors

深入探讨 Actor 与协程的调度

Swift 的协程在执行调度问题上目前还比较含蓄,文档当中很少提及异步函数的执行以及异步函数返回时如何恢复。实际上,异步函数所在的调用位置会关联一个调度器,这个调度器要么来自于所在的 Task,要么来自于当前函数所属于的 actor 实例。

Swift 定义了两个默认的调度器,一个是并发的,一个是串行的;另外就是我们前面提到的,用于将异步函数调度到主线程上的主线程的调度器。

为了搞清楚 Swift 协程究竟是如何调度的,我们用 MainActor 和自定义的 MyActor 来调度我们的异步函数,看看有什么新发现。

在下面的例子当中,我们使用 @MainActor 修饰函数 calledOnMain:

接下来创建一个 Task 来调用它:

这里我们使用 log 这个定义的函数来打印输出,它与 print 的不同之处在于它会同时打印当前线程:

可以看到,calledOnMain 被调度到了 MainThread 上执行。task start 和 task end 执行所在的线程相同(当然也可以不同,但一定是相同的调度器所属的线程),这说明 calledOnMain 返回之后 Task 又被调度与之关联的调度器上执行。

@MainActor 也可以被用于修饰闭包的类型,例如:

我们试着调用一下这个函数:

运行结果如下:

这次只有 block 才会被调度到 MainThread 上,因为只有它被 @MainActor 修饰。

从这个例子当中我们其实还能推测出调度发生的位置,即:

  • 异步函数开始执行
  • 异步函数返回之处

实际上除此之外,Task 开始时也可能会发生一次调度。这些都是可能的调度位置,Swift 的运行时会根据实际情况判断调度前后是不是属于同一个调度器,以决定是不是真的需要发生调度。这些也能从我们待会儿的例子当中得到印证。

接下来我们使用 MyActor 依样画葫芦,完成类似的例子:

首先是函数的定义:

然后调用它们:

运行结果如下:

注意到 calledOnMyExecutor 调用时、 runOnMyExecutor 当中的 block 执行时、 block 当中的 sleep 之后恢复时分别执行了一次 enqueue。大家有兴趣的话也可以在其中穿插一些需要调度到主线程的函数调用,看看实际的调度情况。

Task 与 actor 上下文

我们在前面介绍 Task 的构造时,讲到过可以使用 initdetached 两种方式来构造 Task 实例,前者会继承外部的上下文,包括 actor、TaskLocal 等,后者则不会。

下面的例子将会证明这其中有关 actor 的部分:

通过前面的介绍,我们已经知道 runOnMain 的参数 block 会被调度到 MainThread 上执行,那么其中的两个 Task 的日志输出理论上会有不同的表现:

实际上也正是如此, task in runOnMain 打印到了 MainThread 上,而 detached task in runOnMain 因为通过 detached 创建的 Task 实例不会继承外部的 actor(以及其调度器),因此打印到了其他线程上(也就是默认的调度器上)。

Task 的两种不同的构造方式对于 TaskLocal 的继承情况同样如此,我们将在下一篇文章当中再给出对比示例。

本文我们详细介绍了 GlobalActor 的设计初衷、实现方式以及使用方法,也探讨了 Swift 协程的调度细节,相信读者看到这里时,已经掌握了绝大多数 Swift 协程的相关知识。

下一篇文章我们将简单介绍一下 TaskLocal 的使用方法。

Original: https://www.cnblogs.com/strengthen/p/16243566.html
Author: 山青咏芝
Title: 7、Swift协程详解:异步函数的调度和GlobalActor

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/553571/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

  • 【转载】谈谈Unicode编码,简要解释UCS、UTF、BMP、BOM等名词

    谈谈Unicode 编码,简要解释UCS 、UTF 、BMP 、BOM 等名词 这是一篇程序员写给程序员的趣味读物。所谓趣味是指可以比较轻松地了解一些原来不清楚的概念,增进知识,类…

    技术杂谈 2023年7月24日
    077
  • sqlx操作MySQL实战及其ORM原理

    sqlx是Golang中的一个知名三方库,其为Go标准库database/sql提供了一组扩展支持。使用它可以方便的在数据行与Golang的结构体、映射和切片之间进行转换,从这个角…

    技术杂谈 2023年7月11日
    084
  • 二十一、XML

    二十一、XML 21.1 XML介绍 21.1.1 一个问题引入 XML 思考:前面的反射可以加载配置文件里的信息,获取类的字节码对象从而动态创建对象和调用方法,但是如果需要创建多…

    技术杂谈 2023年7月11日
    077
  • 工程的本质是分工、合作及效率

    工程的本质是分工、合作及效率 一个人最好的习惯是独立思考、全面思考、深度思考、勤于思考、理性冷静、静坐沉思、定时反思、有仪式感、二分格物、三思而行、适量运动、适量饮食、早睡早起,最…

    技术杂谈 2023年5月31日
    090
  • Spring事务(三)-事务传播行为

    在Spring里,一个事务方法被另外一个事务方法调用时,两个方法的事务应该如何进行,说白话一点,就是说当出现异常需要回滚时,各个方法的数据操作是否要全部回滚,事务传播行为就是决定了…

    技术杂谈 2023年7月11日
    078
  • 详解重绘与回流

    1、输入url ( 协议、网络地址、资源路径 ) 2、查看浏览器缓存,看是否有缓存,如果有缓存,继续查看缓存是否过期,如果没有过期,直接返回缓存页面,如果没有缓存或者缓存过期,发送…

    技术杂谈 2023年6月1日
    091
  • 1.层次遍历

    title: 层次遍历 📃 题目描述 题目链接:二叉树的层次遍历 🔔 解题思路 简简单单,用队列来保存每一层的数量,再进行遍历。 class Solution { public: …

    技术杂谈 2023年7月24日
    073
  • 手把手教你:人脸识别考勤系统

    系列文章 手把手教你:人脸识别考勤系统 本文为系列第一篇 @ 系列文章 项目简介 一、项目展示 二、环境需求 环境安装实例 三、功能模块介绍 1.人脸库图像 2.构建人脸库 3.启…

    技术杂谈 2023年7月25日
    071
  • 运算符(2)

    运算符 算术运算符补充 +的作用: 表示正数(省略不写) 表示相加操作 进行字符串的拼接:+号左右两侧的任意一侧有字符串,那么这个加号就是字符串拼接的作用,结果一定是字符串 【2】…

    技术杂谈 2023年7月11日
    062
  • MySQLmax()min()函数取值错误

    今天日志出现异常,一步一步debug发现SQL语句返回值出错,进一步发现是max()函数返回出错。点击跳转解决办法,赶时间的朋友可以去获得答案。当然我还是希望大伙看看原由。 sel…

    技术杂谈 2023年7月24日
    072
  • Dependency 源码

    1. 思考 : 依赖的本质是什么呢?作用是什么? 1. 在我们初始化RDD的时候, 需要指定 RDD的依赖关系 abstract class RDD[T: ClassTag]( @…

    技术杂谈 2023年7月10日
    068
  • 对两个数求解对大公约数

    对于这个最大公约数的球阀有两种, 第一种是: 自己手写规律: int lcm(int a,int b) {int max = (a >= b?a:b),min = (a &l…

    技术杂谈 2023年5月31日
    0107
  • HIT软构博客4-lab1记录与总结

    ​ 完成一个实验或小的项目使用java在需要的时候去搜索和看书比直接看很厚的书有意义一些,体验更加良好。自己对java的掌握不是很好,大一结束的夏天认真学习了java看了核心技术的…

    技术杂谈 2023年7月11日
    085
  • xxl-job 小结

    配置属性详细说明: <span class=”pun”>&#x57FA;&#x7840;&#x914D;&#x7F6E;&#xF…

    技术杂谈 2023年5月31日
    094
  • react新手demo——TodoList

    今天我们就使用 react 来实现一个简易版的 todolist ,我们可以使用这个 demo 进行 list 的增删改差,实际效果如上图所示。大家可以 clone下来查看:rea…

    技术杂谈 2023年5月31日
    098
  • HR易问问题列举

    Date: 2012-11-16 17:54:57 中国标准时间 Author: csophys Org version 7.8.11 with Emacs version 24V…

    技术杂谈 2023年7月24日
    083
亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球