如何在 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
延迟更新值的执行,并且我可以使用usleep
或sleep
休眠当前线程。
但是,由于 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 中睡觉并获取最新的异步值?的主要内容,如果未能解决你的问题,请参考以下文章
如何在 iPad 上的 Swift Playgrounds 中打印到控制台?
如何在 iPad 上让 Swift Playgrounds 无边界?