Monday, February 23, 2015

Async IO

Today, I added SendSocket<T> and ReceiveSocket<T> structs to Playform. These replace and subsume the spark_socket_sender and spark_socket_receiver functions that were there before, which would spawn a new thread to pump messages between a socket and async queue. I briefly debated adding support for a kill signal, to be sent from the parent, so the threads could cleanly kill themselves. But since the socket threads can be blocked indefinitely (e.g. waiting on a disconnected client), some mechanism of external kill by the parent needs to exist anyway, so we might as well use that all the time. So far, I've been returning the socket endpoints to the caller of spark_socket_{sender,receiver}, and the caller can shut them down, which causes the sparked thread to die. But that's far from clean, and the resources that were moved into the sparked threads would be kind of messily dealt with.

Now we have SendSocket::spawn and ReceiveSocket::spawn. They do the same thing, but return a SendSocket/ReceiveSocket, which hold all the state created. This requires that the threads spawned have a more constrained lifetime (so they can be put inside a data structure), which turns out to actually be a good thing: it means that the compiler now lets those threads make use of values that aren't around for as long, e.g. sockets created at runtime.

Adding these types meant changing a lot of signatures, though. A lot of Playform's threads look something like:
/// Keep the terrain around the client loaded.
pub fn surroundings_thread(
  client: &Client,
  ups_to_view: &Mutex<Sender<ClientToView>>,
  ups_to_server: &Mutex<Sender<ClientToServer>>,
) {
  ...
}
Because the high-level design is still evolving, these types change a lot. Sometimes references get added or removed, sometimes Mutexes get added or removed, and here, I'm changing from a Sender to SendSocket - a different type entirely. This is annoying.

My solution was to let the threads just say "I want to fire off an event of type X", without caring much about exactly how. I replaced the offending signatures with things like:
/// Keep the terrain around the client loaded.
pub fn surroundings_thread<UpdateView, UpdateServer>(
  client: &Client,
  update_view: &mut UpdateView,
  update_server: &mut UpdateServer,
) where
  UpdateView: FnMut(ClientToView),
  UpdateServer: FnMut(ClientToServer),
{
  ...
}
It's longer and clunkier, but it means I don't have to change these signatures nearly as much. What this does is say that the surroundings_thread can be run with any two functions that accept ClientToView and ClientToServer respectively. The exact nature of these functions isn't important (e.g. whether it's a plain-ol' rust function, a closure on the stack, or something else that just kinda works like a function), which is why the types of functions themselves are parameters (<UpdateView, UpdateServer>).

At use time, we specify what exactly it means to apply our updates:
thread::spawn(move || {
  surroundings_thread::surroundings_thread(
    client,
    &mut |msg| { client_to_view_send.lock().unwrap().send(msg).unwrap() },
    &mut |msg| { ups_to_server.lock().unwrap().send(msg).unwrap() },
  );
})

No comments:

Post a Comment