如何使用C#中的TPL任务将工作编组到主线程上而不会导致死锁?

时间:2013-11-11 22:22:37

标签: c# .net multithreading task-parallel-library

我正在编写一个消耗资源的库,无论出于什么原因,API的设计方式是在不同的线程上引发事件,但是必须在主线程上调用API。

假设我尝试使用的API被定义为(我将省略事件定义):

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

为了使用这个API,我在我的库中添加了一个名为Service(Yup,非常原始的名称)的服务,该服务将创建一个新任务(将在主线程上运行,因为我指定了一个已经创建的TaskScheduler SynchronizationContext)。

这是我的实施:

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);
  }

}

现在,如果我执行我的服务调用(在主线程上)并尝试在主线程上等待,则应用程序进入死锁状态,因为主线程将等待已计划在主线程上执行的任务主线。

如何在不阻塞整个过程的情况下将这些调用编组到主线程上?

在某些时候,我想在创建新任务之前执行主线程的检测,但我不想破解它。

对于任何感兴趣的人,我得到了一个here的代码和一个展示该问题的WPF应用程序。

在btw上,必须在.net framework 4.0上编写库

编辑! 我按照Scott Chamberlain提供的here

提供的建议解决了我的问题

3 个答案:

答案 0 :(得分:8)

  

因为主线程将等待任务

这是一个保证死锁。任务无法在主线程上执行,直到它处于空闲状态,运行调度程序循环(也称为消息循环)。正是该调度程序循环实现了使代码在特定线程上运行的魔力。然而,主线程不会空闲,它是“等待任务”。所以任务无法完成,因为主线程不会空闲,主线程无法进入空闲状态,因为任务无法完成。死锁城市。

必须重写代码,以便主线程不会等待。将等待调用后出现的任何代码移动到主线程上运行的另一个任务,就像ReallyLongCall()一样。

请注意,您似乎没有任何使用任务的里程数,您的代码段表明重要的代码 none 在工作线程上运行。所以你不妨直接称它,也解决问题。

答案 1 :(得分:2)

来自您的示例程序:

  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是一个阻塞调用,你不能在主线程上执行阻塞调用,否则会导致死锁。您可以执行的操作(如果您使用的是Visual Studio 2012或更高版本)使用NuGet包Microsoft.Bcl.Async,它为.Net 4.0提供async/await支持。

添加软件包后,将代码更改为

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);
}

并且您的程序将不再死锁(它也不会在await TaskEx.WhenAll(list);之后执行任何代码,但这是因为此代码在关闭过程中正在运行,当您await时它会让关闭继续在处理上,如果它被放置在其他地方,就像点击事件,你会看到更多的正常行为。)


另一种选择是拥有第二个“主线程”并将工作分配给它。通常当必须在“”线程上运行某些内容实际上是说它们需要在“一个STA Windows消息上运行,该对象最初是在”线程上创建的。以下是如何使用它的示例(取自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 :(得分:0)

@HansPassant是正确的;通过阻止调度程序线程等待任务,可以防止任务被执行。您可能做的最简单的更改是将Task.WaitAll(list)替换为:

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

...然后将调用WaitAll()之后的任何代码移动到延续中。请记住检查任务结果并对可能发生的任何异常做出适当的响应。

但除非使用在您的示例代码中不明显的任务有一些切实的好处,否则我会听从Hans的建议并简单地放弃任务以支持同步调用。