Formal threading model for the API

The MC server has basically a single “main” thread and lots of other minor threads.

Almost all events occur on the main thread (except those that don’t) and similarly almost all methods in the API had to be called from the main thread (again except those that didn’t).

In future versions, worlds might be handled on their own thread. Glowstone already does this and there has been suggestions that MC might go that way too.

If we say that calling any API method from inside an event handler is always safe, then we could end up in difficulties when someone tries to alter blocks in one world during an event in a different world.

Alternatively, we could say that inter-world interactions are potentially not thread-safe. For the time being that is over cautious.

Whatever we decide, we should have some kind of annotation to indicate which methods can be called in a thread-safe manner.

In Spout, we went to far down the road of making everything thread-safe and that harmed performance. For Sponge, we can’t change the internal thread model anyway, so that is not an issue.

This could be included into the API by adding a base type of an event for async events.

AsyncEvent extends Event

This means that the event is async. Only thread-safe API methods can be called and methods in the event itself.

@Threadsafe

This is a thread-safe method and can be called at any time.

I think that any calls that interact directly with the MC core would be hard to make “anytime” thread-safe. In Spout we had a “snapshot-lock”. The idea was that if you locked that, you were sure that you wouldn’t update when data needed to be stable (e.g. when sending block updates to the network).

This is as an alternative to submitting a task. You can write a thread and it will sync to the main thread.

Lock apiLock = game.getScheduler().geAPILock();

apiLock.lock();

try {

} finally {
apiLock.unlock();
}

When you lock the api lock lock, your thread would wait until the main thread is ready. On the main thread, at some point in the tick sequence, it would check if any locks were pending. If so, it would release the api-lock and wake up the waiting threads.

This means that if you lock the api lock and then Thread.sleep(…), it is like doing it on the main thread.

If threading is handled at the World level, then there could get .getAPILock(World world) for that world’s API lock. This would allow interworld interactions.

7 Likes

Personally I think threadsafety should be achieved using a task/invoke-system (whatever suits). This means that you queue parts of your code to run either on the main thread, or async on a threadpool. This can be nested into a continuation-passing style, keeping it fairly simple.

The problem with locking is that if you make it the main model, I imagine a lot of unexperienced plugin developers not realizing the disastrous effects of locking it for a long time. Not to say that it shouldn’t be allowed, but IMO it would be wiser to default to a non-blocking solution (e.g. scheduling sync/async).

1 Like

The big question is what the official threading model is. It should be implementation independent, but we don’t want inefficiencies due to a big difference.

If we say the API assumes a single “main thread” and the MC server moves away from that, then we have to either do lots of syncing (inter world in the example where each world gets a thread) or change the thread model, which is a breaking change for plugins that assume they can do stuff that has suddenly become thread un-safe.

When using the proposal here here one could, instead of server.scheduler.async, provide server.scheduler.fromContext(someWorld). This is futureproof, maybe add some other context. Initially said method would return one thread/context for whatever world, but when MC changes the internals, one could change the thread/context returned to match Minecrafts’.

That is interesting. The threading check is handled when the event is registered, rather than when the event happens. That means the overhead is once off.

Consider a “portal” system. The event could be PlayerMoveVoxelEvent. This fires whenever a player moves from one block to another. When a player teleports, it creates a portal in the other world.

The context can’t be known in advance.

There could be a PlayerChangeWorldEvent, which would fire when a player moves from one world to another. That could fire in the destination world’s thread.

We could include in the javadocs on which thread the event fires. Plugins would have to handle the cases where syncing is required. That requires that we decide in advance what are reasonable contexts.

So basically one would be able to specify in context of executing a ‘task’ what ‘systems’ (worlds) are involved.

If we defined what is and isn’t safe then plugin authors would have to do the whole invokeLater() thing.

If we could make sure that events generally happen on the thread that makes the most sense, then this mostly won’t be a problem. Using the world thread example, all world related events would naturally happen on the world thread, so the plugin’s handler could just call the api directly.

I would just like to point out that I believe that with 1.8, all worlds have their own thread versus having a single main thread.

I gave this some thought and I agree with a task-based and scheduler-based API. However, a task-based API could be built on top of the locking API you suggested, which I think would be great, for the main reason that normal threads could work with schedulers.
Here’s what it could look like:

public interface Lock {
    public void lock();
    public void unlock();
}

public interface Scheduler {
    public void schedule(Task task);
    public void stop(Task task);
    public Lock getLock();
}

public interface Task<I, O> {
    public O run(I input);
    public void dependOn(Task task);
}

public interface Producer<O> extends Task<Void, O> {
    public O run();
}

public interface Consumer<I> extends Task<I, Void> {
    public Void run(I input);
}

public interface Blind extends Task, java.lang.Runnable {
    public void run();
}

A runtime error is thrown if the task inputs and outputs don’t match.
The way you create and run tasks is using the scheduler, or maybe using a builder API as well.
The entire Rx-based system could be build on just this simple set of components.

Then there would be two main schedulers, per-world and synchronous. probably world.getScheduler() and server.getScheduler(), respectively. Events that happen ‘in between’ worlds happen in the server scheduler.

I’m also wondering if it would be a good idea to tie the events system to schedulers, e.g. the schedulers each have an event bus for which to get events. For the worlds this would be tricky, but there would probably be two ‘event buses’ under the server scheduler – one for synchronous and one for asynchronous events, as you said.

I think any event should be able to be asynchronous. Event should extend Async(able)Event, not the other way around. Let’s say another implementation than Forge decides to make more things asynchronous, then it should be able to call the events asynchronously without anything breaking. Is it really up to the API to decide what can be asynchronous and what can not? Giving more choice to the implementations will make the API fit in at more places.

But giving more choice to the implementation will possibly result in inconsistent results when the API is used with different implementations.