Difference between revisions of "Creating Multithreaded Skyrim Mods Part 3 - Callbacks"

From the CreationKit Wiki
Jump to navigation Jump to search
imported>Chesko
(Added Futures tutorial as a template; will edit to fit Callbacks.)
imported>Chesko
 
(15 intermediate revisions by the same user not shown)
Line 1: Line 1:
[[Category: Tutorials]]
[[Category: Tutorials]]
[[Category: Community Tutorials]]


{{Tutorial Index
{{Tutorial Index
Line 9: Line 10:


We will be implementing a multithreaded solution to our example problem (a Conjuration mod that spawns many actors) using the '''Callback pattern'''.
We will be implementing a multithreaded solution to our example problem (a Conjuration mod that spawns many actors) using the '''Callback pattern'''.
{{NewFeature| [http://www.creationkit.com/images/b/bd/TutorialExampleMod_Multithreading_Callbacks.zip Download Tutorial Example Plugin] - A fully functional, installable mod. Includes all tutorial files and source code.}}


== Pattern Overview ==
== Pattern Overview ==
To recap the Pros and Cons of this approach:
==== 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:''' Their 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 ====
* '''...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.


Here is a diagram of how the Callback pattern works.
Here is a diagram of how the Callback pattern works.


[[File:Multithreading_fig3_1.png|1128px|center|Fig. 3.1, 3.2]]
[[File:Multithreading_fig3_1.png|1056px|center|Fig. 3.1, 3.2]]


Above, you can see that the sequence is:
Above, you can see that the sequence is:
Line 26: Line 49:
== Creation Kit ==
== 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 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'''.
# '''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 into the Render Window and name the reference. We'll name ours '''GuardPlacementFutureAnchor'''. We'll use this to <code>PlaceAtMe()</code> Futures on this object later on.


<gallery widths="240px" heights="120px" perrow="3">
<gallery widths="240px" heights="200px" perrow="3">
Image:Multithreading-fig1-1.JPG|<b>Fig. 2.4</b>: <br> Create Quest
Image:Multithreading-fig1-1.JPG|<b>Fig. 3.3</b>: <br> Create Quest
Image:Multithreading-fig1-2.JPG|<b>Fig. 2.5</b>: <br> Create Future Activator
Image:Multithreading-fig1-3.JPG|<b>Fig. 2.6</b>: <br> Create Anchor
</gallery>
</gallery>


Line 47: Line 66:
<source lang="papyrus">
<source lang="papyrus">
scriptname GuardPlacementThread extends Quest hidden
scriptname GuardPlacementThread extends Quest hidden
 
;Thread control variables
;Thread variables
ObjectReference future
bool thread_queued = false
bool thread_queued = false
 
;Variables you need to get things done go here  
;Variables you need to get things done go here  
ActorBase theGuard
ActorBase theGuard
Static theMarker
Static theMarker
 
;Thread queuing and set-up
;Thread queuing and set-up
ObjectReference function get_async(Activator akFuture, ObjectReference akFutureAnchor, ActorBase akGuard, Static akXMarker)
ObjectReference function get_async(ActorBase akGuard, Static akXMarker)
        ;Let the Thread Manager know that this thread is busy
    ;Let the Thread Manager know that this thread is busy
        thread_queued = true
    thread_queued = true
        ;Store our passed-in parameters to member variables
    ;Store our passed-in parameters to member variables
theGuard = akGuard
theGuard = akGuard
theMarker = akXMarker
theMarker = akXMarker
;Create the Future that will contain our result
future = akFutureAnchor.PlaceAtMe(akFuture)
return future
endFunction
endFunction
 
;Allows the Thread Manager to determine if this thread is available
;Allows the Thread Manager to determine if this thread is available
bool function queued()
bool function queued()
return thread_queued
return thread_queued
endFunction
endFunction
 
;For Thread Manager troubleshooting.
bool function has_future(ObjectReference akFuture)
    if akFuture == future
        return true
    else
        return false
    endif
endFunction
 
;For Thread Manager troubleshooting.
;For Thread Manager troubleshooting.
bool function force_unlock()
bool function force_unlock()
Line 91: Line 96:
     return true
     return true
endFunction
endFunction
 
;The actual set of code we want to multithread.
;The actual set of code we want to multithread.
Event OnGuardPlacement()
Event OnGuardPlacement()
if thread_queued
if thread_queued
;OK, let's get some work done!
;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
ObjectReference tempMarker = Game.GetPlayer().PlaceAtMe(theMarker)
MoveGuardMarkerNearPlayer(tempMarker)
MoveGuardMarkerNearPlayer(tempMarker)
ObjectReference result = tempMarker.PlaceAtMe(theGuard)
ObjectReference result = tempMarker.PlaceAtMe(theGuard)
                tempMarker.Disable()
tempMarker.Disable()
                tempMarker.Delete()
tempMarker.Delete()
 
                ;OK, we're done - let's pass the result back to the future
        ;OK, we're done - raise event to return results
                ;UNCOMMENT THIS after compiling GuardPlacementFuture
RaiseEvent_GuardPlacementCallback(result)
;(future as GuardPlacementFuture).result = result
 
        ;Set all variables back to default
                ;Set all variables back to default
clear_thread_vars()
clear_thread_vars()
 
                ;Make the thread available to the Thread Manager again
        ;Make the thread available to the Thread Manager again
thread_queued = false
thread_queued = false
endif
endif
endEvent
endEvent
 
;Another function that does things we want to multithread.
;Called from Event OnGuardPlacement
function MoveGuardMarkerNearPlayer(ObjectReference akMarker)
function MoveGuardMarkerNearPlayer(ObjectReference akMarker)
;Expensive SetPosition, GetPosition, FindNearestRef, etc calls here (illustration only)
;Some difficult calculations, etc
endFunction
EndFunction
 
function clear_thread_vars()
function clear_thread_vars()
;Reset all thread variables to default state
;Reset all thread variables to default state
theGuard = None
theGuard = None
theMarker = None
theMarker = None
endFunction
;Create the callback
function RaiseEvent_GuardPlacementCallback(ObjectReference akGuard)
    int handle = ModEvent.Create("MyMod_GuardPlacementCallback")
    if handle
    ModEvent.PushForm(handle, akGuard as Form)
        ModEvent.Send(handle)
    else
        ;pass
    endif
endFunction
endFunction
</source>
</source>
Line 130: Line 145:
As you can see, our thread does a few important things:
As you can see, our thread does a few important things:
* 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 has a <code>get_async()</code> function, which takes in all of the parameters necessary to do the work we need to perform.
* <code>get_async()</code> creates a <code>Future</code> which will eventually make its way back to the script that called our Thread Manager function.


* <code>Event OnGuardPlacement()</code> will fire if the Thread Manager raises the event.
* <code>Event OnGuardPlacement()</code> will fire if the Thread Manager raises the event.


* The thread returns its results back to the Future it created.
* The thread "calls back" to any scripts that have registered for our callback event.


* It clears all of the member variables using <code>clear_thread_vars()</code>.
* It clears all of the member variables using <code>clear_thread_vars()</code>.
Line 180: Line 193:


Declare any properties that your threads will need in this script; the threads themselves will not have properties defined (since this would be tedious to hook up in the Creation Kit for each thread).
Declare any properties that your threads will need in this script; the threads themselves will not have properties defined (since this would be tedious to hook up in the Creation Kit for each thread).
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
scriptname GuardPlacementThreadManager extends Quest
 
Quest property GuardPlacementQuest auto
Quest property GuardPlacementQuest auto
{The name of the thread management quest.}
{The name of the thread management quest.}
 
Activator property GuardPlacementFutureActivator auto
{Our Future object.}
 
ObjectReference property GuardPlacementFutureAnchor auto
{Our Future Anchor object reference.}
 
Static property XMarker auto
Static property XMarker auto
{Something a thread needs; our threads don't declare their own properties.}
{Tedious to define properties in the threads and hook up in CK over and over, so define things we need here. MoveGuardMarkerNearPlayer() needs XMarkers.}


GuardPlacementThread01 thread01
GuardPlacementThread01 thread01
Line 204: Line 209:
GuardPlacementThread09 thread09
GuardPlacementThread09 thread09
GuardPlacementThread10 thread10
GuardPlacementThread10 thread10
 
Event OnInit()
Event OnInit()
     ;Register for the event that will start all threads
     ;Register for the event that will start all threads
Line 217: Line 222:
     thread10 = GuardPlacementQuest as GuardPlacementThread10
     thread10 = GuardPlacementQuest as GuardPlacementThread10
EndEvent
EndEvent
 
;The 'public-facing' function that our MagicEffect script will interact with.
;The 'public-facing' function that our MagicEffect script will interact with.
ObjectReference function PlaceConjuredGuardAsync(ActorBase akGuard)
function PlaceConjuredGuardAsync(ActorBase akGuard)
        int i = 0
    if !thread01.queued()
ObjectReference future
        thread01.get_async(akGuard, XMarker)
while !future
    elseif !thread02.queued()
if !thread01.queued()
thread02.get_async(akGuard, XMarker)
future = thread01.get_async(GuardPlacementFutureActivator, GuardPlacementFutureAnchor, akGuard, XMarker)
    ;...and so on
elseif !thread02.queued()
    elseif !thread09.queued()
future = thread02.get_async(GuardPlacementFutureActivator, GuardPlacementFutureAnchor, akGuard, XMarker)
        thread09.get_async(akGuard, XMarker)
...
    elseif !thread10.queued()
elseif !thread09.queued()
        thread10.get_async(akGuard, XMarker)
future = thread09.get_async(GuardPlacementFutureActivator, GuardPlacementFutureAnchor, akGuard, XMarker)
    else
elseif !thread10.queued()
;All threads are queued; start all threads, wait, and try again.
future = thread10.get_async(GuardPlacementFutureActivator, GuardPlacementFutureAnchor, akGuard, XMarker)
        wait_all()
else
        PlaceConjuredGuardAsync(akGuard)
;All threads are queued; start all threads, wait, and try again.
endif
                        wait_all()
endif
endWhile
 
return future
endFunction
endFunction
 
function wait_all()
function wait_all()
     RaiseEvent_OnGuardPlacement()
     RaiseEvent_OnGuardPlacement()
Line 266: Line 266:
endFunction
endFunction


;A helper function that can avert permanent thread failure if something goes wrong
;Create the ModEvent that will start this thread
function TryToUnlockThread(ObjectReference akFuture)
    bool success = false
    if thread01.has_future(akFuture)
        success = thread01.force_unlock()
    elseif thread02.has_future(akFuture)
        success = thread02.force_unlock()
    ;...and so on
    elseif thread09.has_future(akFuture)
        success = thread09.force_unlock()
    elseif thread10.has_future(akFuture)
        success = thread10.force_unlock()
    endif
   
    if !success
        debug.trace("Error: A thread has encountered an error and has become unresponsive.")
    else
        debug.trace("Warning: An unresponsive thread was successfully unlocked.")
    endif
endFunction
 
;Create the ModEvent that will start all threads
function RaiseEvent_OnGuardPlacement()
function RaiseEvent_OnGuardPlacement()
     int handle = ModEvent.Create("MyMod_OnGuardPlacement")
     int handle = ModEvent.Create("MyMod_OnGuardPlacement")
Line 299: Line 278:




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.
The PlaceConjuredGuardAsync() function handles making sure that our work gets delegated to an available thread.




Line 309: Line 288:
'''Compile and attach''' this script to your GuardPlacementQuest. then, double-click the Thread Manager script and '''fill the properties'''. Once you've done that, your quest's script section should look something like this:
'''Compile and attach''' this script to your GuardPlacementQuest. then, double-click the Thread Manager script and '''fill the properties'''. Once you've done that, your quest's script section should look something like this:


image here
[[File:Multithreading_quest_scripts.JPG|509px|center]]
 
 
== Back to the Future ==
 
'''Futures''' are [https://cloud.google.com/appengine/docs/python/ndb/futureclass a concept from parallel processing]. It can be thought of as a placeholder in lieu of your result until your result has arrived. 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, which should return your results immediately.
 
 
{{InDepth|A few notes about Futures:
* Futures '''contain the result''' of a thread that has finished.
* Futures are lightweight Activator ObjectReferences placed in an unloaded cell.
* A <code>Future</code> is '''temporary''', and exists until the result is read, after which, the <code>Future</code> is destroyed. Make sure to save your results to your own variable if you will need them later, since the <code>Future</code> will no longer exist after calling <code>get_result()</code>. This keeps the number of ObjectReferences created under control, and helps prevent save game size bloat.
* <code>get_result()</code> is technically ''blocking'', meaning it waits until results are received from the thread, and then returns. However, since <code>wait_all()</code> waits for all threads to complete, there should be no reason you should have to wait on your results when calling this function, unless something went wrong.
* 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.
* Futures will attempt to unlock threads that have become unresponsive.}}
 
 
Let's create our Future:
 
 
<source lang="papyrus">
scriptname GuardPlacementFuture extends ObjectReference
 
Quest property GuardPlacementQuest auto
 
ObjectReference r
ObjectReference property result hidden
function set(ObjectReference akResult)
done = true
r = akResult
endFunction
endProperty
 
bool done = false
bool function done()
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)
       
        if i >= 100
                ;Our thread probably encountered an error and is locked up; we need to unlock it.
                (GuardPlacementQuest as GuardPlacementThreadManager).TryToUnlockThread(self as ObjectReference)
        endif
return r
endFunction
 
Event OnUpdate()
self.Disable()
self.Delete()
endEvent
</source>
 
 
This script should be '''compiled and attached to the Future Activator''' object we created earlier. After you've attached it, make sure to '''fill the properties.'''
 
 
{{ProTip|Note the Type of the result; this could be changed to any data type you need to return.}}
 
 
{{ProTip|As a best practice, only interface with the <code>Future</code> using its member functions, <code>done()</code> and <code>get_result()</code>.}}
 
 
=== Quick Detour: Revisiting the Thread Script ===
 
 
There was a line from our thread script that was commented out, because our Future script didn't exist yet:
 
<source lang="papyrus">
  ;(future as GuardPlacementFuture).result = result
</source>
 
Go back and uncomment this line and recompile the parent thread script. You don't need to recompile all of the children.
 
 
{{WarningBox|This is important! If you don't uncomment this line, your thread will never return results to the Future!}}




== Tying it All Together ==
== Tying it All Together ==


Now that we've created our Threads, our Thread Manager, and our Future script, we can start to put them to work. Since we aren't calling the functions we want to execute directly, we need to change how we do things slightly.  
Now that we've created our Threads and our Thread Manager, we can start to put them to work. Since we aren't calling the functions we want to execute directly, we need to change how we do things slightly.  


The previous execution flow was:
The previous execution flow was:
Line 404: Line 301:
The flow using threads now is:
The flow using threads now is:


# Call an Async function on our Thread Manager, and store the <code>Future</code> it returns.
# Call an Async function on our Thread Manager.
# Later, call the <code>get_results()</code> function of the <code>Future</code> to retrieve the results.
# Handle return events as they are raised and store our results.
 


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:
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
scriptname SummonArmy extends ActiveMagicEffect
 
Quest property GuardPlacementQuest auto
Quest property GuardPlacementQuest auto
{We need a reference to our quest with the threads and Thread Manager defined.}
{We need a reference to our quest with the threads and Thread Manager defined.}
Line 420: Line 316:
ObjectReference Guard1
ObjectReference Guard1
ObjectReference Guard2
ObjectReference Guard2
...
;...and so on
ObjectReference Guard20
ObjectReference Guard9
 
ObjectReference Guard10
Event OnEffectStart(Actor akTarget, Actor akCaster)
Event OnEffectStart(Actor akTarget, Actor akCaster)
if akCaster == Game.GetPlayer()
if akCaster == Game.GetPlayer()
;Cast the Quest as our Thread Manager and store it
;Cast the Quest as our Thread Manager and store it
GuardPlacementThreadManager threadmgr = GuardPlacementQuest as GuardPlacementThreadManager
GuardPlacementThreadManager threadmgr = GuardPlacementQuest as GuardPlacementThreadManager
;Register for the callback event
RegisterForModEvent("MyMod_GuardPlacementCallback", "GuardPlacementCallback")


;Call PlaceConjuredGuardAsync for each Guard and store the returned Future
;Call PlaceConjuredGuardAsync for each Guard and store the returned Future
ObjectReference Guard1Future = threadmgr.PlaceConjuredGuardAsync(Guard)
threadmgr.PlaceConjuredGuardAsync(Guard)
ObjectReference Guard2Future = threadmgr.PlaceConjuredGuardAsync(Guard)
threadmgr.PlaceConjuredGuardAsync(Guard)
ObjectReference Guard3Future = threadmgr.PlaceConjuredGuardAsync(Guard)
;...and so on
;...and so on
ObjectReference Guard19Future = threadmgr.PlaceConjuredGuardAsync(Guard)
threadmgr.PlaceConjuredGuardAsync(Guard)
ObjectReference Guard20Future = threadmgr.PlaceConjuredGuardAsync(Guard)
threadmgr.PlaceConjuredGuardAsync(Guard)
 
threadmgr.wait_all()
                ;Begin working and wait for all of our threads to complete.
                threadmgr.wait_all()
 
;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
endif
endEvent
endEvent
 
Event OnEffectFinish(Actor akTarget, Actor akCaster)
Event OnEffectFinish(Actor akTarget, Actor akCaster)
if akCaster == Game.GetPlayer()
if akCaster == Game.GetPlayer()
Guard1.Disable()
DisableAndDelete(Guard1)
Guard1.Delete()
DisableAndDelete(Guard2)
;...and so on
                ;...and so on
Guard20.Disable()
DisableAndDelete(Guard9)
Guard20.Delete()
DisableAndDelete(Guard10)
endif
endif
endEvent
endEvent
</source>


bool locked = false
Event GuardPlacementCallback(Form akGuard)
;A spin lock is required here to prevent us from writing two guards to the same variable
while locked
Utility.wait(0.1)
endWhile
locked = true
ObjectReference myGuard = akGuard as ObjectReference
if !Guard1
Guard1 = myGuard
elseif !Guard2
Guard2 = myGuard
;...and so on
elseif !Guard9
Guard9 = myGuard
elseif !Guard10
Guard10 = myGuard
endif


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' <code>get_result()</code> function. We don't have to worry about our threads or the state of the Futures; those are freed up and cleared for us by the system.
locked = false
endEvent


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.
function DisableAndDelete(ObjectReference akReference)
akReference.Disable()
akReference.Delete()
endFunction
</source>


== Notes on Futures ==


* Make sure to always call wait_all() after calling your asynchronous functions, or your threads '''will not start'''.
Here, instead of doing the work in our script, registered for a callback Mod Event and delegated the work to the Thread Manager. We then called the Thread Manager's <code>wait_all()</code> function to make sure every thread has completed before continuing. Our return values are handed to us when the <code>GuardPlacementCallback()</code> event is raised.


* We call <code>RegisterForModEvent()</code> on our Thread Manager's <code>OnInit()</code> block. Remember that this will need to be re-registered after '''every game load'''. You will need to define a Player Alias with an attached script that has an <code>OnPlayerLoadGame()</code> event defined that re-registers for this mod event. Any script attached to the quest with the threads can register for the event, and all threads will begin receiving those events.
You'll notice that our callback event employs a spin lock. This is very important, since it is possible for two callback events to accidentally write to the same variable using this pattern.


* 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.
== Notes on Callbacks ==


* You can create as many threads as you want, but I wouldn't recommend more than 10 or so. It depends on your needs, the strain each thread places on the Papyrus VM, and how quickly you need your results.
* You can create as many threads as you want, but I wouldn't recommend more than 10 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 might not be best for you. You may want to create different thread base scripts purpose-built for your various tasks and then call their get_async() functions directly, blocking on <code>queued()</code> until they're available. You can still run many different tasks concurrently this way, even if they're not the same.
* If you need to perform a set of actions that are not all the same, the Thread Manager might not be best for you. You may want to create different thread base scripts purpose-built for your various tasks and then call their get_async() functions directly, blocking on <code>queued()</code> until they're available. You can still run many different tasks concurrently this way, even if they're not the same.


== Playing the Example Plugin ==
== Playing the Example Plugin ==


{{NewFeature| [http://www.creationkit.com/images/a/a5/TutorialExampleMod_Multithreading_Futures.zip Download Tutorial Example Plugin] - A fully functional, installable mod. Includes all tutorial files and source code.}}
{{NewFeature| [http://www.creationkit.com/images/b/bd/TutorialExampleMod_Multithreading_Callbacks.zip Download Tutorial Example Plugin] - A fully functional, installable mod. Includes all tutorial files and source code.}}


The example plugin can be installed using a mod manager, or by dragging all of the zipped files into the Skyrim\Data directory of your installation.
The example plugin can be installed using a mod manager, or by dragging all of the zipped files into the Skyrim\Data directory of your installation.
Line 491: Line 403:
In my personal experience, I saw greatly diminishing returns after 10 threads in this example.
In my personal experience, I saw greatly diminishing returns after 10 threads in this example.
* '''1 Thread:''' Avg. 3.4 seconds to complete
* '''1 Thread:''' Avg. 3.4 seconds to complete
* '''10 Threads:''' Avg. 1.4 seconds to complete
* '''10 Threads:''' Avg. 0.8 seconds to complete
* '''20 Threads:''' Avg. 1.1 seconds to complete
* '''20 Threads:''' Avg. 0.5 seconds to complete


This could be due to the fact that actors are more "expensive" to place than, say, a Static. In another mod, I saw that using 30 threads reduced my object placement time from 8.5 seconds to less than 1 on average. Obviously, profiling your script is critical to determine if your unique application would benefit the most from more or less threads (or threading at all).
Profiling your script is critical to determine if your unique application would benefit the most from more or less threads (or threading at all).


Your experience and times may differ based on your current load order and system performance. Give it a try and see what results you obtain.
Your experience and times may differ based on your current load order and system performance. Give it a try and see what results you obtain.

Latest revision as of 15:59, 27 January 2015


Creating Multithreaded Skyrim Mods Part 3 - Callbacks
Multithreading Series, Chapter 3
Return to Tutorial Hub
LeftArrow.png Previous Tutorial Next TutorialRightArrow.png

We will be implementing a multithreaded solution to our example problem (a Conjuration mod that spawns many actors) using the Callback pattern.


NewFeature.jpg Download Tutorial Example Plugin - A fully functional, installable mod. Includes all tutorial files and source code.


Pattern Overview[edit | edit source]

To recap the Pros and Cons of this approach:

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: Their 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.

Here is a diagram of how the Callback pattern works.

Fig. 3.1, 3.2

Above, you can see that the sequence is:

  1. Register for our Callback Event and call a function on our Thread Manager.
  2. The Thread Manager delegates the work to an available thread.
  3. After the thread finishes, we handle the callback in an event.

That is the Callback pattern. Just like the Futures pattern, we will now piece it all together.

Creation Kit[edit | edit source]

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.


Threads[edit | edit source]

The thread is what will perform the work we want to perform in parallel. Just like the PlaceAtMe() needed to spawn our guards, we expect the result of our Thread to be an ObjectReference.

First, let's define a base Thread "class", called GuardPlacementThread.


scriptname GuardPlacementThread extends Quest hidden
 
;Thread variables
bool thread_queued = false
 
;Variables you need to get things done go here 
ActorBase theGuard
Static theMarker
 
;Thread queuing and set-up
ObjectReference function get_async(ActorBase akGuard, Static akXMarker)
 
    ;Let the Thread Manager know that this thread is busy
    thread_queued = true
 
    ;Store our passed-in parameters to member variables
	theGuard = akGuard
	theMarker = akXMarker
endFunction
 
;Allows the Thread Manager to determine if this thread is available
bool function queued()
	return thread_queued
endFunction
 
;For Thread Manager troubleshooting.
bool function force_unlock()
    clear_thread_vars()
    thread_queued = false
    return true
endFunction
 
;The actual set of code we want to multithread.
Event OnGuardPlacement()
	if thread_queued
		;OK, let's get some work done!
		ObjectReference tempMarker = Game.GetPlayer().PlaceAtMe(theMarker)
		MoveGuardMarkerNearPlayer(tempMarker)
		ObjectReference result = tempMarker.PlaceAtMe(theGuard)
		tempMarker.Disable()
		tempMarker.Delete()
 
        ;OK, we're done - raise event to return results
		RaiseEvent_GuardPlacementCallback(result)
 
        ;Set all variables back to default
		clear_thread_vars()
 
        ;Make the thread available to the Thread Manager again
		thread_queued = false
	endif
endEvent
 
;Called from Event OnGuardPlacement
function MoveGuardMarkerNearPlayer(ObjectReference akMarker)
	;Some difficult calculations, etc
EndFunction
 
function clear_thread_vars()
	;Reset all thread variables to default state
	theGuard = None
	theMarker = None
endFunction

;Create the callback
function RaiseEvent_GuardPlacementCallback(ObjectReference akGuard)
    int handle = ModEvent.Create("MyMod_GuardPlacementCallback")
    if handle
    	ModEvent.PushForm(handle, akGuard as Form)
        ModEvent.Send(handle)
    else
        ;pass
    endif
endFunction


As you can see, our thread does a few important things:

  • It has a get_async() function, which takes in all of the parameters necessary to do the work we need to perform.
  • Event OnGuardPlacement() will fire if the Thread Manager raises the event.
  • The thread "calls back" to any scripts that have registered for our callback event.
  • It clears all of the member variables using clear_thread_vars().
  • We set thread_queued back to False, which tells the Thread Manager that this thread is available to be used again.


InDepth.jpg Reacting to an Event allows the Event OnGuardPlacement() to begin working in parallel to other threads. If we called the functions that do work directly from get_async(), the calling script would block until the work was complete, which would defeat the purpose.


Achtung.png Be diligent about error handling and what could go wrong while your thread is running. If your thread aborts before it can set thread_queued back to False, your thread will become locked and unusable until it times out on the next get_result(). If the thread is hung waiting for an external function call that will never return (such as a PlaceAtMe() on an ObjectReference that cannot complete its OnInit() block), it may become permanently locked.


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.


;GuardPlacementThread01.psc
scriptname GuardPlacementThread01 extends GuardPlacementThread

;GuardPlacementThread02.psc
scriptname GuardPlacementThread02 extends GuardPlacementThread

...

;GuardPlacementThread09.psc
scriptname GuardPlacementThread09 extends GuardPlacementThread

;GuardPlacementThread10.psc
scriptname GuardPlacementThread10 extends GuardPlacementThread


Once all of your Thread child scripts are saved and compiled, attach the 10 child scripts to your Quest.

"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!


Thread Manager[edit | edit source]

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.

Declare any properties that your threads will need in this script; the threads themselves will not have properties defined (since this would be tedious to hook up in the Creation Kit for each thread).


scriptname GuardPlacementThreadManager extends Quest
 
Quest property GuardPlacementQuest auto
{The name of the thread management quest.}
 
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.}

GuardPlacementThread01 thread01
GuardPlacementThread02 thread02
;...and so on
GuardPlacementThread09 thread09
GuardPlacementThread10 thread10
 
Event OnInit()
    ;Register for the event that will start all threads
    ;NOTE - This needs to be re-registered once per load! Use an alias and OnPlayerLoadGame() in a real implementation.
    RegisterForModEvent("MyMod_OnGuardPlacement", "OnGuardPlacement")

    ;Let's cast our threads to local variables so things are less cluttered in our code
    thread01 = GuardPlacementQuest as GuardPlacementThread01
    thread02 = GuardPlacementQuest as GuardPlacementThread02
    ;...and so on
    thread09 = GuardPlacementQuest as GuardPlacementThread09
    thread10 = GuardPlacementQuest as GuardPlacementThread10
EndEvent
 
;The 'public-facing' function that our MagicEffect script will interact with.
function PlaceConjuredGuardAsync(ActorBase akGuard)
    if !thread01.queued()
        thread01.get_async(akGuard, XMarker)
    elseif !thread02.queued()
	thread02.get_async(akGuard, XMarker)
    ;...and so on
    elseif !thread09.queued()
        thread09.get_async(akGuard, XMarker)
    elseif !thread10.queued()
        thread10.get_async(akGuard, XMarker)
    else
	;All threads are queued; start all threads, wait, and try again.
        wait_all()
        PlaceConjuredGuardAsync(akGuard)
	endif
endFunction
 
function wait_all()
    RaiseEvent_OnGuardPlacement()
    begin_waiting()
endFunction

function begin_waiting()
    bool waiting = true
    int i = 0
    while waiting
        if thread01.queued() || thread02.queued() || thread03.queued() || thread04.queued() || thread05.queued() || \
           thread06.queued() || thread07.queued() || thread08.queued() || thread09.queued() || thread10.queued()
            i += 1
            Utility.wait(0.1)
            if i >= 100
                debug.trace("Error: A catastrophic error has occurred. All threads have become unresponsive. Please debug this issue or notify the author.")
                i = 0
                ;Fail by returning None. The mod needs to be fixed.
                return
            endif
        else
            waiting = false
        endif
    endWhile
endFunction

;Create the ModEvent that will start this thread
function RaiseEvent_OnGuardPlacement()
    int handle = ModEvent.Create("MyMod_OnGuardPlacement")
    if handle
        ModEvent.Send(handle)
    else
        ;pass
    endif
endFunction


The PlaceConjuredGuardAsync() function handles making sure that our work gets delegated to an available thread.


InDepth.jpg Threads begin working when either:
  • All threads are queued.
  • When wait_all() is called from the calling script.


Compile and attach this script to your GuardPlacementQuest. then, double-click the Thread Manager script and fill the properties. Once you've done that, your quest's script section should look something like this:

Multithreading quest scripts.JPG


Tying it All Together[edit | edit source]

Now that we've created our Threads and our Thread Manager, we can start to put them to work. Since we aren't calling the functions we want to execute directly, we need to change how we do things slightly.

The previous execution flow was:

  1. Call each function, one by one, and store the results. (PlaceAtMe(), etc)

The flow using threads now is:

  1. Call an Async function on our Thread Manager.
  2. Handle return events as they are raised and store our results.

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.


scriptname SummonArmy extends ActiveMagicEffect
 
Quest property GuardPlacementQuest auto
{We need a reference to our quest with the threads and Thread Manager defined.}
ActorBase property Guard auto

ObjectReference Guard1
ObjectReference Guard2
;...and so on
ObjectReference Guard9
ObjectReference Guard10
 
Event OnEffectStart(Actor akTarget, Actor akCaster)
	if akCaster == Game.GetPlayer()
		;Cast the Quest as our Thread Manager and store it
		GuardPlacementThreadManager threadmgr = GuardPlacementQuest as GuardPlacementThreadManager

		;Register for the callback event
		RegisterForModEvent("MyMod_GuardPlacementCallback", "GuardPlacementCallback")

		;Call PlaceConjuredGuardAsync for each Guard and store the returned Future
		threadmgr.PlaceConjuredGuardAsync(Guard)
		threadmgr.PlaceConjuredGuardAsync(Guard)
		;...and so on
		threadmgr.PlaceConjuredGuardAsync(Guard)
		threadmgr.PlaceConjuredGuardAsync(Guard)
		threadmgr.wait_all()
	endif
endEvent
 
Event OnEffectFinish(Actor akTarget, Actor akCaster)
	if akCaster == Game.GetPlayer()
		DisableAndDelete(Guard1)
		DisableAndDelete(Guard2)
                ;...and so on
		DisableAndDelete(Guard9)
		DisableAndDelete(Guard10)
	endif
endEvent

bool locked = false
Event GuardPlacementCallback(Form akGuard)
	;A spin lock is required here to prevent us from writing two guards to the same variable
	while locked
		Utility.wait(0.1)
	endWhile
	locked = true

	ObjectReference myGuard = akGuard as ObjectReference

	if !Guard1
		Guard1 = myGuard
	elseif !Guard2
		Guard2 = myGuard
	;...and so on
	elseif !Guard9
		Guard9 = myGuard
	elseif !Guard10
		Guard10 = myGuard
	endif

	locked = false
endEvent

function DisableAndDelete(ObjectReference akReference)
	akReference.Disable()
	akReference.Delete()
endFunction


Here, instead of doing the work in our script, registered for a callback Mod Event and delegated the work to the Thread Manager. We then called the Thread Manager's wait_all() function to make sure every thread has completed before continuing. Our return values are handed to us when the GuardPlacementCallback() event is raised.

You'll notice that our callback event employs a spin lock. This is very important, since it is possible for two callback events to accidentally write to the same variable using this pattern.


Notes on Callbacks[edit | edit source]

  • You can create as many threads as you want, but I wouldn't recommend more than 10 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 might not be best for you. You may want to create different thread base scripts purpose-built for your various tasks and then call their get_async() functions directly, blocking on queued() until they're available. You can still run many different tasks concurrently this way, even if they're not the same.


Playing the Example Plugin[edit | edit source]

NewFeature.jpg Download Tutorial Example Plugin - A fully functional, installable mod. Includes all tutorial files and source code.

The example plugin can be installed using a mod manager, or by dragging all of the zipped files into the Skyrim\Data directory of your installation.

There are some differences between the examples provided here and the example plugin's code. Particularly, the example plugin is implemented with 20 threads instead of 10, and shows how you might scale the number of threads you use up or down.

When you start the game, you will be given 3 spells: Summon Army (Single Threaded), Summon Army (10 Threads), and Summon Army (20 Threads). These will let you play around with the example scenario provided and see the time difference in completing the script between 1, 10, and 20 threads. Casting the spell will summon 20 Stormcloak Soldiers, which will disappear after 15 seconds. Wait until all guards have disappeared before casting the spell (or another spell) again.

In my personal experience, I saw greatly diminishing returns after 10 threads in this example.

  • 1 Thread: Avg. 3.4 seconds to complete
  • 10 Threads: Avg. 0.8 seconds to complete
  • 20 Threads: Avg. 0.5 seconds to complete

Profiling your script is critical to determine if your unique application would benefit the most from more or less threads (or threading at all).

Your experience and times may differ based on your current load order and system performance. Give it a try and see what results you obtain.

This example plugin is provided to help understand the principles outlined in this tutorial, not for real gameplay. The mod's spells will cease to function after saving and loading the game.

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