15

I have seen numerous other questions similar to this but did not find my answer there.

My problem was that I was creating threads with the following flow:

private void btn_Click(object sender, EventArgs e)
{
    service.GetCount(
        (count, ex) =>
        {
            if (ex != null)
                return;

            for (int i = 0; i < count; i++)
            {
                service.Get(onItemReceived, i);
            }
        }
    );
}

public void GetCount(Action<int, Exception> callback)
{
    var callingThread = TaskScheduler.FromCurrentSynchronizationContext();

    Func<int> action = () =>
    {
        return client.GetCount(); // Synchronous method, could take a long time
    };

    Action<Task<int>> completeAction = (task) =>
    {
        Exception ex = (task.Exception != null) ? task.Exception.InnerException : task.Exception;

        if (callback != null)
            callback(task.Result, ex);
    };

    Task.Factory.StartNew(action).ContinueWith(completeAction, callingThread);
}

public void Get(Action<object, Exception> callback, int index)
{
    var callingThread = TaskScheduler.FromCurrentSynchronizationContext();

    Func<object> action = () =>
    {
        return client.Get(index); // Synchronous method, could take a long time
    };

    Action<Task<object>> completeAction = (task) =>
    {
        Exception ex = (task.Exception != null) ? task.Exception.InnerException : task.Exception;

        if (callback != null)
            callback(task.Result, ex);
    };

    Task.Factory.StartNew(action).ContinueWith(completeAction, callingThread);
}

In this way, each of my service's asynchronous methods would callback the thread they were originally called on (generally the UI thread). So I am simulating how the await/async keywords work (I cannot use .NET 4.5).

The problem with this pattern is that I get inexplicably locked to the UI thread after the first call to "ContinueWith". So in this case if I attempt to spawn 5 threads to each process a synchronous function Get, they will execute 1 by 1 instead of in parallel and they will block the UI thread while doing so, even if I try specifying TaskCreationOptions.LongRunning.

This never happens with my first call to Task.Factory.StartNew, it only happens to subsequent calls from within the first callback.

Trevor Elliott
  • 11,292
  • 11
  • 63
  • 102
  • Possible duplicate of [How to guarantee a new thread is created when using the Task.StartNew method](https://stackoverflow.com/questions/8039936/how-to-guarantee-a-new-thread-is-created-when-using-the-task-startnew-method) – Giulio Caccin Sep 05 '17 at 12:53

1 Answers1

28

In order to force the launch of a new thread, you should specify TaskScheduler.Default in the call to Task.Factory.StartNew as follows:

Task.Factory.StartNew(action,
                      CancellationToken.None,
                      TaskCreationOptions.None,
                      TaskScheduler.Default).ContinueWith(completeAction);

In my testing you do not need to specify TaskCreationOptions.LongRunning in order to force a background thread, though it shouldn't hurt.

Trevor Elliott
  • 11,292
  • 11
  • 63
  • 102
  • 2
    `LongRunning` forces the creation of an entirely new *non-thread pool thread*. – Servy Apr 11 '13 at 19:38
  • 4
    @Servy - it doesn't force anything, it's a _hint_ for the scheduler. – H H Apr 11 '13 at 19:43
  • 3
    @HenkHolterman In theory, yes, that's correct. In practice, in the current implementation, it results in a new thread being created. While I agree it might be best to assume it's possible that it won't, you should assume that marking a task as long running when it's really not will consume additional resources. – Servy Apr 11 '13 at 19:45
  • 3
    In my scenario listed in the question above, TaskCreationOptions.LongRunning does NOT force a new background thread. Only using TaskScheduler.Default did. – Trevor Elliott Apr 11 '13 at 19:50
  • @Servy The reason for raising this issue is that if anyone *depends on* this implementation detail (that the following task will execute on a different thread then the current one), it is possible to cause a deadlock (aptly known as the "self-deadlock" or "self-waiting deadlock") if the assumption is violated. – rwong Jan 22 '15 at 00:49
  • @rwong And the fact that changing the implementation of this method would cause such a deadlock is exactly why MS wouldn't make such a change. – Servy Jan 22 '15 at 00:58