Please note, this article refers to an old version of the TaskRunner which is not supported anymore. The latest version is here.

Every now and then it is necessary to run a series of tasks in a given order, for example to guarantee that some data has been downloaded before to continue the execution of the application. C# 4.0 offers powerful tools in order to perform these operations, like the Task Parallel Library, which seems very straightforward to use.

However Unity3D supports c# 3.5 only and beside the System.Threading routines, there are not easy way to accomplish the task.

All the Unity3D programmers know that Unity 3D exploits nicely the yield instruction to simulate multi-threading using coroutines (which are, of course, single threaded), but what are they exactly?

As explained in many tutorials, once an iterator block is met, the compiler creates a special switch case function that allows even complicated methods to be time-sliced; Something very similar to the various pseudo threading framework implementations that exist for single-threading environments (like in actionscript).

Time sliced techniques have been used for ages in the game industry, mostly to spread complicated routine execution over several rendering frames. So nothing new here, except the fact that c# is so smart to be able to create very sophisticated time-sliced routine on its own, so that the code stays very readable. Albeit, using yield for the execution of several tasks could get awkward.

This is why I decided to create a library that could accept, combine and run async tasks in parallel and serial. An example of the way it can be used is given by the following method:

public void RunSerialTasksExecutedInParallel ()
{
	SerialTasks serialTasks1 = new SerialTasks ();
	SerialTasks serialTasks2 = new SerialTasks ();

	ParallelTasks  parallelTasks1 = new ParallelTasks ();

	ITask task1 = new Task ();
	ITask task2 = new Task ();

	IEnumerable iterable1 = new Enumerable ();
	IEnumerable iterable2 = new Enumerable ();

	serialTasks1.Add (iterable1);
	serialTasks1.Add (iterable2);
	serialTasks1.onComplete += () => { Debug.Log("First bunch of serial tasks completed"); };

	serialTasks2.Add (task1);
	serialTasks2.Add (task2);
	serialTasks2.onComplete += () => { Debug.Log("Second bunch of serial tasks completed"); };

	parallelTasks1.Add (serialTasks1);
	parallelTasks1.Add (serialTasks2);
	parallelTasks1.onComplete += () => { Debug.Log("All Done"); };

	TaskRunner.Instance.Run(parallelTasks1);
}

As you can see, the code is pretty straightforward, there are just some notes to take in consideration:

  • TaskRunner is a Monobehaviour that exploits the StartRoutine function to register the task(list) to run. You must have one enabled in order to start the execution.
  • SerialTasks and ParallelTasks are two classes of the framework, they are quite self-explanatory
  • SerialTasks and ParallelTasks can execute both standard IEnumerable and ITask objects
  • ITask is a special interface added in order to manage special cases, but it is not mandatory to use.

The following is a naive example (for testing purposes) of an ITask object:

class Task : ITask
{
	public event TasksComplete onComplete;

	public bool isDone { get; private set; }

	public void Execute ()
	{
		isDone = false;

		//wait synchronously for 1 second
		//usually it is an async operation
		IEnumerator e = WaitHalfSecond ();
		while (e.MoveNext());

		isDone = true;

		if (onComplete != null)
			onComplete ();
	}

	private IEnumerator WaitHalfSecond ()
	{
		float time = Time.realtimeSinceStartup;

		while (Time.realtimeSinceStartup - time < 0.5)
			yield return null;
	}
}

So what is the trick that let the sequential and parallel tasks be mixed together? The idea behind this code is to create a simple stack structure that recognize when a new IEnumerable (or IEnumerator) object is returned from a yield return. Nothing harder than:

foreach (IEnumerator enumerator in registeredEnumerators)
{	//create a stack for each task to run
	Stack stack = new Stack();
	//push the first task
	stack.Push(enumerator);
	//until the stack is not empty
	while (stack.Count > 0)
	{	//get the first task without removing it
		IEnumerator ce = stack.Peek();
		//iterate over it
		if (ce.MoveNext() == false)	//is it done?
		{
			stack.Pop(); //now it can be popped
		}
		else //ok the iteration is not over
		if (ce.Current != null && ce.Current != ce) //the interesting part
		{	//is the current task actually another Enumerator (or IEnumerable)?
			if (ce.Current is IEnumerable)
				stack.Push(((IEnumerable)ce.Current).GetEnumerator()); //push it, this will be next task to be executed
			else
			if (ce.Current is IEnumerator)
				stack.Push(ce.Current as IEnumerator);  //push it, this will be next task to be executed
		}

		yield return null;
	}
}

In this way I can run a set of sequential tasks from parallel tasks and viceversa, but as you probably got the code is not limited just to these two cases, it actually can handle all the possible combinations deriving from the use of IEnumerable functions or objects.

I wish to thank my friend Amedeo Margarese who gave me the idea to write this code and Francesco Carucci who helped to write the unit tests for NunitLite

0 0 votes
Article Rating
Subscribe
Notify of
guest

10 Comments
Most Voted
Newest Oldest
Inline Feedbacks
View all comments
Caue C M Rego (@cawas)
Caue C M Rego (@cawas)
11 years ago

Please, I wish to see more examples. 😛

I’m trying to figure out how to use this… And I’m stuck in 2 points.

First, very basic, how should I implement the actual task?

Second, how can I serialize it within the task?

Thanks in advance, and great job!

Caue C M Rego (@cawas)
Caue C M Rego (@cawas)
11 years ago

Yes, that I can see in the current examples… But then, how would I implement the `IEnumerable`? 😀

I’ll eventually figure it out and I’m actually having some progress with good old trial and error, but since you asked about needing more examples I thought those would be nice additions.

Also, we can use it just like IEnumerators, can’t we? Make RunSerialTasksExecutedInParallel a IEnumerator (rather than void) and insert a `while (! parallelTasks1.isDone) yield return null;` in the end. Is there any problems in doing this?

Danko
Danko
11 years ago

If you are interested, eDriven has the TaskQueue class for synchronizing subsequent tasks (i.e. waiting for each task to end before handling the consequent one): https://github.com/dkozar/eDriven/blob/master/eDriven.Core/Tasks/TaskQueue.cs (and does it single threaded)

(seems we share the interest in this area :))

Vili Volčini
Vili Volčini
10 years ago

Hello, one question!

Are those parallel Tasks multi-threaded? Thanks for this script, I hope it will be usefull 🙂

I need to process multiple meshes at startup and I really want to use multi-threading for this.

Oleg
Oleg
9 years ago

courutines neither parallel nor async. They are synchronous, but concurrent. Learn the difference.

Michael
Michael
9 years ago

Hello Sebastiano ! Your Asyncronius Tasks is very good and helpfull ! SerialTaskCollection codestyle allow keep all sequence in one function. That make async logic clear. But some times we need to pass result from one async function to next, without using this result elsewhere. So I write args helper .. ( may be not elegant but do job ). Take a look. May be it will be usefull for You and Your library. http://www.codeshare.io/jufP5 Usage : public void Run() { mutual = new MutableArgs(); SerialTaskCollection st = new SerialTaskCollection(); st.Add(Print(1, mutual.Out)); st.Add(AsyncWaitAndCreateURL(mutual.In, mutual.Out)); st.Add(WWWTest(mutual.In)); StartCoroutine(st.GetEnumerator()); } IEnumerator Print(int i,… Read more »

Sebastiano Mandalà
Admin
Sebastiano Mandalà
9 years ago
Reply to  Michael

Hi Michael, I do the same, but I don’t think you need to use Func (unless I misunderstood what you want to solve), I solved the same problem using simple references. Assuming we are talking about a single thread scenario, we can safely pass references as parameters of IEnumerator functions and they will work as well. I also implemented the “token” parameter to solve the same problem when ITask are used instead. I once wrote an elegant solution for a quite complicated scenario. I needed the serial tasks to branch according a parameter. It worked more or less like this:… Read more »