Tutorial by: kuraiskrap

To clarify, I'm the idiot, but no big brains have made a Async guide yet, so i figured id do what i can.

What is Async?
Async is a way to run code off the main server thread, it runs separately, does not run in order with the rest of your code, and is not in sync with server ticks
The goal of Async is to take intensive tasks (usually alot of math) moving that load off of the main thread

Basicly this means if you have access to multiple CPU cores/threads, you can run certain code on other cores to improve performance.
sadly Async is limited in what it can and cant do, and I'm unaware of a proper list, for example
loop all players in radius 5 around player: will cause big scary red errors in your console (watch for errors when running Async code)
loop all blocks in radius 80 around player: will be totally fine when run async
If you want a full list of what can and cant be done Async, theres no list im aware of, try things and find the limitations
for example i learned spawning entities cant be done, teleporting entities cant be done, but rotating display entities is fine

I have included some code you can run, it will broadcast feedback so you can follow the logic
the code contains two ways to run Async code, one from Skbee with "run task later" and one from Skript Reflect with "sections"
run task later is good for smaller bits of code
sections are basicly functions you can run async, and you dont need to specify input types

Requirements for these skripts
1: Skbee (for the run task later async)
2: Skript Reflect (for the sections and importing code to async wait, check time, and check current thread)
3: the below imports for skript reflect

These skripts will use skript reflect to import extra java code, basicly letting you expand skripts functionality beyond premade addons.
This is great if you understand java but prefer writing in skript, If you dont understand java I feel your pain.

the imported code used in these examples are the following
Thread.currentThread() which is used to check the current thread, to see if it is Async or not (Skbee or reflect threads are Async, "server thread" is sync)
Thread.sleep() basicly Async wait, however this does not account for time spent running code, and will drift out of sync with the servers ticks over time if not dynamic
System.currentTimeMillis() this checks the current system time of your server, it can be used to see how long code was running, letting us adjust how long the thread sleeps to help stay more in sync with server ticks

the code below should be able to be copy pasted into a skript file and run
Its comments and broadcasts should help you understand what the code is doing, Ctrl+F is your friend
pictures of Example outputs shown below code

This focuses on Async using Skbee and switching back and forth


#this command switches between Async and Sync code and displays its current thread as it changes
#more complicated version with Sections is shown later

command /getthread:
#broadcasts starting thread which will be in sync with the main thread
        broadcast "&e starting Thread: &b %Thread.currentThread()%"
#Switch to Async code
        async run 0 ticks later:
#broadcasts the async thread
            broadcast "&e after async run: &b %Thread.currentThread()%"
#Async Wait without returning to main thread, 50 is one tick
#confirms that thread has not changed as it would with "wait 1 tick"
            broadcast "&e after sleep: &b %Thread.currentThread()%"
#uses skripts wait 1 tick which will cause it to resync
            wait 1 tick
            broadcast "&e async broken by wait: &b %Thread.currentThread()%"
        broadcast "&e before run 0 ricks later: &b %Thread.currentThread()%"
        async run 0 ticks later:
            broadcast "&e after 2nd async run: &b %Thread.currentThread()%"
#returns back to main thread with a non async run 0 ticks later
            run 0 ticks later:
                broadcast "&e async resync from nested run: &b %Thread.currentThread()%"

This section compares TPS impact from Sync, Async, and mixed code with a heavy load

Sync version

command /timethreadsync:
        wait 1 tick
        set {_starttime} to System.currentTimeMillis()
        set {_tps} to "%tps%"
        set {_count} to 0
        loop all blocks in radius 100 around player:
            add 1 to {_count}
        set {_endtime} to System.currentTimeMillis()
        set {_timeDifference} to difference between {_endtime} and {_starttime}
        broadcast "&e process time: %{_timeDifference}% &b looped blocks: %{_count}%"
        wait 1 second
        broadcast "&e looping blocks in sync made your TPS go from &b %{_tps}% &f to &d %tps%"

Async Version

this also includes dynamic async waits, the default loop size wont use it, but lowering the radius will

command /timethreadasync:
#This will run the section below in Async, and passes on the variable player
        run section {-Asynctime} async with player
#this shows that the code continues to run without waiting for the above section to finish, because it does not say "and wait" at the end
        send "&e if you dont include &d %"and wait"% &e on a async section it will keep running the below code"
        send "&e if you want to &b Return &e a variable like you would in a function, you must use &d and wait"

#On load means this will be run when skript loads it
on load:
#this creates the section with the variable {_p} which is the player from the above code and stores it in ram as {-Asynctime} where it can be accessed from anywhere
    create section with {_p} stored in {-Asynctime}:
#This sets the variable for how many milliseconds to pause the thread for
        set {_sleep} to 50
#this tells the thread to pause with the variable
#This saves the current time as a variable, basicly starting a timer, 
        set {_starttime} to System.currentTimeMillis()
#Put a load on the server
        set {_tps} to "%tps%"
        set {_count} to 0
        loop all blocks in radius 100 around {_p}:
            add 1 to {_count}
#This ends the timer by saving the current time to a variable
        set {_endtime} to System.currentTimeMillis()
#This turns the time difference between the start and end times and saves the difference as a variable
        set {_timeDifference} to difference between {_endtime} and {_starttime}
        broadcast "&e process time: %{_timeDifference}% &b new delay time: %{_sleep}% &d looped blocks: %{_count}%"
#This removes the time spent processing the code from the pause time, so if it spends 10ms doing a loop, it will only wait 40ms
        if {_timeDifference} is less than 51:
            set {_sleep} to 50 - {_timeDifference}
            broadcast "&f Since it took &e %{_timeDifference}%ms &f to run the loop, the sleep time is being dropped from 50ms to &b%{_sleep}%ms"
#This sets the pause to 0, so it will skip pausing if the code is taking longer then a tick to run
            set {_sleep} to 0       
#wait for TPS to update by waiting 20 ticks Async as each 50ms is 1 tick
#shows TPS impact change before and after loop which should be nothing if you have more then 1 thread
        broadcast "&e looping blocks Async made your TPS go from &b %{_tps}% &f to &d %tps%"

#IF TPS shows over 20, it is most likely catching up for times it was under 20 to average it back out as 20
#If your TPS is over 20 for more then 6 hours, call a Sysadmin
#As long as your TPS is 20 a tick should be 50ms and you should be able to keep roughly in sync with it
#If you want to learn how to stay aproximately in sync with ticks on a server under 20TPS please locate a big brains guide to Async

Mixed version, with just the loop being Async

command /timethreadmixed:
        set {_starttime} to System.currentTimeMillis()
        set {_tps} to "%tps%"
        run section {-asyncloop} async with player and store result in {_count} and wait
        set {_endtime} to System.currentTimeMillis()
        set {_timeDifference} to difference between {_endtime} and {_starttime}
        broadcast "&e process time: %{_timeDifference}% &b looped blocks: %{_count}%"
        wait 1 second
        broadcast "&e just looping blocks async made your TPS go from &b %{_tps}% &f to &d %tps%"

#short section that only makes the block looping Async, as thats where the performance issue is.
on load:
    create section with {_p} stored in {-asyncloop}:
        loop all blocks in radius 100 around {_p}:
            add 1 to {_count}
        return {_count}

This part covers running/nesting sections inside sections

useful if you need to run sync code in the middle of your Async code.

#command starts a Async section
command /nestedsections:
        broadcast "&einitialcommand: &b%Thread.currentThread()%"
        run section {-newsection} async

#first section is run async, and opens another section without specifying to run it Async
on load:
    create section stored in {-newsection}:
        broadcast "&eNewSection: &b%Thread.currentThread()%"
        run section {-nestedsection}

#second section ran by a async section will automatically be async
on load:
    create section stored in {-nestedsection}:
        broadcast "&eNestedSection: &b%Thread.currentThread()%"

#this will show what i think is a bug within nested sections

#thead does not change because its just async without waiting
        loop 20 times:
            run section {-donothing} async
        broadcast "&eAfterNestedAsync: &b%Thread.currentThread()%"

#thread does not change because its not async, and is only waiting
        loop 20 times:
            run section {-donothing} and wait
        broadcast "&eAfterSync&wait: &b%Thread.currentThread()%"

#thread changes and theres a weird delay, due to running a section specifying its async and ending it with "and wait" while inside a Async section
        loop 20 times:
            run section {-donothing} async and wait
        broadcast "&eAfterAsync&wait: &b%Thread.currentThread()%"

#breaking out of Async
#this runs a section, which will be async automatically as its run from a async section but will use "wait 1 tick" to resync, you can also use "run 0 ticks later:"
        run section {-resync}
#this shows this method did not break the rest of the code from being Async
        broadcast "&eAfter running resync section: &b%Thread.currentThread()%"
#this runs sync code 0 ticks later, letting you run some sync code when needed
        run 0 ticks later:
            broadcast "&erun 0 tick later: &b%Thread.currentThread()%"
#rest of the code continues running Async
        broadcast "&eAfter run 0 tick later: &b%Thread.currentThread()%"

#this is the section run from the Async section above, it will resync allowing it to run sync requiring code
on load:
    create section stored in {-resync}:
        wait 1 tick
#shows it has returned to the main thread
        broadcast "&eAfter wait 1 tick in resync section: &b%Thread.currentThread()%"

#this only exists to do nothing so it could be looped to show the nested async and wait bug
on load:
    create section stored in {-donothing}:


command results







final notes: Async seems to lack documentation, to help document it please leave comments with more information
1: I would like comments with example sections for common tasks, could be as simple as merging 2 lists
2: I would like comments that bring up the particular tasks that can not be run Async, or tasks that can be Async that you wouldn't think could be
3: I would love any examples of edgecases or general weirdness like running a nested async and wait section causing a weird pause and thread change.

Did you find kuraiskrap's tutorial helpful?

You must be logged in to comment

  • June 7, 2024, 9:10 p.m. - DjDisaster  

    Async can be very good for particles as it can be costy to calculate and does not cause any errors with bukkit.
    The tasks that cannot be ran Async depend on what they are, most tasks that modify the server's data directly such as spawning an entity are not able to be ran async.
    Running any delay will cause the code to continue running from the main thread after that delay.

  • June 8, 2024, 11:15 a.m. - cowile  

    this was very helpful