Here it is(Note: quickly written prototype code lies ahead):
using System.Threading;
namespace ThreadUtils
{
public class Job : IAsyncResult
{
Action jobTask;
Exception exception = null;
bool isCompleted;
public bool IsCompleted
{
get
{
return isCompleted;
}
}
AsyncCallback callback = null;
object asyncState;
public object AsyncState
{
get
{
return asyncState;
}
}
ManualResetEvent waitHandle = null;
public WaitHandle AsyncWaitHandle
{
get
{
return GetWaitHandle();
}
}
public bool CompletedSynchronously
{
get
{
return false;
}
}
public Job(Action workToDo, AsyncCallback asyncCallback = null, object stateObject = null)
{
jobTask = workToDo;
callback = asyncCallback;
asyncState = stateObject;
}
public void Run()
{
try
{
jobTask();
}
catch( Exception e)
{
exception = e;
}
}
public void Completed()
{
if (exception != null)
{
throw exception;
}
if (callback != null)
{
callback(this);
}
isCompleted = true;
if(waitHandle != null)
{
waitHandle.Set();
}
}
object waitHandleLock = new object();
WaitHandle GetWaitHandle()
{
lock(waitHandleLock)
{
if(waitHandle == null)
{
waitHandle = new ManualResetEvent(false);
}
if(isCompleted)
{
waitHandle.Set();
}
}
return waitHandle;
}
}
public class JobScheduler : MonoBehaviour
{
Queue<Job> workToBeDone = new Queue<Job>();
Queue<Job> completedWork = new Queue<Job>();
Thread workerThread;
ManualResetEvent workerThreadResetEvent = new ManualResetEvent(false);
static JobScheduler instance;
public static JobScheduler Instance
{
get
{
return instance ?? (instance = new GameObject("JobScheduler").AddComponent<JobScheduler>());
}
}
void Awake()
{
StartCoroutine(Run());
//If you want this to run in Editor use this instead
//IEnumerator coroutine = Run();
//EditorApplication.update += delegate{ coroutine.MoveNext(); };
}
void OnDestroy()
{
if(workerThread != null)
{
workerThread.Abort();
}
}
public Job AddJob(Action workToDo, AsyncCallback callback = null, object asyncState = null)
{
Job job = new Job(workToDo, callback, asyncState);
lock (workToBeDone)
{
workToBeDone.Enqueue(job);
}
workerThreadResetEvent.Set();
return job;
}
IEnumerator Run()
{
workerThread = new Thread(new ThreadStart(ProcessWork));
workerThread.IsBackground = true;
workerThread.Start();
while (true)
{
while (completedWork.Count > 0)
{
Job doneWork = null;
lock (completedWork)
{
doneWork = completedWork.Dequeue();
}
doneWork.Completed();
}
yield return null;
}
}
void ProcessWork()
{
while (true)
{
if (workToBeDone.Count == 0)
{
workerThreadResetEvent.Reset();
workerThreadResetEvent.WaitOne();
}
Job currentWork = null;
lock (workToBeDone)
{
currentWork = workToBeDone.Dequeue();
}
currentWork.Run();
lock (completedWork)
{
completedWork.Enqueue(currentWork);
}
}
}
}
}
It’s probably a bit daunting in one big block. The main idea behind it is the JobScheduler receives Jobs, which are simply a block of processing work to be done with a bit of wrapping structure. Then it works through them in a first in first out fashion, on a separate thread. Each job has an OnCompleted callback you can provide if you want to do anything on the main thread after the Job has completed (say pass the calculations to a Unity API). This example only uses 1 thread to process all tasks but it can be rearranged to do each task on a separate thread (though that would take more work).
Run() is on the main thread, and handles OnCompletion events. ProcessWork is on a separate thread and handles doing each Job in order and keeping track of which jobs to do next/mark jobs as completed.
You have 3 options to doing something at the end of a Job: Use the WaitHandle and pause the current thread until the work is completed (not common in Unity, because of the reliance on the main loop for Unity API stuff), poll the Job.IsCompletedproperty to know that the work was completed, or provide a callback delegate to do the finalization work.
Here is an example(I’ve chosen to use the OnCompleted Callback):
using namespace TheadUtils;
public class Curve : MonoBehaviour
{
struct MeshData
{
public Vector3[] verts;
public int[] tris;
}
public void Awake()
{
MeshData visualMeshData = new MeshData();
JobScheduler.Instance.AddJob(delegate { CreateMeshThreaded(ref visualMeshData); }, delegate(IAsyncResult result) { FinalizeMeshThreaded(visualMeshData); });
}
void CreateMeshThreaded(ref MeshData data)
{
//Fill in MeshData struct here (this is off the main thread, and ideal for multi-frame resource intensive work). This is cut out because it's a large example
}
void FinalizeMeshThreaded(MeshData data)
{
//This is on the main thread, and is safe to use Unity APIs, and is always called on the frame after the Job was completed
meshFilter.mesh.Clear();
meshFilter.mesh.vertices = data.verts;
meshFilter.mesh.triangles = data.tris;
}
}
}
This code is heavily truncated, but it shows a basic example of performing the mesh generation in a thread and then applying it to Unity back on the main thread once it’s completed. The ref keyword on the Creation function allows me to use a value-type struct and pass it into the threaded function, but still have a proper reference for the callback, rather than the normal behaviour.
The only elements I locked are A) The JobQueue only when Enqueueing and Dequeueing, and B) The Jobs WaitHandle because it’s only created if requested, and could cause issues if 2 threads request it at the same time.
Also the Exception object in Job is to make sure that any errors or exceptions are thrown on the main thread, which can help make them easier to catch and handle properly.
Also another nice feature with the WorkerThreadResetEvent, is that if the Job Queue is empty, it won’t do any work or check the queue until another job is added, meaning that if there is no work to be done, there is not processing done for that thread.
Ask any questions about it, As if you are unused to .NET threads there could be some strange stuff, but hopefully this helps you come up with a good system.
Edit: Added workaround for Editor not having Coroutines.