Swift 新结构化并发中鲜为人知的 isolated 参数

在这里插入图片描述

概述

伴随着 Swift 5.5(WWDC21)推出的新结构化并发到今年的 WWDC 24 已经有 3 个多年头了。想必大家都对其中 async/awiat、async let、TaskGroup、Actor 等各种概念都了然于胸了吧?

不过小伙伴们可能不知道的是:新结构化并发(或叫现代结构化并发)中还有一个“隐藏宝藏”,它就是 isolated 参数。

Swift 现代结构化并发中 isolated parameters 的存在对于某些应用场景有着不可或缺的重要意义!不信?且看分晓!

Let‘s go!!!😉


本文对应的视频课在此,请小伙伴们恣意观赏:

Swift 新结构化并发中鲜为人知的 isolated 参数


1. isolated 参数(isolated parameters)简介

在 Swift 5.5 新并发模型中有一个 isolated parameters 的概念。它很好理解:isolated 其实是一个关键字,isolated 可以用在修饰方法或闭包的参数上。

空说无凭,撸码为证!

在下面的代码中我们将 isolated 关键字应用在了 run 方法的 god 参数上:

actor GodActor {}

func run(_ god: isolated GodActor) {}

与此类似,isolated 关键字同样可以应用在闭包中的参数上:

actor GodActor {
    func handler<R>(
        _ action: @Sendable (_ god: isolated GodActor) throws -> R
    ) rethrows -> R {
        try action(self)
    }
}

值得注意的是:isolated 关键字修饰的参数类型必须是一个 Actor。

在这里插入图片描述

如果违反这一条,编译器就会立即毫不留情的“勃然大怒”:

‘isolated’ parameter type ‘Int’ does not conform to ‘Actor’ or ‘DistributedActor’

在了解 isolated parameters 的用法之后,满腹狐疑的小伙伴们肯定要问了:那么它到底是干嘛滴的呢?

2. isolated parameters 的作用

isolated 参数的作用非常简单:无论它被用来修饰方法或是闭包中的 Actor 参数,都意味着该方法或闭包在运行时都会限定在此 Actor 所在的上下文中。

An isolated parameter means the function runs on whatever actor is passed in.

所以任何方法或闭包都只能有一个被 isolated 修饰的 Actor 参数,否则天知道它要被用在哪个 Actor 上了。

还拿之前的 run() 方法来说,它有点像下面代码的意思:

extension MyActor {
    func run() { ... }
}

不过,包含 isolated 参数方法的重要意义在于:我们可以将特定 Actor 的同步上下文传递到任何方法或闭包的执行中去了。

不知小伙伴们发现了没有,上面包含 isolated 参数的 run() 或 handler() 方法本身都没有再被 async 所修饰,这是有意而为之的!因为像其它异步并发隔离(isolation)一样,它们在调用时要不要加上 await 关键字取决于当时的执行语境。


这有点像某种“隐式异步”方法。更多关于“隐式异步”方法的介绍请小伙伴们移步如下链接观赏精彩的内容:


说了上面这么一大堆,可能有的小伙伴还是搞不清 isolated 参数存在的真谛吧?

别急,下面我们就用一个活灵活现的例子让大家彻底茅塞顿开!

3. isolated parameters 的应用场景

假设我们要绕过 CoreData 直接写一个 SQLite 数据库的包装器。

我们会用该包装器来执行 SQLite 数据库中的一些 SQL 命令,比如查询、插入、修改和删除等等。为了处理好数据库操作中的同步问题,我们新创建一个 Connection Actor 来排忧解难:

public actor Connection {
	public func execute(_ query: String) throws {
		//...
	}
}

比如,当我们想向数据库中插入对象时可以这么写:

let conn = Connection()
await conn.execute("INSERT INTO table1 VALUES ('a', 'b', 'c')")

但是,随后我们可能发现每次执行单个 SQL 语句是效率极其低下的,我们更希望能够以“原子的”方式一次性执行多条 SQL 语句。

所谓以“原子的”方式意思是:

  • 要么所有 SQL 语句都执行成功,数据库被正确更新;
  • 若其中有任何一条 SQL 语句出错,那么就好像所有语句都没有被执行一样——数据库保持原封不动;

这种以“原子的”执行方式称为事务(Transactions),SQLite 数据库或任何其它现代数据库都对其提供了更好的支持。我们利用这一点可以很轻松的在 Connection 中实现一个 transaction 方法来“拔刀相助”:

public actor Connection {

  ...

  @discardableResult
  func transaction<R>(
    _ action: @Sendable (_ connection: isolated Connection) throws -> R
  ) throws -> R {
    try execute("BEGIN TRANSACTION")
    do {
      let result = try action(self)
      try execute("COMMIT TRANSACTION")
      return result
    } catch {
      try execute("ROLLBACK TRANSACTION")
      throw error
    }
  }
}

现在,我们可以这样调用 transaction 方法来实现 SQLite 包装器对事务的支持了:

let conn = Connection()
conn.transaction { 
  $0.execute("INSERT INTO table1 VALUES ('a', 'b', 'c')")
  $0.execute("INSERT INTO table2 VALUES ('d', 'e', 'f')")
}

可能眼尖的小伙伴们已经发现了:上面 transaction 方法闭包中的 connection 参数是被 isolated 关键字所修饰着的。

按照之前的解释,这说明 transaction 方法闭包会在 connection Actor 的上下文中执行,它由此引来的重要推论是:transaction 闭包中所有代码的执行都不会被打断!

这一点非常关键!

如果我们不用 isolated 来修饰 transaction 方法闭包中的 connection 参数,那么它就会是下面这个样子:

public actor Connection {

  ...

  @discardableResult
  func transaction<R>(
    _ action: @Sendable (_ connection: Connection) async throws -> R
  ) throws -> R {
    ...
  }
}

connection.transaction { 
  await $0.execute("INSERT INTO table1 VALUES ('a', 'b', 'c')")
  await $0.execute("INSERT INTO table2 VALUES ('d', 'e', 'f')")
}

如上代码所示,现在 transaction 方法中的闭包必须被 async 所修饰,这带来的直接后果就是:其内部的所有 execute() 方法的调用前面都要加上 await 关键字!

回忆一下,任何用 await 关键字所修饰方法的执行都有可能被挂起!大家知道在并发执行中挂起意味着指令流可能会被打断,从而引起重入(Reentrancy)问题。


重入问题会导致隔离一致性被打破。更多关于 Actor 重入问题的讨论请小伙伴们移步如下链接观赏:


回到上面的例子,执行用 await 修饰的两条 execute() 方法是非常危险的!因为这可能会导致我们的事务执行到一半被挂起(suspend),如果此时相同的 Connection 对象中有另一个任务开始执行就会发生嵌套(nested)事务的错误(在调用 COMMIT TRANSACTION / ROLLBACK TRANSACTION 之前又调用了 BEGIN TRANSACTION)。

而使用 isolated 关键字则恰恰可以避免这种情况!因为这时 transaction 方法中任何与 Connection 相关方法的调用都无需用 await 修饰,从而不会发生潜在的挂起行为。

这就是 isolated parameters 存在的真谛啊!棒棒哒!


更多关于 Swift 新结构化并发中同步问题的例子,请小伙伴们到下面的博文中观赏精彩的内容:


总结

在本篇博文中,我们介绍了 Swift 现代并发模型中少有人知的 isolated parameters 机制,并用了一个非常通俗易懂的“栗子”让大家豁然开朗!

虽然 isolated parameters 不是那种我们在撸码中天天都会用到的解决方案,但在某些场景下它的确能够为我们扶危拯溺,雪中送炭!

感谢观赏,再会!😎

  • 37
    点赞
  • 36
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 14
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 14
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

大熊猫侯佩

赏点钱让我买杯可乐好吗 ;)

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值