Startnew Is Dangerous

Recently I was developing an ActionQueue. It supports serial asynchronous execution of Sytem.Action<ICancellationToken> delegates. I ran into a problem similar to this: Memory leak in recursive continuation Tasks I have a state object (e.g. of type Tuple<,,,>) and I pass this to Task.Factory.StartNew and if I run a memory profiler on my application I see that those state objects are not GC’ed. (Go is of type System.Action<object>).

// state is never released:
var task = Task.Factory.StartNew(Go, state);
 
// same problem:
var task = new Task(Go, state, TaskCreationOptions.DenyChildAttach | TaskCreationOptions.HideScheduler);
task.Start(TaskScheduler.Default);

My implementation had the assumption that “task” would represent the entire “Go” method but it represents only the first (synchronous) part if “Go” points to an asynchronous method (one that uses the async keyword). Some additional insight can be found here: http://blog.stephencleary.com/2013/08/startnew-is-dangerous.html The reason for the memory leak is that my implementation spawns continuations whose AsyncState recursively contains information about the previous Task. (In a memory profiler you will probably see its backing field “m_State”) Since you can’t reset the AsyncState property of Task (it is readonly and Dispose() does not clear it) all your Tasks and all their states will stay alive. This will not happen if you capture the state variable by passing a lambda expression to Task.Run:

// this works:
var task = Task.Run(() => Go(state));

So Task.Factory.StartNew is not only dangerous because of the reasons stated by Stephen Cleary, but also because it tempts you to use Task.AsyncState which also can be dangerous.

Leave a Reply

Your email address will not be published. Required fields are marked *