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

User script updates.
imported>Chesko
m
imported>Chesko
(User script updates.)
Line 274: Line 274:
== 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 282: Line 282:
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 298: Line 297:
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
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
;...and so on
Guard19 = (Guard19Future as GuardPlacementFuture).get_result()
threadmgr.PlaceConjuredGuardAsync(Guard)
Guard20 = (Guard20Future as GuardPlacementFuture).get_result()
threadmgr.PlaceConjuredGuardAsync(Guard)
threadmgr.wait_all()
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


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.
debug.trace("[Callback] Assigning " + myGuard + "...")
if !Guard1
Guard1 = myGuard
elseif !Guard2
Guard2 = myGuard
;...and so on
elseif !Guard9
Guard9 = myGuard
elseif !Guard10
Guard10 = myGuard
endif


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.
locked = false
endEvent
 
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() function to make sure every thread has completed before continuing. Our return values are handed to us when the GuardPlacementCallback() 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.
Anonymous user