How to configure work on the main thread using TPL tasks in C # without causing a dead end?

I am writing a library that consumes a resource, and for some reason the API was designed in such a way that events will occur in different threads, but API calls must be made in the main thread.

Let's say the API I'm trying to use is defined as (I will omit event definitions):

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

To use this API, I added a service to my library called Service (Yup, a very original name) that will create a new task (which will work in the main thread, as I specify the TaskScheduler that was created from 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 make a call to my service (in the main thread) and try to wait for the main thread, the application comes to a standstill, as the main thread will wait for the scheduled tasks to be completed on the main thread.

How to redirect these calls to the main thread without blocking the whole process?

At some point, I thought about doing a main thread discovery before creating a new task, but I don't want to hack this.

For everyone who is interested, I got a gist here with code and a WPF application that shows the problem.

In btw, the library should be written in .net framework 4.0

Change! I solved my problem following the advice of Scott Chamberlain as provided here

+7
multithreading c # task-parallel-library
source share
3 answers

From the sample program:

  private void HandleClosed(object sender, EventArgs e) { var list = new[] { _service.ExecuteAsync("first task"), _service.ExecuteAsync("second task"), _service.ExecuteAsync("third task") }; //uncommenting this line blocks all three previous activities as expected //as it drives the current main thread to wait for other tasks waiting to be executed by the main thread. //Task.WaitAll(list); } 

Task.WaitAll is a blocking call, you cannot make blocking calls in the main thread or you will create deadlocks. What you can do (if you are using Visual Studio 2012 or newer), then use the Microsoft.Bcl.Async NuGet package, which provides async/await support for .Net 4.0.

After adding the package, change the code to

 private async void HandleClosed(object sender, EventArgs e) { var list = new[] { _service.ExecuteAsync("first task"), _service.ExecuteAsync("second task"), _service.ExecuteAsync("third task") }; //uncommenting this line blocks all three previous activities as expected //as it drives the current main thread to wait for other tasks waiting to be executed by the main thread. await TaskEx.WhenAll(list); } 

and your program will stop going to a dead end (it also does not execute code after await TaskEx.WhenAll(list); but this is because this code works during the shutdown process, and when you are await , it allows you to exit during processing if it was placed in another place, like the click event, you will see more normal behavior).


Another option is the second "Main thread" and sending this work. Often, when something needs to be launched in the "main" thread, it is actually said that they need to run the "STA Windows Message, downloaded that the object was originally created on the" thread. Here is an example of how to it (taken from here )

 private void runBrowserThread(Uri url) { var th = new Thread(() => { var br = new WebBrowser(); br.DocumentCompleted += browser_DocumentCompleted; br.Navigate(url); Application.Run(); }); th.SetApartmentState(ApartmentState.STA); th.Start(); } void browser_DocumentCompleted(object sender, WebBrowserDocumentCompletedEventArgs e) { var br = sender as WebBrowser; if (br.Url == e.Url) { Console.WriteLine("Natigated to {0}", e.Url); Application.ExitThread(); // Stops the thread } } 
+2
source share

since the main thread will wait for tasks

This is a guaranteed dead end. A task cannot be executed in the main thread until it becomes inactive, starting a dispatcher loop (for example, pumping a message loop). This is a dispatcher loop that implements the magic of getting code to work in a specific thread. The main thread, however, will not be idle; it is "waiting for tasks." Thus, the task cannot be completed because the main thread will not be idle, the main thread cannot be idle because the task will not be completed. Dead End City.

You must rewrite the code so that your main thread does not wait. Move any code that appears after the wait call to another task that runs in the main thread, just like ReallyLongCall ().

Please note that you do not seem to get any mileage from using tasks, your snippet suggests that none of the codes that matters does not work in the workflow. So you can also call it directly, solves the problem.

+8
source share

@HansPassant is true; By blocking the dispatcher thread to wait for tasks, you prevent tasks from completing. The simplest change you could probably make would be to replace Task.WaitAll(list) with:

 _taskFactory.ContinueWhenAll( list, tasks => { /* resume here */ }); 

... and then move any code that followed the WaitAll() call to continue. Be sure to check the results of the task and respond appropriately to any exceptions that may have occurred.

But if there is no tangible benefit to using Tasks that are not explicit in your code example, I would advise Hans and just refuse the Tasks in favor of synchronous calls.

0
source share

All Articles