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;
}
}