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

no edit summary
imported>Chesko
(User script updates.)
imported>Chesko
 
(8 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 29: Line 52:


<gallery widths="240px" heights="200px" 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
</gallery>
</gallery>


Line 203: Line 226:
function PlaceConjuredGuardAsync(ActorBase akGuard)
function PlaceConjuredGuardAsync(ActorBase akGuard)
     if !thread01.queued()
     if !thread01.queued()
        debug.trace("[Callback] Selected thread01")
         thread01.get_async(akGuard, XMarker)
         thread01.get_async(akGuard, XMarker)
     elseif !thread02.queued()
     elseif !thread02.queued()
        debug.trace("[Callback] Selected thread02")
thread02.get_async(akGuard, XMarker)
thread02.get_async(akGuard, XMarker)
     ;...and so on
     ;...and so on
     elseif !thread09.queued()
     elseif !thread09.queued()
        debug.trace("[Callback] Selected thread09")
         thread09.get_async(akGuard, XMarker)
         thread09.get_async(akGuard, XMarker)
     elseif !thread10.queued()
     elseif !thread10.queued()
        debug.trace("[Callback] Selected thread10")
         thread10.get_async(akGuard, XMarker)
         thread10.get_async(akGuard, XMarker)
     else
     else
Line 269: 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]]




Line 339: Line 358:
ObjectReference myGuard = akGuard as ObjectReference
ObjectReference myGuard = akGuard as ObjectReference


debug.trace("[Callback] Assigning " + myGuard + "...")
if !Guard1
if !Guard1
Guard1 = myGuard
Guard1 = myGuard
Line 361: Line 379:




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


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.
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.
Line 371: Line 389:


* 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 387: Line 406:
* '''20 Threads:''' Avg. 0.5 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.
Anonymous user