Skip to content

Commit ee431c9

Browse files
authored
Create writing-a-multithreaded-plugin.md
1 parent 42c1b23 commit ee431c9

File tree

1 file changed

+183
-0
lines changed

1 file changed

+183
-0
lines changed
Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
---
2+
title: Writing a multithreaded plugin
3+
layout: default
4+
---
5+
6+
# Writing a multithreaded plugin
7+
8+
The Minecraft server runs game code primarily on a single thread. This is huge limitation as a single thread can only be so fast.
9+
10+
Luckily, there are server cores that allow the game code to run on multiple threads.
11+
These include [Folia](https://papermc.io/software/folia) and [ShreddedPaper](https://github.com/MultiPaper/ShreddedPaper).
12+
13+
## What is a thread?
14+
15+
### Single threaded
16+
17+
A thread is what your code executes on. One thread can execute one piece of code at a time.
18+
This means if your plugin is single-threaded, only one part of the plugin will be executing at a given time.
19+
20+
```mermaid
21+
flowchart LR
22+
A(Method A)
23+
B(Method B)
24+
C(Method C)
25+
D(Method D)
26+
A --> B --> C --> D
27+
```
28+
29+
### Asynchronous calls
30+
31+
When you make an asynchronous call on your single-threaded plugin, a second thread is spawned to execute the call.
32+
This is good for when you want to execute slow I/O tasks without it blocking your primary thread.
33+
34+
With Bukkit, you can create an asynchronous call with `Bukkit.getScheduler().runTaskAsynchronously`.
35+
Some events like `AsyncPlayerPreLoginEvent` run asynchronous by default.
36+
37+
```mermaid
38+
flowchart LR
39+
A(Method A)
40+
B(Method B)
41+
C(Method C)
42+
D(Method D)
43+
Async(Asynchronous I/O call)
44+
A --> B --> C --> D
45+
A --> Async --> D
46+
```
47+
48+
### Multithreaded
49+
50+
When your plugin is multithreaded, it means that there is more than just a single thread executing your code.
51+
This results in many parts of your plugins being executed at the same time as eachother.
52+
53+
Issues will arise if you try to run a single-threaded plugin in a multithreaded environment, and these will be covered below.
54+
55+
```mermaid
56+
flowchart LR
57+
A(Method A)
58+
B(Method B)
59+
C(Method C)
60+
D(Method D)
61+
A --> B
62+
C --> D
63+
```
64+
65+
## The APIs for multithreaded plugins
66+
67+
There are certain APIs that are specific to multithreaded plugins.
68+
These APIs exist in all recent versions of Paper, however if you want your plugin to be compatible with older versions and Spigot, check out [MultiLib](https://github.com/MultiPaper/MultiLib?tab=readme-ov-file#shreddedpaper--folia-methods).
69+
70+
### plugin.yml
71+
72+
Firstly, in your `plugin.yml`, you will need to tell the server that your plugin is a multithreaded plugin.
73+
74+
Do that by adding `folia-supported: true`:
75+
76+
```yml
77+
name: MyPlugin
78+
version: 1.0.0
79+
main: com.exmaple.MyPlugin
80+
api-version: 1.20
81+
folia-supported: true
82+
```
83+
84+
### Accessing the world
85+
86+
Each thread is only able to access the region of the world that it is in charge of.
87+
88+
First, check if your thread is in charge of that region of world:
89+
90+
```java
91+
Location location = new Location(Bukkit.getWorld("world"), 0, 0, 0);
92+
if (Bukkit.isOwnedByCurrentRegion(location)) {
93+
// We are in charge of this location! Let's modify the block here
94+
location.getBlock().setType(Material.AIR);
95+
}
96+
```
97+
98+
Otherwise, if we aren't in charge of that region, we will need to schedule our code to run in that region:
99+
100+
```java
101+
// If we aren't in charge of that location we need to schedule it
102+
Location location = new Location(Bukkit.getWorld("world"), 0, 0, 0);
103+
Bukkit.getRegionScheduler().run(plugin, location, t -> {
104+
// Now we are in charge of that location! Let's modify the block!
105+
location.getBlock().setType(Material.AIR);
106+
});
107+
```
108+
109+
We can also do this for entities:
110+
111+
```java
112+
Entity entity = Bukkit.getWorld("world").getEntities().get(0);
113+
if (Bukkit.isOwnedByCurrentRegion(entity)) {
114+
// We are in charge of this entity! Let's remove it
115+
entity.remove();
116+
} else {
117+
// We aren't in charge of that entity, let's schedule it
118+
entity.getScheduler().run(plugin, t -> {
119+
// Now we are in charge of that entity! Let's remove it!
120+
entity.remove();
121+
}, null);
122+
}
123+
```
124+
125+
### Tips
126+
127+
During most events and commands, you will be on the thread of the player/entity/block that triggered the event or ran the command.
128+
129+
## Java gotchas for multithreaded plugins
130+
131+
### Race conditions
132+
133+
Consider the code below.
134+
135+
```java
136+
Player player;
137+
138+
void nextPlayer() {
139+
Player nextPlayer = this.getTheNextPlayer(this.player);
140+
nextPlayer.sendMessage("You are now the player!");
141+
this.player = nextPlayer;
142+
}
143+
```
144+
145+
If you ran the method `nextPlayer` twice in a single-threaded plugin, you would expect the following to occur:
146+
1. `player` is `PlayerA`
147+
2. Call 1 executes `this.getTheNextPlayer()` and gets the next player `PlayerB`
148+
3. Call 1 sends `PlayerB` the message `"You are now the player!"`
149+
4. Call 1 saves `player` to be `PlayerB`
150+
5. Call 2 now executes `this.getTheNextPlayer()` and gets the next player `PlayerC`
151+
6. Call 2 sends `PlayerC` the message `"You are now the player!"`
152+
7. Call 2 saves `player` to be `PlayerC`
153+
154+
This makes sense. However, if the method `nextPlayer` is running on two threads at the same time, the following will occur:
155+
156+
1. `player` is `PlayerA`
157+
2. Thread 1 executes `this.getTheNextPlayer()` and gets the next player `PlayerB`
158+
3. Thread 2 also executes `this.getTheNextPlayer()`. Since `player` is still `PlayerA`, the next player is still `PlayerB`
159+
4. Thread 1 sends `PlayerB` the message `"You are now the player!"`
160+
5. Thread 2 also sends `PlayerB` the message `"You are now the player!"`
161+
6. Thread 1 saves `player` to be `PlayerB`
162+
7. Thread 2 also saves `player` to be `PlayerB`
163+
164+
This shows the issue of race conditions and why you need to try to avoid them
165+
166+
### Data types
167+
168+
Common data types are typically **not** multithread safe. For example:
169+
- `ArrayList`
170+
- `LinkedList`
171+
- `HashMap`
172+
- `HashSet`
173+
174+
Consider these following thread-safe data types instead:
175+
- `Collections.synchronizedList(new ArrayList())`
176+
- `CopyOnWriteArrayList`
177+
- `LinkedBlockingDeque`
178+
- `ConcurrentHashMap`
179+
- `ConcurrentHashMap.newKeySet()`
180+
181+
### Locks
182+
183+
Locks

0 commit comments

Comments
 (0)