【问题标题】:Thread Join() causes Task.RunSynchronously not to finishThread Join() 导致 Task.RunSynchronously 未完成
【发布时间】:2015-08-29 15:28:46
【问题描述】:

调用_thread.Join() 会导致GetConsumingEnumerable 循环卡在最后一个元素上。为什么会出现这种行为?

  public abstract class ActorBase : IDisposable
  {
    private readonly BlockingCollection<Task> _queue = new BlockingCollection<Task>(new ConcurrentQueue<Task>());
    private readonly Thread _thread;
    private bool _isDisposed;

    protected ActorBase()
    {
      _thread = new Thread(ProcessMessages);
      _thread.Start();
    }

    protected void QueueTask(Task task)
    {
      if (_isDisposed)
      {
        throw new Exception("Actor was disposed, cannot queue task.");
      }
      _queue.Add(task);
    }

    private void ProcessMessages()
    {
      foreach (var task in _queue.GetConsumingEnumerable())
      {
        task.RunSynchronously();
      }
    }

    public void Dispose()
    {
      _isDisposed = true;
      _queue.CompleteAdding();
      _thread.Join();
    }
  }

  public class SampleActor : ActorBase
  {
    private string GetThreadStatus()
    {
      Thread.Sleep(500);
      return string.Format("Running on thread {0}", Thread.CurrentThread.ManagedThreadId);
    }

    public async Task<string> GetThreadStatusAsync()
    {
      var task = new Task<string>(GetThreadStatus);
      QueueTask(task);
      return await task;
    }
  }

  class Program
  {
    public static async Task Run()
    {
      using (var sa = new SampleActor())
      {
        for (int i = 0; i < 3; i++)
        {
          Console.WriteLine(await sa.GetThreadStatusAsync());
        }
      }
    }

    public static void Main(string[] args)
    {
      Console.WriteLine("Main thread id {0}", Thread.CurrentThread.ManagedThreadId);
      var task = Task.Run(async ()=> { await Run(); });
      task.Wait();
    }
  }

这种方法的背景是我需要确保所有操作都在一个 OS 线程上执行,这将允许应用程序的一部分使用与主线程不同的凭据。

【问题讨论】:

  • 我无法向您解释为什么会发生这种情况,但它锁定的原因是GetThreadStatusAsync() 中的return await task; 在调用RunSynchronously() 的同一线程上完成了它的等待。当GetThreadStatusAsync() 返回并退出Run 中的using 块时,这会导致Dispose 从保存的_thread 线程内部运行,现在您正在从要加入的线程内部调用Join() ,僵局!

标签: c# multithreading async-await task-parallel-library threadpool


【解决方案1】:

async-await 与延续一起使用。为了提高效率并减少调度,这些延续通常在完成前一个任务的同一线程上运行。

这意味着在您的情况下,您的特殊线程不仅在运行任务,而且还在这些任务之后运行所有延续(for 循环本身)。您可以通过打印线程 id 来查看:

using (var sa = new SampleActor())
{
    for (int i = 0; i < 3; i++)
    {
        Console.WriteLine(await sa.GetThreadStatusAsync());
        Console.WriteLine("Continue on thread :" + Thread.CurrentThread.ManagedThreadId);
    }
}

for 循环完成并且SampleActor 被处置时,您从您尝试加入的同一线程调用Thread.Join,这样您就会陷入僵局。您的情况归结为:

public static void Main()
{
    Thread thread = null;
    thread = new Thread(() =>
    {
        Thread.Sleep(100);
        thread.Join();
        Console.WriteLine("joined");
    });
    thread.Start();
}

在 .Net 4.6 中,您可以使用 TaskCreationOptions.RunContinuationsAsynchronously 解决此问题,但在当前版本中,您可以指定默认的 TaskScheduler

public Task<string> GetThreadStatusAsync()
{
    var task = new Task<string>(GetThreadStatus);
    QueueTask(task);
    return task.ContinueWith(task1 => task1.GetAwaiter().GetResult(), TaskScheduler.Default);
}

【讨论】:

  • 这确实是框架中的设计错误。内置重入。如此肮脏。
  • @usr: 是的,当我first ran into this behavior a few years ago 时,我将它报告为 MSConnect 上的一个错误(这个错误已经神秘地消失了,就像我的大多数错误报告一样)。 IIRC,该决议是“按设计的”。这对 ASP.NET 应用程序来说是有意义的,但在其他情况下我还是有点不舒服。
【解决方案2】:

您可能很想进行简单的检查以查看您尝试访问Join 的线程是否为Thread.CurrentThread,但这是错误的。

此外,我认为整个方法 - 使用自定义的、不符合 TPL 的调度程序来调度和运行冷 Task 对象 - 是错误的。您应该使用对 TPL 友好的任务调度程序,类似于 Stephen Toub 的 StaTaskScheduler。或者为您的演员服务线程运行自定义SynchronizationContext(如Toub的AsyncPump)并使用TaskScheduler.FromCurrentSynchronizationContextTask.Factory.StartNew使用自定义调度程序安排任务(或者如果您必须处理冷任务,请使用Task.Start(TaskScheduler) )。

这样,您将完全控制任务及其延续的运行位置,以及task inlining

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-11-21
    • 2018-01-27
    • 1970-01-01
    • 2013-08-24
    • 2010-11-26
    • 2011-12-22
    相关资源
    最近更新 更多