Notifications
Clear all

[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) 
20 Replies
 PEN

This is very interesting. Not sure what to do with it at this point but know that it is possible is very cool.

 JHN

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

 PEN

Well thats it isn’t it, multi thread your day!

1 Reply
 JHN
(@jhn)
Joined: 11 months ago

Posts: 0

You’re right, how could I’ve missed it

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.

Awesome!! I’m already thinking about where I can use it

Shane

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!

Page 1 / 2