Difference between revisions of "Creating Multithreaded Skyrim Mods"

From the CreationKit Wiki
Jump to navigation Jump to search
imported>Chesko
imported>Threedee
m (→‎Callback Pros: careless typo)
 
(51 intermediate revisions by 3 users not shown)
Line 1: Line 1:
'''''Under Construction - Incomplete'''''
[[Category:Tutorials]]
[[Category:Community Tutorials]]
[[Category:Papyrus Tutorials]]
[[category:Scripting]]
{{Tutorial Index
|series=Multithreading
|chapter=1
|Prev=:Category:Tutorials
|Next=Creating_Multithreaded_Skyrim_Mods_Part_2_-_Futures
}}


This tutorial is intended to show how modders can use Papyrus more effectively by leveraging its inherent multithreading capability. This guide includes plenty of examples and explanations to help you understand the design pattern. Using multithreading can greatly increase the performance of complex or repetitive calculations, reduce overall strain on the Papyrus VM (making you a better mod-citizen), and help execute time-sensitive tasks.
This tutorial covers how modders can use Papyrus more effectively by leveraging its inherent multithreading capability. This guide includes plenty of examples and explanations to help you understand the design pattern. Using multithreading can greatly increase the performance of mods that have many external function calls back-to-back (''function A calls external function B, then calls external function C, then calls...'') or deeply nested (''function A calls external function B calls external function C calls...'') and help execute time-critial tasks, at the cost of a short burst of resource utilization.


[http://www.creationkit.com/Threading_Notes_%28Papyrus%29 Papyrus is a threaded scripting language]. However, it can be a challenge harness this attribute of the language.
[http://www.creationkit.com/Threading_Notes_%28Papyrus%29 Papyrus is a threaded scripting language]. However, it can be a challenge to harness this attribute of the language.


The intended audience for this guide is intermediate to expert Papyrus developers. This design pattern '''requires SKSE''' for its use of Mod Events.


The examples provided are intended to be used as a reference to adapt to your own needs; as each mod's needs are different, and because of the way Papyrus (and Skyrim) is designed, writing a generic framework that provides a solution for everyone is not possible; change it to fit your unique requirements. Please take your time going through this guide; there is a lot of information, but once you've grasped the idea, you'll be up and running in no time. There are a lot of codependencies, so some of what you'll be doing may not make sense until the end.
{{WarningBox|The intended audience for this guide is intermediate to expert Papyrus developers. This design pattern '''requires SKSE''' for its use of [http://www.creationkit.com/ModEvent_Script Mod Events].}}
 
 
The examples provided are intended to be used as a reference to adapt to your own needs; as each mod's needs are different, and because of the way Papyrus (and Skyrim) is designed, writing a generic framework that provides a solution for everyone is not possible; change it to fit your unique requirements.  
 
Please take your time going through this guide; there is a lot of information, but once you've grasped the idea, you'll be up and running in no time. There are a lot of codependencies, so some of what you'll be doing may not make sense until the end.




Line 15: Line 28:
The first question to answer is whether or not a multithreading solution is a good fit for your mod. There's no sense in refactoring hundreds of lines of code if you're not going to stand to benefit from it.
The first question to answer is whether or not a multithreading solution is a good fit for your mod. There's no sense in refactoring hundreds of lines of code if you're not going to stand to benefit from it.


'''Does your mod:'''
'''Does your mod / script:'''


* Have many objects that must be placed quickly using things like <code>MoveTo()</code> or <code>PlaceAtMe()</code>?
* Have many external function calls to complete a single task?
* Have many objects that must be placed quickly using things like <code>MoveTo()</code>?
* Extensively or repeatedly use [http://www.creationkit.com/Category:Latent_Functions latent functions]?
* Extensively or repeatedly use [http://www.creationkit.com/Category:Latent_Functions latent functions]?
* Have time-critical tasks that rely on the results of other (potentially slow) functions?
* Have time-critical tasks that rely on the results of other (potentially slow) functions?
* Have need of doing the same thing to a large group of objects?
* Have need of doing the same thing to a large group of objects?


If you answered "yes" to any of these bullets, a multithreaded design pattern may increase the performance of your mod. Multithreading excels at taking tasks that would otherwise be run in sequence and making them run simultaneously. I have personally seen performance over 10 times faster (an action that once took ~8.5 seconds to now takes ~0.5 seconds) in my own mods using this method.
If you answered "yes" to any of these bullets, a multithreaded design pattern may increase the performance of your mod. This pattern provides two distinct advantages:
 
# Multithreading takes tasks that would otherwise be run in sequence and allows them to run simultaneously, which can reduce the time it takes to complete all tasks.
# Due to the way the Papyrus scheduler must sync external calls to frames, many external calls can add a great deal of overhead (see [http://www.creationkit.com/Threading_Notes_%28Papyrus%29 this page] on notes regarding how external calls suspend and resume threads); this pattern can greatly reduce the number of external function calls your script must use at any one time.


== Key Terms ==
Only profiling your scripts by using one of the various profiling functions can tell you whether or not these patterns will improve your mod's behavior. I have personally seen performance over 10 times faster (an action that once took ~8.5 seconds to now takes ~0.5 seconds) in my own mods using this method.


* '''Thread''' - An individual script instance that does work. Returns results to a <code>Future</code>.
Note that by spinning up many threads simultaneously, you are invariably placing increased load on the Papyrus VM for as long as it takes your threads to complete. Ideally, this should be a much shorter frame of time than if the task were done in a single thread. You must decide whether or not the narrow "spike" of resource consumption using threads is better than the more spread-out "swell" of a single thread calling many functions back-to-back. Again, '''profile''' before and after!
* '''Thread Manager''' - A script that controls which thread handles a task. Returns a <code>Future</code> to the user of the script.
 
* '''Future''' - An object that will contain the result of a thread's task, at some point in the future.
 
* '''Future Anchor''' - An object reference in an unused cell that we use to create Futures with using <code>PlaceAtMe()</code>.
{{WarningBox|This design pattern is '''not''' intended to replace the way you do things in every script. It is meant to tackle increasing the performance of ''specific, slow, and usually repetitive'' tasks.}}
 
{{WarningBox|Keep in mind that asynchronous operations means that you don't know how fast, or in what order, your threads will run or finish.
 
If the tasks you need to perform are order-dependent, the methods detailed below are probably not a good fit for your needs.}}




== A Private Army ==
== A Private Army ==


Our example problem is that we are developing a Conjuration mod, and we need to spawn 20 guards very quickly when the player casts a spell; ideally, they should all appear at close to the same time. We also need to keep track of the guards we create, so we can destroy them after the spell ends. This guide will not cover creating a spell, instead we will skip to a point after we've created our Spell and our MagicEffect that we want to add a script to.
In this example, we are developing a Conjuration mod. We need to spawn 20 guards very quickly when the player casts a spell; ideally, they should all appear at close to the same time. We also need to keep track of the guards we create, so we can destroy them after the spell ends. This guide will not cover creating a spell, instead we will skip to a point after we've created our Spell and our MagicEffect that we want to add a script to.


We come up with the following script to drop onto our MagicEffect in the Creation Kit when our spell is cast:
We come up with the following script to drop onto our MagicEffect in the Creation Kit when our spell is cast:


<source lang="papyrus">
<source lang="papyrus">
Line 55: Line 77:
MoveGuardMarkerNearPlayer(2)
MoveGuardMarkerNearPlayer(2)
Guard2 = GuardMarker.PlaceAtMe(Guard)
Guard2 = GuardMarker.PlaceAtMe(Guard)
...
;...and so on
MoveGuardMarkerNearPlayer(20)
MoveGuardMarkerNearPlayer(20)
Guard20 = GuardMarker.PlaceAtMe(Guard)
Guard20 = GuardMarker.PlaceAtMe(Guard)
Line 65: Line 87:
Guard1.Disable()
Guard1.Disable()
Guard1.Delete()
Guard1.Delete()
...
;...and so on
Guard20.Disable()
Guard20.Disable()
Guard20.Delete()
Guard20.Delete()
Line 72: Line 94:
</source>
</source>


We test this in-game, and we see each guard appear one-by-one. Kinda lame. You decide to upload it anyway, and your users complain that the spell is "slow" and "clunky".
<code>PlaceAtMe()</code> can be slow, especially over this many objects. We also have (for illustration purposes) some preprocessing that needs to happen (<code>MoveGuardMarkerNearPlayer(int Index)</code>) before we know where to put the guard. We have decided that multithreading this task would be much faster than placing each Actor one-by-one.
== Creation Kit ==
'''Create Quest:''' Begin by opening the Creation Kit and creating a new Quest. We'll call our quest '''GuardPlacementQuest'''. Click OK to save and close the quest, then open it again (to prevent the CK from crashing). Make sure that "Start Game Enabled", "Run Once", "Warn on Alias Failure" and "Allow repeated stages" are unchecked. Click OK to close it again.


'''Create Future (Activator):''' Next, we want to create an object we will need later, called a <code>Future</code>. We'll get into what these do later. Open the Activator tree in the Creation Kit Object Window, and find ''''xMarkerActivator''''. Right click and Duplicate this object. Double-click the duplicate and rename it's Editor ID to identify it later; we'll call ours '''GuardPlacementFutureActivator'''. Click 'No' when asked if you want to create a new form. Click 'Yes' to confirm.
We test this in-game, and we see each guard appear one-by-one. Your users complain that the spell is "slow" and "clunky". You'd like things to appear much faster so that the spell feels responsive.


'''Create Anchor (Object Reference):''' We now want to create a "Future Anchor". This is an XMarker object reference that we will be placing in a far-off, unused cell. You can create your own blank cell, but '''AAADeleteWhenDoneTestJeremy''' is also a good candidate. Wherever you decide to place it, drag an XMarker Static from the Object Window of the Creation Kit and name the reference. We'll name ours '''GuardPlacementFutureAnchor'''. We'll use this to <code>PlaceAtMe()</code> Futures on this object later on.
We have (for illustration purposes) some preprocessing that needs to happen (<code>MoveGuardMarkerNearPlayer(int Index)</code>) before we know where to put the guard, which turns out to be slow (several MoveTo(), etc). We have decided that multithreading this task would be much faster than placing each Actor one-by-one.




== Threads ==
== Two Approaches: Futures and Callbacks ==


The thread is what will perform the work we want to perform in parallel. Just like <code>PlaceAtMe()</code>, we expect the result of our Thread to be an ObjectReference.
There are two basic threaded patterns that you can decide to implement. They each have pros and cons. You will need to decide which approach is best for your application.
 
First, let's define a base Thread "class", called '''GuardPlacementThread'''.


=== Futures ===


As Papyrus developers, we are accustom to calling functions, having those functions return values, and storing those returned values. You're probably used to seeing code like this:


<source lang="papyrus">
<source lang="papyrus">
scriptname GuardPlacementThread extends Quest
    ObjectReference my_sword = PlayerRef.PlaceAtMe(Sword)
 
ObjectReference future
int thread_id = -1
bool thread_queued = false
 
ActorBase theGuard
Static theMarker
 
ObjectReference function get_async(Activator akFuture, ObjectReference akFutureAnchor, ActorBase akGuard, Static akXMarker)
thread_queued = true
if thread_id == -1
thread_id = GetThreadId()
endif
theGuard = akGuard
theMarker = akXMarker
 
;Raise the event that will start this thread
RaiseEvent_OnGuardPlacement(thread_id)
 
;Create the Future that will contain our result
future = akFutureAnchor.PlaceAtMe(akFuture)
return future
endFunction
 
bool function busy()
return thread_queued
endFunction
 
Event OnGuardPlacement(int aiThreadId)
if thread_queued && aiThreadId == thread_id
;OK, let's get some work done!
ObjectReference tempMarker = Game.GetPlayer().PlaceAtMe(theMarker) ;We could have passed PlayerRef in as a get_async() parameter, too
MoveGuardMarkerNearPlayer(tempMarker)
ObjectReference result = tempMarker.PlaceAtMe(theGuard)
(future as GuardPlacementFuture).result = result
clear_thread_vars()
thread_queued = false
endif
endEvent
 
function clear_thread_vars()
;Reset all thread variables to default state
theGuard = None
theMarker = None
endFunction
 
function MoveGuardMarkerNearPlayer(ObjectReference akMarker)
;Expensive SetPosition, GetPosition, FindNearestRef, etc calls here (illustration only)
endFunction
</source>
</source>




In the Futures pattern, we avoid calling functions like this directly. Instead, we call a function on a special script we will write, the '''Thread Manager''', that will delegate our work to a '''thread'''.


As you can see, our thread does a few important things:
Instead of receiving a return value, we will receive something called a '''Future'''. A <code>Future</code> is not the return value; instead, it ''represents'' the return value ''at some point in the future.'' It can be thought of as a placeholder for the "real" value.
* It has a <code>get_async()</code> function, which takes in all of the parameters necessary to do the work we need to perform.


* It grabs a unique <code>thread_id</code> if it doesn't already have one, which is used to act only on an Event raised by this thread.


* It defines and registers for an <code>OnGuardPlacement</code> Event, which is a custom [http://www.creationkit.com/ModEvent_Script Mod Event] that does the work.
{{NewFeature|
* '''Thread''' - An individual script instance that does work. Returns results to a Future or raises a callback event.


* <code>get_async()</code> returns a <code>Future</code> back to the Thread Manager (who will in turn give the <code>Future</code> back to our script).
* '''Thread Manager''' - A script that controls which thread handles a task. Returns a Future to the user of the script, if using that pattern.


* It does some work in the Event, but only if the thread has been 'queued' and the ThreadId of the Event matches ours.
* '''Future''' - An object that will contain the thread's return value at some point in the future.}}


* We return our results back to the <code>Future</code> we created.
* We clear all of our member variables using <code>clear_thread_vars()</code>.
* We set <code>thread_queued</code> back to <code>False</code>, which tells the Thread Manager that this thread is available to be used again.
Raising an Event against itself allows the <code>Event OnGuardPlacement()</code> to begin working as soon as <code>get_async()</code> is finished. If we called our function that does our work directly from <code>get_async()</code>, the calling script would block until the work was complete, which would defeat the purpose. We use a Thread ID to filter out events from other threads attached to the same Quest that we don't care about.
Now that we've set up our base Thread script, we will create 10 child scripts that will extend this one. They will each contain only one line, the scriptname definition.


So our code using a Future pattern might look something like this:




<source lang="papyrus">
<source lang="papyrus">
;GuardPlacementThread01.psc
    ObjectReference my_sword_future = ThreadManager.PlaceAtMeAsync(Sword)
scriptname GuardPlacementThread01 extends GuardPlacementThread
    ThreadManager.wait_all()
 
;GuardPlacementThread02.psc
scriptname GuardPlacementThread02 extends GuardPlacementThread
 
...
 
;GuardPlacementThread09.psc
scriptname GuardPlacementThread09 extends GuardPlacementThread
 
;GuardPlacementThread10.psc
scriptname GuardPlacementThread10 extends GuardPlacementThread
</source>
</source>




<code>PlaceAtMeAsync</code> is a function we've written that gets assigned to a thread. A thread that has been given data to work on is referred to as being '''queued'''. <code>wait_all()</code> tells the Thread Manager that it should start running any queued threads and that we will wait until they're finished.


Once all of your Thread child scripts and your base Thread script is saved and compiled, attach the 10 child scripts to your Quest. (Do '''NOT''' attach the parent Thread script, only the children.)
Later, when we decide we want the result of our thread, we just ask for it:


"'''''But wait,'''''" you ask. "'''''We need to place 20 guards, but we only have 10 threads. Won't something break?'''''" The Thread Manager, which we'll talk about next, can handle having more work than there are threads!


<source lang="papyrus">
    ObjectReference my_sword = (my_sword_future as FutureScript).get_result()
</source>


== Thread Manager ==


We will next define the Thread Manager script. This script handles delegating our work to an available thread. If a thread is not available, it waits until one is.
Why would we want to do this? In the above example, it might not make much sense. But what if our code looked more like,


Since we may have many thread scripts, and it would be tedious to hook up properties we need to do our task in each and every one, define them here instead and we will pass them in as parameters to our threads. The threads themselves do not have to have Creation Kit-resolved properties, saving you some time.
In the end, the function that we call in our Thread Manager will return a <code>Future</code>, which we can use to get our return value later.


<source lang="papyrus">
<source lang="papyrus">
scriptname GuardPlacementThreadManager extends Quest
    ObjectReference my_sword = PlayerRef.PlaceAtMe(Sword)
 
    my_sword.MoveTo(SwordPositionMarker)
Quest property GuardPlacementQuest auto
    SwordPositionMarker.MoveTo(OriginLocation)
{The name of the thread management quest.}
    my_sword.SetAngle(my_sword.GetAngleX(), my_sword.GetAngleY(), my_sword.GetAngleZ() + 120.0)
 
    ;...and so on
Activator property GuardPlacementFutureActivator auto
{Our Future object.}
 
ObjectReference property GuardPlacementFutureAnchor auto
{Our Future Anchor object reference.}
 
Static property XMarker auto
{Tedious to define properties in the threads and hook up in CK over and over, so define things we need here. MoveGuardMarkerNearPlayer() needs XMarkers.}
 
;Let's cast our threads to local variables so things are less cluttered in our code
GuardPlacementThread01 thread01 = GuardPlacementQuest as GuardPlacementThread01
GuardPlacementThread02 thread02 = GuardPlacementQuest as GuardPlacementThread02
...
GuardPlacementThread09 thread09 = GuardPlacementQuest as GuardPlacementThread09
GuardPlacementThread09 thread10 = GuardPlacementQuest as GuardPlacementThread10
 
 
ObjectReference function PlaceConjuredGuardAsync(ActorBase akGuard)
ObjectReference future
while !future
if !thread01.busy()
future = thread01.get_async(GuardPlacementFutureActivator, GuardPlacementFutureAnchor, akGuard, XMarker)
elseif !thread02.busy()
future = thread02.get_async(GuardPlacementFutureActivator, GuardPlacementFutureAnchor, akGuard, XMarker)
...
elseif !thread09.busy()
future = thread09.get_async(GuardPlacementFutureActivator, GuardPlacementFutureAnchor, akGuard, XMarker)
elseif !thread10.busy()
future = thread10.get_async(GuardPlacementFutureActivator, GuardPlacementFutureAnchor, akGuard, XMarker)
else
;All threads are busy; wait and try again.
Utility.wait(0.1)
endif
endWhile
 
return future
endFunction
</source>
</source>


Here, we call the get_async() function we defined in our threads earlier. The PlaceConjuredGuardAsync() function handles making sure that our work gets delegated to an available thread. The function then returns a <code>Future</code> once an available thread is found.


If we had to do this collection of operations on not just one sword, but say, two dozen, things start to take a while to process; it might be a few seconds before anything ever even happens to sword #12, 18, or 22. And everything happens one by one. With the Future pattern, calling <code>PlaceAtMeAsync()</code> would return almost immediately, leaving your script free to do other things while all of your swords are placed and moved. And with threads, all of these swords would be placed and moved nearly simultaneously. When you're ready to get each sword's ObjectReference, you call <code>get_result()</code> on your Future.


== Back to the Future ==
==== Futures Pros ====
* '''Pull-based:''' Using Futures is a ''pull'' pattern, where you must explicitly ask (pull) for the results of a thread you have started by calling <code>get_result()</code>.
* '''Control who can access results:''' The result can only be retrieved by someone who knows the Future of your thread. You have control over who can retrieve your results.
* '''Control when results are retrieved:''' The result is only retrieved when you ask for it. This is important when you need to retrieve results in a particular order.
* '''Easier to trace execution order:''' A thread can be started and results retrieved all within the same function; your code does not have to "jump around" as much as it does in the Callback pattern.
* '''Abstracts away "locks":''' With Futures, we don't have to worry about two threads accidentally manipulating the same variable in the wrong order because one finished faster or slower than the other. We just request our results from our futures in the order that we want them.
* '''Requires less state management:''' Managing the state of your script is almost as simple as when you wrote scripts in a single thread calling functions directly.


A '''Future''', in parallel processing, is [https://cloud.google.com/appengine/docs/python/ndb/futureclass the representation of an asynchronous operation]. Like the Google App Engine version that this was inspired by, when the Future is created, it will probably not have any results yet. Your script can store a <code>Future</code> and later call the <code>Future</code> object's <code>get_result()</code> function.  
==== Futures Cons ====
* '''Harder to understand:''' Implementing a Future-based approach requires learning several new concepts.
* '''Harder to implement:''' There are more pieces involved in setting up a Future-based approach.
* '''More overhead (slower):''' A Future-based approach can be slower than using a Callback approach, due to the fact that Futures are ObjectReferences and must be created and destroyed when a thread runs and data is read.
* '''Results availability and delays:''' If you call <code>get_result()</code> on a Future, and the result is not yet ready, the script will wait until it is, and then return the result. You are at the mercy of the thread to return a value to the future until you can continue. For some applications, this may be considered a pro.
* '''Harder to make results public:''' If you have many intended consumers of your thread's results (many scripts, or even scripts on other people's mods), using Futures may be burdensome for getting your results to everyone who needs them.
* '''May require polling:''' If you can't afford to block execution on <code>get_result()</code>, you may have to poll the Future's <code>done()</code> function to check whether the thread has finished.


A few notes about Futures:
* Futures are lightweight Activator ObjectReferences that have an attached script that '''contain the result''' of a thread.
* A <code>Future</code> (as written) can have its results retrieved '''once''', after which, the <code>Future</code> is destroyed. Make sure to save your results to your own variables if you will need them later, since the <code>Future</code> will no longer exist. This keeps the number of ObjectReferences created under control (a contributor to save game size bloat).
* get_result() is blocking; it waits until results are received from the thread, and then returns.
* You can check if a <code>Future</code> has received a result without blocking the caller by calling the Future's done() function.
* The result of a <code>Future</code> is the same as the result of any other function in Papyrus, and can return None, false, etc if an error is encountered. Code the result of a <code>Future</code> like you would the result of anything else and anticipate errors accordingly.


Let's create our Future:
=== Callbacks ===


<source lang="papyrus">
Callbacks are similar to Futures in that we start '''threads''' using a '''Thread Manager''' to do work for us, instead of calling functions directly and sequentially. The difference is that when we start the thread, there is no return value; instead, the thread will ''call back'' to tell us when it is finished, and what the result was. It does this by raising a Mod Event.
scriptname GuardPlacementFuture extends ObjectReference


ObjectReference r
ObjectReference property result hidden
function set(ObjectReference akResult)
done = true
r = akResult
endFunction
endProperty


bool done = false
{{NewFeature|
bool function done()
* '''Callback''' - A Mod Event that is raised when a thread completes. Passes the thread results in the event parameters.}}
return done
endFunction
 
ObjectReference function get_result()
;Terminate the request after 10 seconds, or as soon as we have a result
int i = 0
while !done && i < 100
i += 1
utility.wait(0.1)
endWhile
RegisterForSingleUpdate(0.1)
return r
endFunction
 
Event OnUpdate()
self.Disable()
self.Delete()
endEvent
</source>


We should only interface with the <code>Future</code> using its member functions, <code>done()</code> and <code>get_result()</code>.


== Tying it All Together ==
Using the above example from Futures, our code might look like:


Now that we've created our Threads, our Thread Manager, and our Future script, we can start to put them to work. In our original ActiveMagicEffect script, we did all of our MoveGuardMarkerNearPlayer() and PlaceAtMe() calls in a row, getting a series of Actor references for our guards in return. We're going to modify that slightly to use our shiny new threaded placement system:


<source lang="papyrus">
<source lang="papyrus">
scriptname SummonArmy extends ActiveMagicEffect
    Event OnInit()
        RegisterForModEvent("MyMod_PlaceSwordAsyncCallback")
    endEvent


Quest property GuardPlacementQuest auto
    function SomeFunction()
{We need a reference to our quest with the threads and Thread Manager defined.}
        ThreadManager.PlaceSwordAsync(Sword)
ActorBase property Guard auto
        ThreadManager.wait_all()
ObjectReference property GuardMarker auto
    endFunction
Actor Guard1
Actor Guard2
...
Actor Guard20


Event OnEffectStart(Actor akTarget, Actor akCaster)
    ;...then somewhere else, in your script
if akCaster == Game.GetPlayer()
;Place actors according to the player's position, taking into account walls, obstacles, etc


;Cast the Quest as our Thread Manager and store it
    Event PlaceSwordAsyncCallback(ObjectReference akPlacedObject)
GuardPlacementThreadManager threadmgr = GuardPlacementQuest as GuardPlacementThreadManager
        ;Anyone that registers for the mod event can get this, too!
 
        my_sword = akPlacedObject
;Call PlaceConjuredGuardAsync for each Guard and store the returned Future
    endEvent
ObjectReference Guard1Future = threadmgr.PlaceConjuredGuardAsync(Guard)
ObjectReference Guard2Future = threadmgr.PlaceConjuredGuardAsync(Guard)
ObjectReference Guard3Future = threadmgr.PlaceConjuredGuardAsync(Guard)
;...and so on
ObjectReference Guard19Future = threadmgr.PlaceConjuredGuardAsync(Guard)
ObjectReference Guard20Future = threadmgr.PlaceConjuredGuardAsync(Guard)
 
;Collect the results
Guard1 = (Guard1Future as GuardPlacementFuture).get_result()
Guard2 = (Guard2Future as GuardPlacementFuture).get_result()
Guard3 = (Guard3Future as GuardPlacementFuture).get_result()
;...and so on
Guard19 = (Guard19Future as GuardPlacementFuture).get_result()
Guard20 = (Guard20Future as GuardPlacementFuture).get_result()
endif
endEvent
 
Event OnEffectFinish(Actor akTarget, Actor akCaster)
if akCaster == Game.GetPlayer()
Guard1.Disable()
Guard1.Delete()
;...and so on
Guard20.Disable()
Guard20.Delete()
endif
endEvent
</source>
</source>


Here, instead of doing the work in our script, we delegated the work to the Thread Manager, and stored the Futures that it returned to us. Then, we gathered the results using our Futures' get_async() function. We don't have to worry about our threads or the created futures; those are freed up and cleared for us by the system.
Even though all of the threads are working in parallel and might not finish at the same time, the <code>get_result()</code> function will wait until a result is available before returning. We can be sure that we will get the results even if they are processed out of order. For instance, if thread 2 completed before thread 1, calling the thread 1 Future's <code>get_result()</code> function will pause the script until a result is available. Then the thread 2 Future's result is gathered, and so on.
== Final Notes ==
* Be a good Papyrus and Skyrim citizen and read the results from your Futures as soon as you are able so that they can be disposed of. If Futures begin to pile up without being read and destroyed, save game bloat could occur.
* If you are running operations in an always-on background script that you want to multithread, and you will always have the same number of results back, it may make more sense for you to implement a static set of Future references that are never destroyed that you continue to reuse. This would prevent the churn of Futures being created and destroyed and may lend itself to faster performance. Keep in mind that this would probably result in some data loss if your Futures are not read from regularly as the new results overwrite the old ones.
* If something doesn't seem to be working correctly, turn on debugging and insert debug.trace() messages where you think your code may be failing. Running the game in windowed mode and using a real-time log viewer like SnakeTail is extremely helpful for diagnosing script problems.
* You can create as many threads as you want, but I wouldn't recommend more than 50 or so. It depends on your needs, the strain each thread places on the Papyrus VM, and how quickly you need your results.


* If you need to perform a set of actions that are not all the same, the Thread Manager pattern might not be best for you. You may want to create different thread base scripts purpose-built for several tasks and then call their get_async() functions directly, blocking on busy() until they're available. You can still run many different tasks concurrently this way, even if they're not the same.
==== Callback Pros ====
* '''Push-based:''' Using callbacks is a ''push'' pattern, where results are returned to you as soon as they're available instead of having to request them.
* '''Anyone can access results:''' The results of a thread are available to anyone who registered for the event that returns them.
* '''Results received without delays:''' Unlike Futures, you do not have to block your script pending results being available. Just register for the appropriate event and react to it.
* '''No polling:''' You no longer have to potentially poll for whether or not your results are ready.
* '''Easier to understand:''' The concepts in a Callback pattern are nothing new to anyone who knows how to use Mod Events.
* '''Easier to implement:''' There are comparatively fewer things to deal with when using a Callback pattern.
* '''Less overhead (faster):''' Using a callback pattern can be a bit faster than a Future-based approach.


== Conclusion ==
==== Callback Cons ====
* '''...Anyone can access results:''' You have no control over who is able to consume your results.
* '''No control when results are retrieved:''' You have no control over when a result will be retrieved, or in what order. You must be able to react to the result events that are raised, and you must assume that threads can finish in any order.
* '''More difficult to trace execution order:''' A callback pattern can make the script flow more difficult to follow and debug, since the function where a thread is started and the event that it returns results to will be in two (or more) different places.
* '''Locks required:''' Locks are required if you have two threads that may write to the same variable.
* '''Requires more state management:''' You can receive result callbacks at any time, which may make it necessary for you to re-evaluate the script's current state each time you receive one, depending on your application.


I hope this tutorial helps shed light on how you might implement your own multithreaded design pattern in your mods to tackle repetitive, resource-hungry tasks. Feel free to [chesko.tesmod@gmail.com shoot me an email] if you have a question about the examples presented here, if you see an error, or just want to say hi. Good luck, and happy modding!
More details about each approach are available in the next tutorials, with example code and definitions. Press on!


- Chesko
{{Tutorial_Bottom_Bar
|Prev=:Category:Tutorials
|Next=Creating_Multithreaded_Skyrim_Mods_Part_2_-_Futures
}}

Latest revision as of 23:29, 5 July 2016

Creating Multithreaded Skyrim Mods
Multithreading Series, Chapter 1
Return to Tutorial Hub
LeftArrow.png Previous Tutorial Next TutorialRightArrow.png

This tutorial covers how modders can use Papyrus more effectively by leveraging its inherent multithreading capability. This guide includes plenty of examples and explanations to help you understand the design pattern. Using multithreading can greatly increase the performance of mods that have many external function calls back-to-back (function A calls external function B, then calls external function C, then calls...) or deeply nested (function A calls external function B calls external function C calls...) and help execute time-critial tasks, at the cost of a short burst of resource utilization.

Papyrus is a threaded scripting language. However, it can be a challenge to harness this attribute of the language.


Achtung.png The intended audience for this guide is intermediate to expert Papyrus developers. This design pattern requires SKSE for its use of Mod Events.


The examples provided are intended to be used as a reference to adapt to your own needs; as each mod's needs are different, and because of the way Papyrus (and Skyrim) is designed, writing a generic framework that provides a solution for everyone is not possible; change it to fit your unique requirements.

Please take your time going through this guide; there is a lot of information, but once you've grasped the idea, you'll be up and running in no time. There are a lot of codependencies, so some of what you'll be doing may not make sense until the end.


Should I Multithread?[edit | edit source]

The first question to answer is whether or not a multithreading solution is a good fit for your mod. There's no sense in refactoring hundreds of lines of code if you're not going to stand to benefit from it.

Does your mod / script:

  • Have many external function calls to complete a single task?
  • Have many objects that must be placed quickly using things like MoveTo()?
  • Extensively or repeatedly use latent functions?
  • Have time-critical tasks that rely on the results of other (potentially slow) functions?
  • Have need of doing the same thing to a large group of objects?

If you answered "yes" to any of these bullets, a multithreaded design pattern may increase the performance of your mod. This pattern provides two distinct advantages:

  1. Multithreading takes tasks that would otherwise be run in sequence and allows them to run simultaneously, which can reduce the time it takes to complete all tasks.
  2. Due to the way the Papyrus scheduler must sync external calls to frames, many external calls can add a great deal of overhead (see this page on notes regarding how external calls suspend and resume threads); this pattern can greatly reduce the number of external function calls your script must use at any one time.

Only profiling your scripts by using one of the various profiling functions can tell you whether or not these patterns will improve your mod's behavior. I have personally seen performance over 10 times faster (an action that once took ~8.5 seconds to now takes ~0.5 seconds) in my own mods using this method.

Note that by spinning up many threads simultaneously, you are invariably placing increased load on the Papyrus VM for as long as it takes your threads to complete. Ideally, this should be a much shorter frame of time than if the task were done in a single thread. You must decide whether or not the narrow "spike" of resource consumption using threads is better than the more spread-out "swell" of a single thread calling many functions back-to-back. Again, profile before and after!


Achtung.png This design pattern is not intended to replace the way you do things in every script. It is meant to tackle increasing the performance of specific, slow, and usually repetitive tasks.
Achtung.png Keep in mind that asynchronous operations means that you don't know how fast, or in what order, your threads will run or finish.

If the tasks you need to perform are order-dependent, the methods detailed below are probably not a good fit for your needs.


A Private Army[edit | edit source]

In this example, we are developing a Conjuration mod. We need to spawn 20 guards very quickly when the player casts a spell; ideally, they should all appear at close to the same time. We also need to keep track of the guards we create, so we can destroy them after the spell ends. This guide will not cover creating a spell, instead we will skip to a point after we've created our Spell and our MagicEffect that we want to add a script to.

We come up with the following script to drop onto our MagicEffect in the Creation Kit when our spell is cast:


scriptname SummonArmy extends ActiveMagicEffect

ActorBase property Guard auto
ObjectReference property GuardMarker auto
Actor Guard1
Actor Guard2
...
Actor Guard20

Event OnEffectStart(Actor akTarget, Actor akCaster)
	if akCaster == Game.GetPlayer()
		;Place actors according to the player's position, taking into account walls, obstacles, etc
		MoveGuardMarkerNearPlayer(1) 	;Moves the GuardMarker where the guard is supposed to go; maybe some GetPositions, etc
		Guard1 = GuardMarker.PlaceAtMe(Guard)
		MoveGuardMarkerNearPlayer(2)
		Guard2 = GuardMarker.PlaceAtMe(Guard)
		;...and so on
		MoveGuardMarkerNearPlayer(20)
		Guard20 = GuardMarker.PlaceAtMe(Guard)
	endif
endEvent

Event OnEffectFinish(Actor akTarget, Actor akCaster)
	if akCaster == Game.GetPlayer()
		Guard1.Disable()
		Guard1.Delete()
		;...and so on
		Guard20.Disable()
		Guard20.Delete()
	endif
endEvent


We test this in-game, and we see each guard appear one-by-one. Your users complain that the spell is "slow" and "clunky". You'd like things to appear much faster so that the spell feels responsive.

We have (for illustration purposes) some preprocessing that needs to happen (MoveGuardMarkerNearPlayer(int Index)) before we know where to put the guard, which turns out to be slow (several MoveTo(), etc). We have decided that multithreading this task would be much faster than placing each Actor one-by-one.


Two Approaches: Futures and Callbacks[edit | edit source]

There are two basic threaded patterns that you can decide to implement. They each have pros and cons. You will need to decide which approach is best for your application.

Futures[edit | edit source]

As Papyrus developers, we are accustom to calling functions, having those functions return values, and storing those returned values. You're probably used to seeing code like this:

    ObjectReference my_sword = PlayerRef.PlaceAtMe(Sword)


In the Futures pattern, we avoid calling functions like this directly. Instead, we call a function on a special script we will write, the Thread Manager, that will delegate our work to a thread.

Instead of receiving a return value, we will receive something called a Future. A Future is not the return value; instead, it represents the return value at some point in the future. It can be thought of as a placeholder for the "real" value.


NewFeature.jpg
  • Thread - An individual script instance that does work. Returns results to a Future or raises a callback event.
  • Thread Manager - A script that controls which thread handles a task. Returns a Future to the user of the script, if using that pattern.
  • Future - An object that will contain the thread's return value at some point in the future.


So our code using a Future pattern might look something like this:


    ObjectReference my_sword_future = ThreadManager.PlaceAtMeAsync(Sword)
    ThreadManager.wait_all()


PlaceAtMeAsync is a function we've written that gets assigned to a thread. A thread that has been given data to work on is referred to as being queued. wait_all() tells the Thread Manager that it should start running any queued threads and that we will wait until they're finished.

Later, when we decide we want the result of our thread, we just ask for it:


    ObjectReference my_sword = (my_sword_future as FutureScript).get_result()


Why would we want to do this? In the above example, it might not make much sense. But what if our code looked more like,


    ObjectReference my_sword = PlayerRef.PlaceAtMe(Sword)
    my_sword.MoveTo(SwordPositionMarker)
    SwordPositionMarker.MoveTo(OriginLocation)
    my_sword.SetAngle(my_sword.GetAngleX(), my_sword.GetAngleY(), my_sword.GetAngleZ() + 120.0)
    ;...and so on


If we had to do this collection of operations on not just one sword, but say, two dozen, things start to take a while to process; it might be a few seconds before anything ever even happens to sword #12, 18, or 22. And everything happens one by one. With the Future pattern, calling PlaceAtMeAsync() would return almost immediately, leaving your script free to do other things while all of your swords are placed and moved. And with threads, all of these swords would be placed and moved nearly simultaneously. When you're ready to get each sword's ObjectReference, you call get_result() on your Future.

Futures Pros[edit | edit source]

  • Pull-based: Using Futures is a pull pattern, where you must explicitly ask (pull) for the results of a thread you have started by calling get_result().
  • Control who can access results: The result can only be retrieved by someone who knows the Future of your thread. You have control over who can retrieve your results.
  • Control when results are retrieved: The result is only retrieved when you ask for it. This is important when you need to retrieve results in a particular order.
  • Easier to trace execution order: A thread can be started and results retrieved all within the same function; your code does not have to "jump around" as much as it does in the Callback pattern.
  • Abstracts away "locks": With Futures, we don't have to worry about two threads accidentally manipulating the same variable in the wrong order because one finished faster or slower than the other. We just request our results from our futures in the order that we want them.
  • Requires less state management: Managing the state of your script is almost as simple as when you wrote scripts in a single thread calling functions directly.

Futures Cons[edit | edit source]

  • Harder to understand: Implementing a Future-based approach requires learning several new concepts.
  • Harder to implement: There are more pieces involved in setting up a Future-based approach.
  • More overhead (slower): A Future-based approach can be slower than using a Callback approach, due to the fact that Futures are ObjectReferences and must be created and destroyed when a thread runs and data is read.
  • Results availability and delays: If you call get_result() on a Future, and the result is not yet ready, the script will wait until it is, and then return the result. You are at the mercy of the thread to return a value to the future until you can continue. For some applications, this may be considered a pro.
  • Harder to make results public: If you have many intended consumers of your thread's results (many scripts, or even scripts on other people's mods), using Futures may be burdensome for getting your results to everyone who needs them.
  • May require polling: If you can't afford to block execution on get_result(), you may have to poll the Future's done() function to check whether the thread has finished.


Callbacks[edit | edit source]

Callbacks are similar to Futures in that we start threads using a Thread Manager to do work for us, instead of calling functions directly and sequentially. The difference is that when we start the thread, there is no return value; instead, the thread will call back to tell us when it is finished, and what the result was. It does this by raising a Mod Event.


NewFeature.jpg
  • Callback - A Mod Event that is raised when a thread completes. Passes the thread results in the event parameters.


Using the above example from Futures, our code might look like:


    Event OnInit()
        RegisterForModEvent("MyMod_PlaceSwordAsyncCallback")
    endEvent

    function SomeFunction()
        ThreadManager.PlaceSwordAsync(Sword)
        ThreadManager.wait_all()
    endFunction

    ;...then somewhere else, in your script

    Event PlaceSwordAsyncCallback(ObjectReference akPlacedObject)
        ;Anyone that registers for the mod event can get this, too!
        my_sword = akPlacedObject
    endEvent


Callback Pros[edit | edit source]

  • Push-based: Using callbacks is a push pattern, where results are returned to you as soon as they're available instead of having to request them.
  • Anyone can access results: The results of a thread are available to anyone who registered for the event that returns them.
  • Results received without delays: Unlike Futures, you do not have to block your script pending results being available. Just register for the appropriate event and react to it.
  • No polling: You no longer have to potentially poll for whether or not your results are ready.
  • Easier to understand: The concepts in a Callback pattern are nothing new to anyone who knows how to use Mod Events.
  • Easier to implement: There are comparatively fewer things to deal with when using a Callback pattern.
  • Less overhead (faster): Using a callback pattern can be a bit faster than a Future-based approach.

Callback Cons[edit | edit source]

  • ...Anyone can access results: You have no control over who is able to consume your results.
  • No control when results are retrieved: You have no control over when a result will be retrieved, or in what order. You must be able to react to the result events that are raised, and you must assume that threads can finish in any order.
  • More difficult to trace execution order: A callback pattern can make the script flow more difficult to follow and debug, since the function where a thread is started and the event that it returns results to will be in two (or more) different places.
  • Locks required: Locks are required if you have two threads that may write to the same variable.
  • Requires more state management: You can receive result callbacks at any time, which may make it necessary for you to re-evaluate the script's current state each time you receive one, depending on your application.

More details about each approach are available in the next tutorials, with example code and definitions. Press on!

LeftArrow.png Previous Tutorial Return to Tutorial Hub Next Tutorial RightArrow.png