I was just integrating a new task-graph system for a C# machine control system when my tests started to go red. Note that the tasks I refer to are not the same as the C# Task
implementation, but the broader concept. Task-graphs are well known to be DAGs, because otherwise the tasks cannot be finished. The general algorithm to execute a task-graph like this is called topological sorting, and it goes like this:
- Find the number of dependencies (incoming edges) for each task
- Find the tasks that have zero dependencies and start them
- For any finished tasks, decrement the follow-up tasks dependency count by one and start them if they reach zero.
The graph that was failed looked like the one below. Task A was immediately followed by a task B that was followed by a few more tasks.

I quickly figured out that the reason that the tests were failing was that node B was executed twice. Looking at the call-stack for both executions, I could see that the first time B was executed was when A was completed. This is correct as per step 3 in the algorithm. However, the second time it was started was directly from the initial Run
method that does the work from step 2: Starting the initial tasks that are not being started recursively. I was definitely not calling Run
twice, so how did that happen?
public void Run()
{
var ready = tasks
.Where(x => x.DependencyCount == 0);
StartGroup(ready);
}
Can you see it? It is important to note that many of the tasks in this graph are asynchronous. Their completion is triggered by an IObserver
, a C# Task
completing or some other event. When the event is processed, StartGroup
is used to start all tasks that have no more dependencies. However, A was no such task, it was synchronous, so the StartGroup({B})
call happened while Run
was still on the stack.
Now what happened was that when A (instantly!) completed, it set the DependencyCount
of B to 0. Since ready in the code snippet is lazily evaluated from within StartGroup
, the ‘contents’ actually change while StartGroup
is running.
The fix was adding a .ToList
after the .Where
, a unit test that checked that this specifically would not happen again, and a mental note that lazy evaluation can be deceiving.