与会话类型的跨线程通信

会话类型是一种告诉编译器你希望用于在线程之间进行通信的协议的方法 - 不是 HTTP 或 FTP 中的协议,而是线程之间的信息流模式。这很有用,因为编译器现在会阻止你意外破坏协议并导致线程之间的死锁或活锁 - 一些最难以调试的问题,以及 Heisenbugs 的主要来源。会话类型与上述通道的工作方式类似,但开始使用时可能会更加令人生畏。这是一个简单的双线程通信:

// Session Types aren't part of the standard library, but are part of this crate.
// You'll need to add session_types to your Cargo.toml file.
extern crate session_types;

// For now, it's easiest to just import everything from the library.
use session_types::*;

// First, we describe what our client thread will do. Note that there's no reason
// you have to use a client/server model - it's just convenient for this example.
// This type says that a client will first send a u32, then quit. `Eps` is
// shorthand for "end communication".
// Session Types use two generic parameters to describe the protocol - the first
// for the current communication, and the second for what will happen next.
type Client = Send<u32, Eps>;
// Now, we define what the server will do: it will receive as u32, then quit.
type Server = Recv<u32, Eps>;

// This function is ordinary code to run the client. Notice that it takes    
// ownership of a channel, just like other forms of interthread communication - 
// but this one about the protocol we just defined.
fn run_client(channel: Chan<(), Client>) {
    let channel = channel.send(42);
    println!("The client just sent the number 42!");
    channel.close();
}

// Now we define some code to run the server. It just accepts a value and prints
// it.
fn run_server(channel: Chan<(), Server>) {
    let (channel, data) = channel.recv();
    println!("The server received some data: {}", data);
    channel.close();
}

fn main() {
    // First, create the channels used for the two threads to talk to each other.
    let (server_channel, client_channel) = session_channel();

    // Start the server on a new thread
    let server_thread = std::thread::spawn(move || {
        run_server(server_channel);
    });

    // Run the client on this thread.
    run_client(client_channel);

    // Wait for the server to finish.
    server_thread.join().unwrap();
}

你应该注意到,如果将服务器移动到其自己的函数,则 main 方法看起来与上面定义的跨线程通信的主方法非常相似。如果你要运行它,你会得到输出:

The client just sent the number 42!
The server received some data: 42

以该顺序。

为什么要经历定义客户端和服务器类型的所有麻烦?为什么我们要重新定义客户端和服务器中的频道?这些问题有相同的答案:编译器将阻止我们破坏协议! 如果客户端试图接收数据而不是发送它(这会导致普通代码死锁),程序将无法编译,因为客户端的通道对象*上没有 recv 方法。*此外,如果我们尝试以可能导致死锁的方式定义协议(例如,如果客户端和服务器都尝试接收值),那么在创建通道时编译将失败。这是因为 SendRecv双重类型,这意味着如果服务器执行了一个,则客户端必须执行另一个 - 如果两者都试图通过 Recv,那么你将遇到麻烦。Eps 是它自己的双重类型,因为它同意客户端和服务器同意关闭通道。

当然,当我们在通道上进行一些操作时,我们会转到协议中的新状态,并且我们可用的功能可能会改变 - 所以我们必须重新定义通道绑定。幸运的是,session_types 为我们处理这个问题,并且总是返回新的频道(close 除外,在这种情况下没有新的频道)。这也意味着通道上的所有方法也会获得通道的所有权 - 因此,如果你忘记重新定义通道,编译器也会给你一个错误。如果你删除一个通道而没有关闭它,那也是一个运行时错误(不幸的是,在编译时无法检查)。

除了 SendRecv 之外,还有更多类型的通信 - 例如,Offer 为通道的另一端提供了在协议的两个可能分支之间进行选择的能力,RecVar 协同工作以允许协议中的循环和递归。session_types GitHub 存储库中提供了更多会话类型和其他类型的示例。库的文档可以在这里找到