我有以下代码,我不确定它是否安全:
extension Timer {
@MainActor // <- 1
static func myScheduled(
interval: TimeInterval,
block: @escaping @MainActor @Sendable () -> Void) -> Timer // <- 2
{
return scheduledTimer(withTimeInterval: interval, repeats: true) { _ in
MainActor.assumeIsolated(block) // <- 3
}
}
}
在上面的代码中,第一个
@MainActor
注释(代码中标记1)保证必须从主参与者隔离上下文中调用myScheduled
函数,这意味着Timer.scheduledTimer
将把计时器放在RunLoop.main
上,这意味着必须在主线程上调用计时器回调。
我的
block
需要在主要演员上调用一些API,所以我必须将其标记为主要演员(代码中标记2)。但是,由于编译器不知道计时器回调将在主线程上运行,因此我必须在这里使用 MainActor.assumeIsolated(block)
(代码中的标记 3)来消除编译器警告。
我的问题是,做出这个假设总是安全的吗?我确信计时器回调发生在主线程上,但是,我不确定假设主线程上下文必须是主隔离的是否总是安全的。
来自
MainActor.assumeIsolated
的API文档:
此方法允许假设并验证当前正在执行的同步函数实际上是在 MainActor 的串行执行器上执行。
这个检查是针对MainActor的串行执行器执行的,这意味着/如果另一个actor使用相同的串行执行器——通过使用sharedUnownedExecutor作为自己的unownedExecutor——这个检查将会成功,因为从并发安全的角度来看,串行执行器保证了互斥那两个演员。
换句话说,如果某个东西在主线程上运行,我们确定它一定是由这个主要参与者的“执行者”执行吗?
主要参与者的执行者是主调度队列,它是与主线程关联的调度队列,所以是的,你的代码是安全的。
MainActor
:
一个单例参与者,其执行者相当于主调度队列。
但是,正如
assumeIsolated
的文档所述,这只能作为最后的手段。在这种情况下,更好的方法是使用 Timer
来消耗 Task
。现在block
甚至不需要是@Sendable
。
@MainActor
static func myScheduled(
interval: TimeInterval,
block: @escaping @MainActor () -> Void) -> Task<Void, Never>
{
Task {
for await _ in publish(every: interval, on: .main, in: .default).values {
block()
}
}
}
Task.init
创建一个在当前参与者(即主要参与者)上运行的任务。
通过返回
Task
,调用者仍然可以通过调用 invalidate
来Task.cancel
计时器。