如何在 Swift Playgrounds 睡觉并获取最新的异步值?

How do I sleep in Swift Playgrounds and get the latest async values?

我想 test/display Swift Playgrounds 中的行为与延迟后改变值的函数相关。为简单起见,我们只说它改变了一个字符串。我知道我可以通过 DispatchQueue.main.asyncAfter 延迟更新值的执行,并且我可以使用 usleepsleep.

休眠当前线程

不过,由于 playground 貌似 运行 在一个同步线程中,所以我在休眠后看不到变化。

这是我想做的一个例子:

var string = "original"

let delayS: TimeInterval = 0.100
let delayUS: useconds_t = useconds_t(delayS * 1_000_000)

func delayedUpdate(_ value: String) {
  DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + delayS) {
    string = value
  }
}

delayedUpdate("test2")
assert(string == "original")
usleep(delayUS)
print(string) // ❌ Prints "original"
assert(string == "test2") // ❌ Assertion failure. string is "original" here

delayedUpdate("test3")
assert(string == "test2") // ❌ Assertion failure. string is "original" here
usleep(delayUS)
print(string) // ❌ Prints "original"
assert(string == "test3") // ❌ Assertion failure. string is "original" here

delayedUpdate("test4")
assert(string == "test3") // ❌ Assertion failure. string is "original" here
usleep(delayUS)
print(string) // ❌ Prints "original"
assert(string == "test4") // ❌ Assertion failure. string is "original" here

注意所有失败的断言,因为顶层的任何内容都看不到对 string 的更改。这似乎是同步线程与异步线程的问题。

我知道我可以通过将 usleep 替换为更多 asyncAfter:

来修复它
delayedUpdate("test2")
assert(string == "original")
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + delayS) {
  print(string)
  assert(string == "test2")

  delayedUpdate("test3")
  assert(string == "test2")
  DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + delayS) {
    print(string)
    assert(string == "test3")

    delayedUpdate("test4")
    assert(string == "test3")
    DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + delayS) {
      print(string)
      assert(string == "test4")
    }
  }
}

然而,这会导致每次应用程序延迟时缩进代码的厄运金字塔。有 3 个级别,这还算不错,但如果我有一个大操场,这将变得非常难以遵循。

有没有办法使用更接近第一种线性规划风格的东西来尊重延迟后的更新?


另一个可能的解决方案是将每个对 string 的引用包装在 asyncAfter:

delayedUpdate("test2")
assert(string == "original")
usleep(delayUS)
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.001) { print(string) }
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.001) { assert(string == "test2") }

delayedUpdate("test3")
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.001) { assert(string == "test2") }
usleep(delayUS)
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.001) { print(string) }
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.001) { assert(string == "test3") }

delayedUpdate("test4")
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.001) { assert(string == "test3") }
usleep(delayUS)
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.001) { print(string) }
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.001) { assert(string == "test4") }

然而,这也不是首选,因为它也非常混乱,并且如果一次执行依赖于 string 的先前值来执行其功能,则可能容易出错,例如。它还需要 0.001 或类似的更正以确保没有竞争条件。


如何在 Swift 游乐场中使用线性编程风格(例如使用 sleep),但在 [=16] 之后的行中正确反映在睡眠期间更新的值=]?

您正在创建竞争条件。忘掉游乐场;只需考虑以下代码:

    print("start")
    DispatchQueue.main.asyncAfter(deadline:.now() + 1) {
        print("delayed")
    }
    sleep(2)
    print("done")

我们延迟 1 秒并打印“delayed”,我们休眠 2 秒并打印“done”。你认为哪个会先出现,“延迟”还是“完成”?如果你认为“delayed”会先出现,那是你不明白sleep是干什么的。它阻塞了主线程。延迟直到阻塞消失后才能重新进入主线程。

matt 的简化示例使这段代码的真正问题变得非常清楚。并不是主队列没有看到值的更新,而是代码的执行顺序与我预期的不同。

你可以认为执行顺序是:

  1. Swift 游乐场(主队列)顶层的所有行
  2. 主队列中安排的任何其他内容

这就解释了为什么代码会按这样的顺序执行:

print("start") // 1
DispatchQueue.main.asyncAfter(deadline:.now() + 1) {
    print("delayed") // 3
}
sleep(2)
print("done") // 2
// All async code on the main thread will execute after this line

注意它是如何以 132 顺序而不是所需的 123 顺序执行的。原因是 Playground 中的顶级代码是 运行 在主线程上,并且 .main 被提供给 DispatchQueue.

如果你用semaphore试一下,你也可以清楚地看到问题所在:

print("start") // 1
let semaphore = DispatchSemaphore(value: 0)
DispatchQueue.main.asyncAfter(deadline:.now() + 1) {
  print("delayed") // never executed
  semaphore.signal()
}
semaphore.wait() // This causes a deadlock
print("done") // never executed

semaphore.wait() 导致死锁,因为 semaphore.signal() 只能在打印 done 之后调用,但由于 wait.

以所需顺序获取代码 运行 的一种方法是将异步代码移动到不同的线程(例如 global()):

print("start") // 1
DispatchQueue.global().asyncAfter(deadline:.now() + 1) {
    print("delayed") // 2
}
sleep(2)
print("done") // 3

只需更改为使用 global 队列而不是 main 队列(并添加更多时间缓冲区)即可使原始代码按预期运行:

var string = "original"

let delayS: TimeInterval = 0.100
let sleepDelayUS: useconds_t = useconds_t(delayS * 1_000_000)
let sleepPaddingUS = useconds_t(100_000)

func delayedUpdate(_ value: String) {
  DispatchQueue.global().asyncAfter(deadline: DispatchTime.now() + delayS) {
    string = value
  }
}

delayedUpdate("test2")
assert(string == "original")
usleep(sleepDelayUS + sleepPaddingUS)
print(string) // ❌ Prints "original"
assert(string == "test2") // ❌ Assertion failure. string is "original" here

delayedUpdate("test3")
assert(string == "test2") // ❌ Assertion failure. string is "original" here
usleep(sleepDelayUS + sleepPaddingUS)
print(string) // ❌ Prints "original"
assert(string == "test3") // ❌ Assertion failure. string is "original" here

delayedUpdate("test4")
assert(string == "test3") // ❌ Assertion failure. string is "original" here
usleep(sleepDelayUS + sleepPaddingUS)
print(string) // ❌ Prints "original"
assert(string == "test4") // ❌ Assertion failure. string is "original" here

在 Playground 中测试延迟代码的另一个选项是使用 XCTWaitersince XCTest works in Playgrounds. If you want an even better way to test, you could use a custom dispatch queue meant for testing rather than literally waiting for time to pass. This episode on PointFree 解释了一种方法。