Sunday, 22 March 2020

Thread safety with network messages [1.14.4+]

Networking is performed in its own thread.  This means that your MessageHandler 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 MessageHandler 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.onMessageReceived() is called, you should create a lambda function and add it to a queue of tasks.
  2. It will look something like this:
    public static void onMessageReceived(final AirstrikeMessageToServer message, Supplier<NetworkEvent.Context> ctxSupplier) {
      ctx.enqueueWork(() -> processMessage(message, sendingPlayer));
  3. After adding the lambda to the queue, your handler onMessageReceived() should return.
  4. Later on, the MinecraftServer tick will take your lambda function  off the queue and execute it  in the Server thread, which for this example will call myMessageHandler.processMessage(message, sendingPlayer).
  5. 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 works in the same way.

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 Future 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.isOnExecutionThread().  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.

No comments:

Post a Comment