写在前面:这是一篇近一年前的草稿了,翻出来发现,关于 Task
(已改名为 Thread
)退出的一些做法仍然适用,而且 zmq.rs 0.2 不出意外也要用到,所以仍然把这篇写完贴出来备查。但请注意,文中关于 libgreen
的一些描述已不属实。
这一篇隔的时间比较长,期间我们的游戏在准备上线,所以也没时间写 Rust,着重在课余时间研读了各种文档、源码和 issue report——关于 Rust 的 I/O。
从试图杀掉一个 Task
开始
我的 zmq.rs 项目终于开始碰到网络操作了。由于 Rust 给封装出来的 I/O 接口都是相对于 Task
同步的,所以目前来看还得给每一批 I/O 操作创建一个 Task
。
这里得多插一点内容了。首先是关于 ZeroMQ,它的一个 socket 是可以跟很多个别的 ZMQ socket 通讯的,而且是可以通过不同的 endpoint 来完成。比如一个 REP 服务端 socket,同时监听了 8080 和 9090 两个端口(endpoint),每个端口可能都有数十个 REQ 客户端 socket 连接上去;更有甚者,这个 REP socket 也可以主动去连接某些客户端 socket 监听的 endpoint,上门服务。而在所有这些网络拓扑结构的上面,一个 REP socket 对程序员的接口是始终一致的,您只需要反复地从这个 REP socket 读一个数据包,然后发一个数据包就好了。
这样一来呢,我就得给 REP socket 底层的每一个 TCP socket 连接创建至少一个 Task
,以满足其并发性。之前我们有提到,Rust 提供了 libgreen
和 libnative
两种运行时环境,对应了两种不同的 Task
实现模型。对于 zmq.rs 来说,基于目前的阻塞式的 I/O 接口来看,我们很有可能需要创建大量的 Task
——这对于 libgreen
的 M:N
模型来说是轻而易举的,但对于 libnative
来说却是值得商榷的,因为 1:1
的模型意味着我们将会用大量操作系统的线程来微操极少量的 ZMQ socket,这对于追求高并发的 ZMQ 也许并不是一个好主意——虽然 Rust 的 Task
模型能极大地避免常规多线程编程中最糟心的那一部分,但是内存占用会不会太高(哈!高!-2015),上下文切换的代价会不会太大等问题还有待于进一步测试。如果结果不理想,也许每批 I/O 一个 Task
的这种设计就要被推倒,新的设计将需要 Rust 提供异步的 I/O 接口。(0.2 确实将这么改 -2015)
回到原来的话题。因为创建了好多 Task
,一定会碰到的问题就是怎么结束它们,所以我一上来就打算先看一眼这个问题。没想到这一看,看出了好多问题。
因为受 Python greenlet 的严重影响,我自然而然地以为,结束一个 Task
应该用 kill()
——对于一个暂停状态的微线程,扔进去一个 GreenletExit
异常是多么正常的一种方式。可是 Rust 不那么认为。我也想到了由于需要支持 libnative
,停止一个阻塞在 I/O 调用中的线程绝非扔一个 GreenletExit
那么简单,但我还是义无反顾地去搜各种 rust task kill terminate shutdown 之类的关键词。
结果逐步明朗,原来 Rust 在 0.9 之前确实有过 Task
的 kill
功能,是通过 supervisor 模型实现的——即一对 Task
,任何一个挂掉都会导致另一个挂掉。但是呢,由于一些原因这个功能被砍掉了,也就是说,在 Rust 里,一个 Task
只能从内部抛错误死掉,没有办法从外部直接杀掉。另外,这个还(居然!)导致了我的第一个 stackoverflow 回答。
正确结束一个 Task
既然无法从别的 Task
中主动杀掉一个 Task
,那么就想办法让这个 Task
自杀。一个长时间运行的 Task
通常处于两种状态:1、执行代码;2、等待事件。
对于一个正在执行代码的 Task
,我们是无法让 Task
自己忽然想到该结束了然后戛然而止。只有在某些特殊情况下,我们才能手工写一些代码,让程序执行一段代码之后,去检查一下是否应该结束了,比如在一个死循环里:
rust
while self.running { // Do everything else }
而大多数其他情况下,从事件等待中跳出来结束一个 Task
更为常见。这里也分两种情况:A、等待 I/O 事件;B、等待 channel
事件。两种情况处理都比较简单,A 的话就给调用加一个稍短的超时,然后重复前面的那个例子,比如:
rust
tcp_stream.set_read_timeout(Some(1000)); while self.running { let result = tcp_stream.read_byte(); // Do the rest }
而对于等待一个 channel
的 Task
来说就更容易了,只要 channel
的另一端销毁了,这个等待的调用就会自动结束,只需要正确处理调用结果就好了。
其实,上面两个例子中的 self.running
应(至少)为一个 Arc
,因为需要从别的 Task
中来设置这个值。这里还有一种也是用 channel
的处理方式,就是在每次循环的开始处,向一个连接到父 Task
的 channel
的 Sender
端,发送一个空白消息,这样如果另一端已经销毁了,发送会失败,也就意味着我们该退出这个 Task
了,比如这样:
rust
let (tx, rx) = channel(); // ... in task let mut a = TcpListener::bind("127.0.0.1:8482").listen().unwrap(); a.set_timeout(Some(1000)); loop { match a.accept() { Ok(s) => { tx.send(Some(s)); } Err(ref e) if e.kind == TimedOut => { tx.send(None).unwrap(); } Err(e) => println!("err: {}", e), // something else } }