Sunday 18 January 2015

Thread safety with network messages

Unlike previous versions of Minecraft, in 1.8 the networking is performed in its own thread.  This means that your IMessageHandler needs to be very careful not to access and client-side or server-side objects while it is running in its own thread.

For a brief background on multithreaded applications and their perils, see here.  The short summary is-  if your IMessageHandler accesses any client-side or server-side objects, for example World or Minecraft, it will introduce a type of bug called a "race condition" which will lead to random crashes and strange, intermittent bugs and weirdness.

The way to overcome this problem is illustrated in the diagram below.  The essential features are:
  1. When your message arrives and your myMessageHandler.onMessage() is called, you should create a "Runnable()" object and add it to the future tasks queue.  The Runnable object will look something like this
    player.getServerForPlayer().addScheduledTask(new Runnable()
    {
      public void run() {
        processMessage(message);
      }
    });
  2. After adding the Runnable to the queue, your handler onMessage() should return.
  3. Later on, the MinecraftServer tick will take your Runnable task off the queue and execute its run() method in the Server thread, which for this example will call myMessageHandler.processMessage(message).
  4. Because your message handler is now running in the server thread, it can call any server-side code it wants.
The protocol for messages sent to the client is the same, using the Minecraft "scheduledTasks" queue instead, Minecraft.getMinecraft().addScheduledTask().

For a working example of client and server messages, see here (example 60).



Some notes on the vanilla thread-safety code

The vanilla code uses the same basic strategy as outlined above, with a couple of differences

  • After queuing up the Runnable task, it uses a ThreadQuickExitException to abort further processing in the Netty thread.  Java coding experts generally regard this as a misuse of Exceptions, which are supposed to be for exceptional conditions only, not routine flow.  It was probably implemented this way to save some programming effort which was presumably in short supply at the time.  The proper way is to have one method for queuing the Runnable task, and a second method for processing the message on the client/server thread; the vanilla method uses the same method for both threads.
  • You will also notice the vanilla code uses thread.isCallingFromMinecraftThread().  This is only necessary because of the ThreadQuickExitException.  You don't need it, because unlike vanilla your handler.onMessage() will always be running in the Netty thread and your handler.processMessage() will always be running in the client / server thread.

Anyway the short summary is - ignore the vanilla PacketThreadUtil.checkThreadAndEnqueue(), it is not good programming style.


3 comments:

  1. Sending all messages straight to the main thread defeats the purpose of a second thread. It'd be nice to have an overview of what actually can be done in the network thread, i.e. which parts of the Minecraft code can safely be called.

    ReplyDelete
  2. Hi
    As far as I can tell, almost none of the vanilla minecraft code is thread safe. Nearly all of the vanilla network code just immediately re-queues itself onto the client or server thread. Perhaps it is a work in progress.
    Given my painful experiences with multithreaded debugging in the past, I personally play it safe and never call any minecraft code at all from the network thread.

    ReplyDelete
  3. Michael,

    I imagine it is still an advantage to have 2 threads like this. At the very least you might be able to do some plain old calculating in the separate thread.

    ReplyDelete