[Closed] MultiThreading in DotNet
Hello,
There has been a couple of threads (no pun intended) recently about running operations as background proceses. You will all be aware that when you perform any intensive calculation process within MXS that pretty much ties up that session of max.
I had used the BackgroundWorker class in my dotnet programming a while ago, and thought i might be able to employ this in Max.
Attached is a small test I set up that demonstrates use of this class. You will notice that when you run the example via MXS, the 3Dsmax UI will be completely tied up. Via the Dotnet method, you can still use the max interface and do other tasks within a single copy of max. It also supports cancellation so you can abort an intensive task should you need to.
I've shared my results as any one interested might be able to use it as a base and adapt it.
The backgroundworker class also has an option for updating progress. I havent implemented it here as it didn't seem to be neccesary, and I have only used it because you use it to update between threads in a windows forms application.
Only thing to note is that i should cancel the threads in the close handler of the rollout in case you exit mid thread.
I'd be interested if anyone can perform any non numeric calculations via this method. The threads recently talked about doing intersectray calculations so at the very least it might be able to perform these in a single instance of Max without locking the UI up. Hopefully this is a start for someone. Sorry if the formatting is a bit awry.
Im not sure it’s operating outside max, but it is certainly on a different thread to the UI.
Global Thread,MainThread,SecondaryThread
Fn WorkThread sender e =
(
for i = 1 to Thread.spnLoops.value do
(
If MainThread.CancellationPending Then
(
e.cancel = true
Thread.lbl2.text = "Primary Thread Aborted"
)
else
(
-- test for object 'Box01'
-- Box01'.height = i
--max zoomext sel
-- or do some nonsense calculation
local asum = i*pi
local prog = (i as float)/Thread.spnLoops.value * 100
-- MainThread.ReportProgress prog asum
Thread.lbl2.text = asum as string
Thread.pb2.value = prog
sleep 0.05
)
)
Thread.lbl2.text = "Primary Thread Complete"
Thread.pb2.value = 0
)
Fn WorkThread2 sender e =
(
for i = 1 to Thread.spnLoops.value do
(
If SecondaryThread.CancellationPending Then
(
e.cancel = true
Thread.lbl3.text = "Secondary Thread Aborted"
)
else
(
-- do some nonsense calculation
local asum = i/pi
local prog = (i as float)/Thread.spnLoops.value * 100
-- MainThread.ReportProgress prog asum
Thread.lbl3.text = asum as string
Thread.pb3.value = prog
sleep 0.025
)
)
Thread.lbl3.text = "Secondary Thread Complete"
Thread.pb3.value = 0
)
Fn MxsFn =
(
for i = 1 to Thread.spnLoops.value do
(
-- test for object 'Box01'
-- Box01'.height = i
-- max zoomext sel
-- or do some nonsense calculation
local asum = i*pi
local prog = (i as float)/ Thread.spnLoops.value * 100
Thread.lbl1.text = asum as string
Thread.pb1.value = prog
sleep 0.025
)
Thread.lbl1.text = "MXS Fn Complete"
Thread.pb1.value = 0
)
Fn UpdateThread sender e =
(
format "FnValue - % Percentage done - %
" e.progresspercentage e.userstate
)
-- Specify the BackgroundWorker Class
MainThread = dotnetobject "System.ComponentModel.BackGroundWorker"
-- MainThread.WorkerReportsProgress = true
MainThread.WorkerSupportsCancellation = true
dotNet.addEventHandler MainThread "DoWork" WorkThread
--dotNet.addEventHandler MainThread "ProgressChanged" UpdateThread
SecondaryThread = dotnetobject "System.ComponentModel.BackGroundWorker"
SecondaryThread.WorkerSupportsCancellation = true
-- SecondaryThread.WorkerReportsProgress = true
dotNet.addEventHandler SecondaryThread "DoWork" WorkThread2
--dotNet.addEventHandler SecondaryThread "ProgressChanged" UpdateThread
rollout Thread "Running Multiple Threads in DotNet" width:728 height:147
(
GroupBox grp1 "MaxScript Function Execution" pos:[7,28] width:192 height:114
GroupBox grp2 "DotNet BackgroundWorker Class" pos:[202,7] width:522 height:135
progressBar pb1 "" pos:[19,88] width:168 height:16 color:(color 255 255 0)
GroupBox grp3 "Function Value" pos:[18,44] width:171 height:39
button btnMXstart "Start" pos:[16,110] width:173 height:23
label lbl1 "" pos:[24,61] width:160 height:18
progressBar pb2 "" pos:[394,33] width:168 height:25 color:(color 0 255 0)
GroupBox grp4 "Function Value" pos:[215,22] width:171 height:39
button btnDNstart "Start Primary" pos:[566,31] width:109 height:28
label lbl2 "" pos:[220,37] width:161 height:18
progressBar pb3 "" pos:[394,70] width:168 height:24 color:(color 30 10 190)
GroupBox grp18 "Function Value" pos:[215,62] width:170 height:39
button btnDNboth "Run Both Threads" pos:[214,105] width:461 height:28
label lbl3 "" pos:[220,78] width:158 height:18
button btncancel1 "Cancel" pos:[678,31] width:42 height:28
button btnDN2 "Start Secondary" pos:[566,67] width:109 height:28
button btnCancel2 "Cancel" pos:[678,66] width:42 height:28
spinner spnLoops "Number of Loop Iterations" pos:[114,8] width:84 height:16 range:[10,10000,100] type:#integer
button btncancelboth "Cancel" pos:[679,104] width:42 height:28
on Thread open do
(
try
(
If MainThread.IsBusy do MainThread.CancelAsync()
)
catch()
)
on btnDN2 pressed do
(
if not SecondaryThread.IsBusy do SecondaryThread.RunWorkerAsync()
)
on btnMXstart pressed do MXSFn()
on btnDNstart pressed do
(
if not MainThread.IsBusy do MainThread.RunWorkerAsync()
)
on btnDNboth pressed do
(
if (not MainThread.IsBusy) and (not SecondaryThread.IsBusy) do
(
MainThread.RunWorkerAsync()
SecondaryThread.RunWorkerAsync()
)
)
on btncancel1 pressed do
If MainThread.IsBusy Then MainThread.CancelAsync()
on btnDN2 pressed do
if not SecondaryThread.IsBusy do SecondaryThread.RunWorkerAsync()
on btnCancel2 pressed do
If SecondaryThread.IsBusy Then SecondaryThread.CancelAsync()
on btncancelboth pressed do
(
if MainThread.IsBusy do MainThread.CancelAsync()
if SecondaryThread.IsBusy do SecondaryThread.CancelAsync()
)
)
createdialog thread style:#(#style_toolwindow, #style_sysmenu)
This is very interesting. Not sure what to do with it at this point but know that it is possible is very cool.
Very interesting indeed if it can be used with max functions! Realtime collision detection and stuff like that maybe… why is there only 24 hours in a day…
Thanks for sharing!
-Johan
Hi LoneRobot!
This is a very interesting idea !! Can’t wait to play with it…
Some questions thought…how do you re-sync the threads to the UI thread?? Anytime you do any reading about threads, they always say not to update the UI from any thread other then the UI thread…??
Sorry if that’s explained in the code was up till 2 in the morning re-building servers and haven’t had time to look into the code…
Shane
Hi Shane,
I have placed reference to this issue in the script but i'll explain it a bit better. You are right about updating UI threads - in dotnet this will throw an exception if not done properly. The backgroundworker class allows this to happen via the ProgressChanged handler.
you will see in the script this can only happen if you set the object's [i]workerreportsprogress[/i] property to true.
The mainthread, once started with the runworkerasync() function performs the work function, and you choose when to update back to the UI thread via the reportprogress method. This takes two arguments, a percentage integer and a userstate object. So, you can put pretty much anything in this object - i have placed an array with various properties in so i can can the userstate[integer]method to retrieve these in the[i] updatethread [/i]function.the[i] ProgressChangedEventArgs [/i]therefore contains a[i] progresspercentage[/i] property to update a progressbar or similar and the [i]userstate[/i] property for anything else. for example I have used this to pass the[i] e.progresspercentage[/i] argument to the painthandler of a control and use GDI+ to draw a custom progressbar over a bitmap that is loading. BTW you dont have to provide a userstate property, the eventarg is overloaded (meaning you can choose which arguments to supply) so that you can just return the progress percentage should you wish.
Where this fits in with the example i posted im not sure, since when i tested it it didnt seem to make any difference whether i updated via the backgroundworker progressupdate or the max work thread. if this has something to do with the way max implements the dotnet in the package i dont know.
But as a concept i thought that its best asset is being able to free the processing from the main max UI thread, something that maxscript cannot do. I will take this as a start and see where it goes.
This is some great info. I’ve already come up with a quick script to make use of it:
(
local theLocalPath
local theThread
fn copyFileTest sender e =
(
deleteFile (maxfilepath + maxfilename)
copyFile theLocalPath (maxfilepath + maxfilename)
)
theLocalPath = getDir #scene + "\\" + maxfilename
saveMaxFile theLocalPath clearNeedSaveFlag:true useNewFile:false quiet:true
theThread = dotnetobject "System.ComponentModel.BackGroundWorker"
dotNet.addEventHandler theThread "DoWork" copyFileTest
theThread.RunWorkerAsync()
)
All it does is save a ‘temporary’ copy of your max file locally and then in a new thread copy it to your max file’s ‘real’ location. Why you might ask? Well i found that saving large max files on our network can take 20 – 30 seconds or more depending how big the file is, whilst saving locally takes around 2 – 3 seconds for the same file. Save enough times in the day and this can easily add up. We use Martin Breidt’s IncrementalSave script here at work and i intend to modify it using this method to speed up our general workflow when i get a spare moment.
Nice one Joel, we have a similar issue where we are although our NAS/local save isnt 20-30 seconds more. But it does make a difference when you are loading and saving large files. I know some of the animators like saving numerous local copies first. I wonder if you were incrementing locally you could also use a timer so that you set an interval to copy to the server location, or even trigger it by a #filepresavecallback that copies when max performs an autobackup. That way it would copy only when the main max UI was tied up in the autosave.
keep em coming!
[font=‘Courier New’][/font]
Very cool… I saw that class and dreamed of “free[ing] the processing from the main max UI thread…” on a few of our scripts and never got a chance to tinker with it… Now it’s readily available… Thanks for sharing!