Date

Notes on structured concurrency, or: Go statement considered harmful

本文为 Trio 作者对于并发 API 的一点看法,并阐述了 Trio 为什么有一个 nurseries 的概念
对于 Golang 的 go 关键字及与其相似的 API 来说,它具有一下缺点: - 破坏抽象:当我们调用一个函数时,它可能创建了一个 goroutine。函数看起来返回了,但是还有任务在后台执行。这种行为破坏了黑盒原则 - 影响自动资源释放:比如 Python 的 with open("my-file") as file_handle: 能够确保在整个 block 执行完后,对资源进行挥手。但是如果在这个 block 中创建了一个后台任务,with 不能保证在资源清理时后台任务已经结束,我们可能会面临使用一个被释放资源的危险 - 对异常处理并不友好:现代语言提供了异常处理机制,当异常发生时可以选择捕获或者向调用者进行传播。但对于新线程来说它应该将异常传递给谁,这是一个问题。目前大部分做法是向控制台打印错误,然后线程结束。另外 JavaScript 提供了 Promise.catch 的机制来捕获异常,但这和语言自身的异常处理似乎有些冗余。补充一点 asyncio 里,如果一个 Task 抛出了异常。await task 时会向外传播,也就是说异常不是传给创建这个 Task 的人,而是想要获取 Task 结果的人。如果没人想要获取这个结果,那么在结束时会打印一个 Task exception was never retrieved 的消息

Trio 的设计别具一格,它有一个 nurseries(托儿所)来管理所有创建的并发任务

async with trio.open_nursery() as nursery:
    nursery.start_soon(myfunc)
    nursery.start_soon(anotherfunc)

myfuncanotherfunc 都在 nursery 的控制之下。只有所有的任务都执行完,这个 block 才会结束,保证了资源回收的正常工作。当然有些情况下我们需要一些 fire-forget 的后台任务,Trio 也能够做到。如果某个任务抛出异常,nursery 会接收到,如果想要继续向外层抛出,nursery 会首先停下所有的并发任务然后才去做这件事情

Unyielding

作者首先表明了自己的立场 “Threads Are Bad”,因为 shared state 会增加我们的脑力负担。提出了下面四种替代方案:

  • Straight callbacks: Twisted’s IProtocol, JavaScript’s on<foo> idiom, where you give a callback to something which will call it later and then return control to something (usually a main loop) which will execute those callbacks,
  • “Managed” callbacks, or Futures: Twisted’s Deferred, JavaScript’s Promises/A[+], E’s Promises, where you create a dedicated result-that-will-be-available-in-the-future object and return it for the caller to add callbacks to,
  • Explicit coroutines: Twisted’s @inlineCallbacks, Tulip’s yield from coroutines, C#’s async/await, where you have a syntactic feature that explicitly suspends the current routine,
  • and finally, implicit coroutines: Java’s “green threads”, Twisted’s Corotwine, eventlet, gevent, where any function may switch the entire stack of the current thread of control by calling a function which suspends it.

前三种我们能够看到显式的调度,而第四种隐式协程其实是线程的伪装,它可能给你埋坑。当我们将代码修改成基于 green thread 的版本时,可能根本不需要去修改代码。但是我们需要熟悉何时会发生切换,以及我们代码原来是怎么写的。作者在此举了一个在转账代码上添加一个通过 Network I/O 的输出日志的例子。而显式的 yield 需要我们去修改源代码,不过这会让我们清晰的认识到是否应该在此发生切换。就如同 Zen of Python 所言:Explicit is better than implicit.

So don’t be fooled: a thread is a thread regardless of its color. If you want your program to be supple and resilient in the face of concurrency, when the storm of concurrency blows, allow it to change. Tell it to yield, just like the reed. Otherwise, just like the steadfast and unchanging oak tree in the storm, your steadfast and unchanging algorithms will break right in half.

其实,在解决并发问题上并没有捷径