Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to marshall work onto the main thread using TPL tasks in C# without causing a deadlock?

I am writing a library that is consuming a resource and for whatever reason the API was designed in a way that events will be raised on different threads but calls of the API has to be done on the main thread.

Let's say the API that I am trying to consume is defined as (I am going to omit event definitions):

public sealed class DodgyService
{
    public void MethodThatHasToBeCalledOnTheMainThread() { ... }
}

To consume this API I have added a service on my library called Service (Yup, very original name) that will create a new task (that will run on the main thread as I am specifying a TaskScheduler that has been created from the SynchronizationContext).

Here is my implementation:

public class Service
{
  private readonly TaskFactory _taskFactory;
  private readonly TaskScheduler _mainThreadScheduler;

  public Service(TaskFactory taskFactory, TaskScheduler mainThreadScheduler)
  {
      _taskFactory = taskFactory;
      _mainThreadScheduler = mainThreadScheduler;
  }

  // Assume this method can be called from any thread.
  // In this sample is called by the main thread but most of the time
  // the caller will be running on a background thread.
  public Task ExecuteAsync(string taskName)
  {
      return _taskFactory.StartNew(
          () => ReallyLongCallThatForWhateverStupidReasonHasToBeCalledOnMainThread(taskName),
          new CancellationToken(false), TaskCreationOptions.None, _mainThreadScheduler)
          .ContinueWith(task => Trace.TraceInformation("ExecuteAsync has completed on \"{0}\"...", taskName));
  }

  private void ReallyLongCallThatForWhateverStupidReasonHasToBeCalledOnMainThread(string taskName)
  {
      Trace.TraceInformation("Starting \"{0}\" really long call...", taskName);
      new DodgyService().MethodThatHasToBeCalledOnTheMainThread();
      Trace.TraceInformation("Finished \"{0}\" really long call...", taskName);
  }

}

Now, if I perform the call of my service (on the main thread) and try to wait on the main thread the application enters a deadlock as the main thread will be waiting for the tasks that has been scheduled to execute on the main thread.

How do I marshall these calls onto the main thread without blocking the entire process?

At some point I thought on performing the detection of the main thread before creating the new task but I don't want to hack this.

For anybody interested, I got a gist here with the code and a WPF app that exhibits the issue.

On btw, the library has to be written on .net framework 4.0

Edit! I solved my issue following the advice provided by Scott Chamberlain as provided here

like image 440
rodrigoelp Avatar asked Nov 11 '13 22:11

rodrigoelp


People also ask

What is Task Parallel Library (TPL)?

Besides Threads, EAP and APM with .Net 4.0 yet another way for building asynchronous applications was introduced: the Task Parallel Library or TPL for short. A major goal of this API was the unification of the async api with the abstraction of a Task.

How do I run a task in a thread pool?

You can start running a Task using Task.Run (Action action). This will queue up the Task on the thread pool, which will run in the background on a different thread. The thread pool takes a queue of tasks, and assigns them to CPU threads for processing.

How to return a result from a thread in a task?

A Task represents some asynchronous operation and is part of the Task Parallel Library, a set of APIs for running tasks asynchronously and in parallel. The task can return a result. There is no direct mechanism to return the result from a thread. Task supports cancellation through the use of cancellation tokens.

What is the difference between a task and a thread?

Fundamentally, it boils down to a “ task” which is equivalent to a thread except that it is more lightweight and comes without the overhead of creating an OS thread. In other words, a task is an easier way to execute something asynchronously and in parallel compare to a thread.


1 Answers

as the main thread will be waiting for the tasks

That's a guaranteed deadlock. A task cannot execute on the main thread until it is idle, running the dispatcher loop (aka pumping the message loop). It is that dispatcher loop that implements the magic of getting code to run on a specific thread. The main thread however won't be idle, it is "waiting for the tasks". So the task cannot complete because the main thread won't go idle, the main thread cannot go idle because the task won't complete. Deadlock city.

You must rewrite the code so your main thread won't wait. Move whatever code that appears after the wait call to another task that runs on the main thread, just like that ReallyLongCall().

Do note that you don't seem to get any mileage at all from using tasks, your snippet suggests that none of the code that matters runs on a worker thread. So you might as well call it directly, solves the problem as well.

like image 141
Hans Passant Avatar answered Sep 30 '22 13:09

Hans Passant