Difference between revisions of "Creating Multithreaded Skyrim Mods Part 2 - Futures"
imported>Chesko |
imported>Chesko (Removed thread_id paradigm, other changes.) |
||
Line 26: | Line 26: | ||
# The Thread Manager delegates the work to an available '''thread'''. | # The Thread Manager delegates the work to an available '''thread'''. | ||
# The Thread Manager returns a '''Future''' to the caller, who stores it as an ObjectReference. | # The Thread Manager returns a '''Future''' to the caller, who stores it as an ObjectReference. | ||
# The calling script calls <code> | # The calling script calls <code>wait_all()</code>, which starts all queued threads and waits for them to finish. | ||
# | # The calling script calls <code>get_result()</code> on the Future. The Future returns the result to the caller. | ||
# After the result has been read, the Future is deleted. | # After the result has been read, the Future is deleted. | ||
That is the Futures pattern in a nutshell. Now, we will implement the various parts of this pattern, and put it all together at the end. | That is the Futures pattern in a nutshell. Now, we will implement the various parts of this pattern, and put it all together at the end. | ||
== Creation Kit == | == Creation Kit == | ||
Line 41: | Line 41: | ||
<gallery widths="240px" heights="120px" perrow="3"> | <gallery widths="240px" heights="120px" perrow="3"> | ||
Image:Multithreading-fig1-1.JPG|<b>Fig. 2. | Image:Multithreading-fig1-1.JPG|<b>Fig. 2.4</b>: <br> Create Quest | ||
Image:Multithreading-fig1-2.JPG|<b>Fig. 2. | Image:Multithreading-fig1-2.JPG|<b>Fig. 2.5</b>: <br> Create Future Activator | ||
Image:Multithreading-fig1-3.JPG|<b>Fig. 2. | Image:Multithreading-fig1-3.JPG|<b>Fig. 2.6</b>: <br> Create Anchor | ||
</gallery> | </gallery> | ||
Line 60: | Line 60: | ||
;Thread control variables | ;Thread control variables | ||
ObjectReference future | ObjectReference future | ||
bool thread_queued = false | bool thread_queued = false | ||
Line 72: | Line 71: | ||
;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 | ;Create the Future that will contain our result | ||
future = akFutureAnchor.PlaceAtMe(akFuture) | future = akFutureAnchor.PlaceAtMe(akFuture) | ||
return future | return future | ||
endFunction | endFunction | ||
Line 107: | Line 84: | ||
bool function busy() | bool function busy() | ||
return thread_queued | return thread_queued | ||
endFunction | endFunction | ||
Line 131: | Line 103: | ||
;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) ;We could have passed PlayerRef in as a get_async() parameter, too | ||
Line 168: | Line 140: | ||
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>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 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 returns its results back to the Future it created. | ||
Line 182: | Line 152: | ||
{{InDepth| | {{InDepth|Reacting to an Event allows the <code>Event OnGuardPlacement()</code> to begin working in parallel to other threads. If we called the functions that do work directly from <code>get_async()</code>, the calling script would block until the work was complete, which would defeat the purpose.}} | ||
Line 219: | Line 189: | ||
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. | 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). | |||
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. | 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. | ||
Line 237: | Line 207: | ||
Static property XMarker auto | Static property XMarker auto | ||
{ | {Something a thread needs; our threads don't declare their own properties.} | ||
GuardPlacementThread01 thread01 | GuardPlacementThread01 thread01 | ||
Line 245: | Line 215: | ||
GuardPlacementThread10 thread10 | 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 | ;Let's cast our threads to local variables so things are less cluttered in our code | ||
thread01 = GuardPlacementQuest as GuardPlacementThread01 | thread01 = GuardPlacementQuest as GuardPlacementThread01 | ||
Line 258: | Line 226: | ||
thread09 = GuardPlacementQuest as GuardPlacementThread09 | thread09 = GuardPlacementQuest as GuardPlacementThread09 | ||
thread10 = GuardPlacementQuest as GuardPlacementThread10 | thread10 = GuardPlacementQuest as GuardPlacementThread10 | ||
EndEvent | EndEvent | ||
Line 282: | Line 243: | ||
future = thread10.get_async(GuardPlacementFutureActivator, GuardPlacementFutureAnchor, akGuard, XMarker) | future = thread10.get_async(GuardPlacementFutureActivator, GuardPlacementFutureAnchor, akGuard, XMarker) | ||
else | else | ||
;All threads are busy; wait and try again. | ;All threads are busy; start all threads, wait, and try again. | ||
wait_all() | |||
endif | endif | ||
endWhile | endWhile | ||
return future | return future | ||
endFunction | |||
function wait_all() | |||
RaiseEvent_OnGuardPlacement() | |||
begin_waiting() | |||
endFunction | |||
function begin_waiting() | |||
bool waiting = true | |||
int i = 0 | |||
while waiting | |||
if thread01.busy() || thread02.busy() || thread03.busy() || thread04.busy() || thread05.busy() || \ | |||
thread06.busy() || thread07.busy() || thread08.busy() || thread09.busy() || thread10.busy() | |||
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 | endFunction | ||
Line 315: | Line 294: | ||
else | else | ||
debug.trace("Warning: An unresponsive thread was successfully unlocked.") | debug.trace("Warning: An unresponsive thread was successfully unlocked.") | ||
endif | |||
endFunction | |||
;Create the ModEvent that will start this thread | |||
function RaiseEvent_OnGuardPlacement(int iThreadId) | |||
int handle = ModEvent.Create("MyMod_OnGuardPlacement") | |||
if handle | |||
ModEvent.Send(handle) | |||
else | |||
;pass | |||
endif | |||
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 | endif | ||
endFunction | endFunction | ||
Line 320: | Line 319: | ||
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. The function then returns a <code>Future</code> once an available thread is found. | ||
{{InDepth|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: | '''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: | ||
Line 329: | Line 334: | ||
== Back to the Future == | == 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. If the result has arrived, <code>get_result()</code> returns it; otherwise, it waits for the result to arrive, and ''then'' returns it. | |||
Line 441: | Line 446: | ||
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 | ||
Line 453: | Line 456: | ||
ObjectReference Guard19Future = threadmgr.PlaceConjuredGuardAsync(Guard) | ObjectReference Guard19Future = threadmgr.PlaceConjuredGuardAsync(Guard) | ||
ObjectReference Guard20Future = threadmgr.PlaceConjuredGuardAsync(Guard) | ObjectReference Guard20Future = threadmgr.PlaceConjuredGuardAsync(Guard) | ||
;Begin working and wait for all of our threads to complete. | |||
threadmgr.wait_all() | |||
;Collect the results | ;Collect the results | ||
Line 481: | Line 487: | ||
== Notes on Futures == | == Notes on Futures == | ||
* Make sure to always call wait_all() after calling your asynchronous functions, or your threads will not start. | |||
* 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. | |||
* 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. | * 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. |
Revision as of 10:08, 22 January 2015
Creating Multithreaded Skyrim Mods Part 2 - Futures | |
---|---|
Multithreading Series, Chapter 2 | |
Return to Tutorial Hub | |
Previous Tutorial | Next Tutorial |
This tutorial picks up where our introduction left off. We will be implementing a multithreaded solution to our example problem (a Conjuration mod that spawns many actors) using the Futures pattern.
Download Tutorial Example Plugin - A fully functional, installable mod. Includes all tutorial files.
Pattern Overview
Here is a diagram of how the Futures pattern works.
Above, you can see that the sequence is:
- Call a function on our Thread Manager.
- The Thread Manager delegates the work to an available thread.
- The Thread Manager returns a Future to the caller, who stores it as an ObjectReference.
- The calling script calls
wait_all()
, which starts all queued threads and waits for them to finish. - The calling script calls
get_result()
on the Future. The Future returns the result to the caller. - After the result has been read, the Future is deleted.
That is the Futures pattern in a nutshell. Now, we will implement the various parts of this pattern, and put it all together at the end.
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
Future
. 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
PlaceAtMe()
Futures on this object later on.
Threads
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 control variables
ObjectReference future
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(Activator akFuture, ObjectReference akFutureAnchor, 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
;Create the Future that will contain our result
future = akFutureAnchor.PlaceAtMe(akFuture)
return future
endFunction
;Allows the Thread Manager to determine if this thread is available
bool function busy()
return thread_queued
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.
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) ;We could have passed PlayerRef in as a get_async() parameter, too
MoveGuardMarkerNearPlayer(tempMarker)
ObjectReference result = tempMarker.PlaceAtMe(theGuard)
tempMarker.Disable()
tempMarker.Delete()
;OK, we're done - let's pass the result back to the future
;UNCOMMENT THIS after compiling GuardPlacementFuture
;(future as GuardPlacementFuture).result = result
;Set all variables back to default
clear_thread_vars()
;Make the thread available to the Thread Manager again
thread_queued = false
endif
endEvent
;Another function that does things we want to multithread.
function MoveGuardMarkerNearPlayer(ObjectReference akMarker)
;Expensive SetPosition, GetPosition, FindNearestRef, etc calls here (illustration only)
endFunction
function clear_thread_vars()
;Reset all thread variables to default state
theGuard = None
theMarker = None
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.
get_async()
creates aFuture
which will eventually make its way back to the script that called our Thread Manager function.
Event OnGuardPlacement()
will fire if the Thread Manager raises the event.
- The thread returns its results back to the Future it created.
- It clears all of the member variables using
clear_thread_vars()
.
- We set
thread_queued
back toFalse
, which tells the Thread Manager that this thread is available to be used again.
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
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).
In the end, the function that we call in our Thread Manager will return a Future
, which we can use to get our return value later.
scriptname GuardPlacementThreadManager extends Quest
Quest property GuardPlacementQuest auto
{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
{Something a thread needs; our threads don't declare their own properties.}
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.
ObjectReference function PlaceConjuredGuardAsync(ActorBase akGuard)
int i = 0
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; start all threads, wait, and try again.
wait_all()
endif
endWhile
return future
endFunction
function wait_all()
RaiseEvent_OnGuardPlacement()
begin_waiting()
endFunction
function begin_waiting()
bool waiting = true
int i = 0
while waiting
if thread01.busy() || thread02.busy() || thread03.busy() || thread04.busy() || thread05.busy() || \
thread06.busy() || thread07.busy() || thread08.busy() || thread09.busy() || thread10.busy()
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
;A helper function that can avert permanent thread failure if something goes wrong
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 this thread
function RaiseEvent_OnGuardPlacement(int iThreadId)
int handle = ModEvent.Create("MyMod_OnGuardPlacement")
if handle
ModEvent.Send(handle)
else
;pass
endif
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. The function then returns a Future
once an available thread is found.
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
Back to the Future
Futures are 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 Future
and later call the Future
object's get_result()
function. If the result has arrived, get_result()
returns it; otherwise, it waits for the result to arrive, and then returns it.
Let's create our Future:
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
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.
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:
;(future as GuardPlacementFuture).result = result
Go back and uncomment this line and recompile the parent thread script. You don't need to recompile all of the children.
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.
The previous execution flow was:
- Call each function, one by one, and store the results. (
PlaceAtMe()
, etc)
The flow using threads now is:
- Call an Async function on our Thread Manager, and store the
Future
it returns. - Later, call the
get_results()
function of theFuture
to retrieve the 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
...
ObjectReference Guard20
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
;Call PlaceConjuredGuardAsync for each Guard and store the returned Future
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)
;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
endEvent
Event OnEffectFinish(Actor akTarget, Actor akCaster)
if akCaster == Game.GetPlayer()
Guard1.Disable()
Guard1.Delete()
;...and so on
Guard20.Disable()
Guard20.Delete()
endif
endEvent
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_result()
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.
Even though all of the threads are working in parallel and might not finish at the same time, the get_result()
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 get_result()
function will pause the script until a result is available. Then the thread 2 Future's result is gathered, and so on.
Notes on Futures
- Make sure to always call wait_all() after calling your asynchronous functions, or your threads will not start.
- We call
RegisterForModEvent()
on our Thread Manager'sOnInit()
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 anOnPlayerLoadGame()
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.
- 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.
- You can create as many threads as you want, but I wouldn't recommend more than 30 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 busy() until they're available. You can still run many different tasks concurrently this way, even if they're not the same.
Previous Tutorial | Return to Tutorial Hub | Next Tutorial |