using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; using Nona; namespace Foundations { /// /// Implements IThreadedLoopManager by putting the simulation and render work on dedicated threads and the draw /// work on the main thread during the idle tick. /// public class ParallelThreadedLoopManager : IThreadedLoopManager { public static string SimulationThreadName = "Main simulation thread"; public static string RenderThreadName = "Main render thread"; private Thread StartThread(string name, Func loopBody) { var thread = new Thread(() => { while (loopBody()); }); // Don't prevent program shutdown if the threads are still running. thread.IsBackground = true; thread.Name = name; thread.Start(); return thread; } public void StartSimulation(Func loopBody) { if (_simulationThread != null) { throw new InvalidOperationException("Cannot start the simulation thread multiple times."); } _simulationThread = StartThread(SimulationThreadName, loopBody); } public void StartRender(Func loopBody) { if (_renderThread != null) { throw new InvalidOperationException("Cannot start the render thread multiple times."); } _renderThread = StartThread(RenderThreadName, loopBody); } /// /// Returns whether the app is idle (no messages being pumped by the main UI message pump) or not. /// private bool IsAppIdle() { NativeMethods.Message message; return !NativeMethods.PeekMessage( out message, hWnd: IntPtr.Zero, // Get messages from any windows associated with the current thread. messageFilterMin: 0, messageFilterMax: 0, // Return all available messages. wRemoveMsg: 0 // Messages are not removed from the queue. ); } private void ApplicationIdle(object sender, EventArgs args) { // See https://docs.microsoft.com/en-us/archive/blogs/tmiller/my-last-post-on-render-loops-hopefully while (_drawLoopOngoing && (_drawLoopOngoing = _drawLoopBody()) && IsAppIdle()) ; } public void StartDraw(Func loopBody) { if (_drawLoopBody != null) { throw new InvalidOperationException("Cannot StartDraw multiple times."); } _drawLoopBody = loopBody; System.Windows.Forms.Application.Idle += ApplicationIdle; } public void Dispose() { System.Windows.Forms.Application.Idle -= ApplicationIdle; // Since updates to states are atomic (we build the new state in its entirety before swapping it to be the // current state) we can abort the threads without waiting from them to get to a good stopping place. // // TODO(numsgil): Thread.Abort is frowned upon generally. The threads are background threads so we could // be safe just leaving them at program exit but if we're before program exit and this instance of main is // in eg: a unit test we'd want to kill the threads sooner than later. But the threads are runing member // functions of Main so we can't safely continue them after Main is disposed, unless we don't need to // dispose of any of the data they reference. if (_simulationThread != null) { _simulationThread.Abort(); _simulationThread.Join(); _simulationThread = null; } if (_renderThread != null) { _renderThread.Abort(); _renderThread.Join(); _renderThread = null; } } private Thread _simulationThread; private Thread _renderThread; private Func _drawLoopBody; private bool _drawLoopOngoing = true; } }