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

Posted

技术标签:

【中文标题】如何在 Swift Playgrounds 中睡觉并获取最新的异步值?【英文标题】:How do I sleep in Swift Playgrounds and get the latest async values? 【发布时间】:2020-07-10 20:27:18 【问题描述】:

我想在 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 Playground 中使用线性编程风格(例如使用 sleep),但在睡眠期间更新的值会被 sleep 之后的行正确反映?

【问题讨论】:

【参考方案1】:

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

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

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

【讨论】:

啊,太好了。感谢您提供的示例,它也让事情变得一目了然。我想我试图在操场的顶层运行异步代码,Swift 不支持。 当然你可以“在操场的顶层运行异步代码”。我的意思是,这不是你问的。你展示了一个自阻塞的竞争条件。如果你没有那个,你当然可以在操场上使用dispatchAfter【参考方案2】:

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

你可以把执行的顺序想象成:

    Swift Playground 顶层的所有行(主队列) 主队列中安排的任何其他内容

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

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 中测试具有延迟的代码的另一个选项是使用 XCTWaiter、since XCTest works in Playgrounds。如果您想要一种更好的测试方式,您可以使用用于测试的自定义调度队列,而不是等待时间过去。 This episode on PointFree 解释了一种方法。

【讨论】:

以上是关于如何在 Swift Playgrounds 中睡觉并获取最新的异步值?的主要内容,如果未能解决你的问题,请参考以下文章

Swift 中 Playgrounds 的用途是啥?

如何在 iPad 上的 Swift Playgrounds 中打印到控制台?

如何在 iPad 上让 Swift Playgrounds 无边界?

如何在 Swift Playgrounds 中使用带有纹理的 SKNode 设置 SKScene?

Swift Playgrounds 是如何工作的?

Apple 的 iPad 版 Swift Playgrounds 应用程序如何执行代码?