【问题标题】:.Net why does Threading.Task.Task still block my UI?.Net 为什么 Threading.Task.Task 仍然会阻止我的 UI?
【发布时间】:2015-06-11 12:20:26
【问题描述】:

我目前正在编写一个控制测量设备定位的应用程序。由于涉及的硬件,我需要在运行电动机时不断地轮询当前位置值。我正在尝试构建负责此的类,以便它在后台线程上进行轮询,并在达到所需位置时引发事件。这个想法是轮询不会阻塞应用程序或 GUI 的其余部分。我想使用新的 Threading.Task.Task 类来为我处理所有后台线程管道。

我还没有硬件,但是已经构建了一个测试存根来模拟这种行为。但是当我像这样运行应用程序时,GUI 仍然会阻塞。请参阅下面代码的简化示例(不完整且未使用单独的类进行设备控制)。代码有一系列测量步骤,应用程序必须定位并测量每个步骤。

public partial class MeasurementForm: Form
{
    private MeasurementStepsGenerator msg = new MeasurementsStepGenerator();
    private IEnumerator<MeasurementStep> steps;

    // actually through events from device control class
    private void MeasurementStarted()
    {
        // update GUI
    }

    // actually through events from device control class
    private void MeasurementFinished()
    {
        // store measurement data
        // update GUI
        BeginNextMeasurementStep();
    }

    private void MeasurementForm_Shown(object sender, EventArgs e)
    {
        steps = msg.GenerateSteps().GetEnumerator();
        BeginNextMeasurementStep();
    }        
    ...
    ...

    private void BeginNextMeasurementStep()
    {
        steps.MoveNext();
        if (steps.Current != null)  
        { 
            MeasurementStarted();
            MeasureAtPosition(steps.Current.Position); 
        }
        else    
        { 
            // finished, update GUI
        }
    }

    // stub method for device control (actually in seperate class)
    public void MeasureAtPosition(decimal position)
    {
        // simulate polling
        var context = TaskScheduler.FromCurrentSynchronizationContext();
        Task task = Task.Factory.StartNew(() =>
        {
            Thread.Sleep(sleepTime);
        }, TaskCreationOptions.LongRunning)
        .ContinueWith(_ =>
        {
            MeasurementFinished();
        }, context);
    }
}

我希望任务在后台线程上运行 Thread.Sleep 命令,以便控制立即返回到主线程并且 GUI 不会被阻塞。但是 GUI 仍然被阻止。就像任务在主线程上运行一样。关于我在这里做错了什么有什么想法吗?

谢谢

【问题讨论】:

  • 我已经用一些繁重的算法而不是 Thread.Sleep 对此进行了测试,然后一切正常。 Thread.Sleep 是否总是在主线程上工作?我希望它可以在当前线程中工作。
  • 我的意思是我希望它阻塞任务线程,而不是 UI 线程。
  • 我发现无论我使用繁重的算法还是 Thread.Sleep(),使用 ContinueWith() 串联运行任务都会阻塞 UI。当您并行运行任务时,对于繁重的算法它不会这样做,但我不能这样做,我的任务必须按顺序运行。
  • 我对 .NET 一无所知。但是,我已经制作了许多 PC 嵌入式系统接口,我知道的一件事是 PC 操作系统不是实时的。任何需要及时更新的事情都需要在您可以更好地控制正在发生的事情的环境中完成,这不是 .NET 与 Thread.Sleep。您说有电机 - 如果您的代码在一段时间内没有响应,这是否会对人或物体造成安全隐患?
  • 无法重新创建。一些问题:您确定它是因为 Thread.Sleep (或您在该任务中拥有的任何东西)而挂起的吗?如果你打破了,你会在那里得到一个断点吗?打开 Debug > Windows > Threads 是否可以看到挂起的线程?

标签: multithreading .net-4.0


【解决方案1】:

因为您的延续任务(通过ContinueWith)指定了TaskScheduler,所以无论您是否实际指定了它,TPL 都将其用于在调用堆栈的下方开始的所有其他任务。换句话说,来自ContinueWith 中指定的Action 委托对Task.Factory.StartNew 的调用将默认自动使用指定的TaskScheduler

我已经修改了您的代码,以帮助您更好地了解正在发生的事情。

private void BeginOperation()
{
    System.Diagnostics.Trace.WriteLine("BeginOperation-top " + Thread.CurrentThread.ManagedThreadId);
    var context = TaskScheduler.FromCurrentSynchronizationContext();
    Task task = Task.Factory.StartNew(() =>
    {
        System.Diagnostics.Trace.WriteLine("  BeginOperation-StartNew-top " + Thread.CurrentThread.ManagedThreadId);
        Thread.Sleep(5000);
        System.Diagnostics.Trace.WriteLine("  BeginOperation-StartNew-bottom " + Thread.CurrentThread.ManagedThreadId);
    }, TaskCreationOptions.LongRunning)
    .ContinueWith(_ =>
    {
        System.Diagnostics.Trace.WriteLine("  BeginOperation-ContinueWith-top " + Thread.CurrentThread.ManagedThreadId);
        EndOperation();
        System.Diagnostics.Trace.WriteLine("  BeginOperation-ContinueWith-bottom " + Thread.CurrentThread.ManagedThreadId);
    }, context);
    System.Diagnostics.Trace.WriteLine("BeginOperation-bottom " + Thread.CurrentThread.ManagedThreadId);
}

private void EndOperation()
{
    System.Diagnostics.Trace.WriteLine("EndOperation-top " + Thread.CurrentThread.ManagedThreadId);
    BeginOperation();
    System.Diagnostics.Trace.WriteLine("EndOperation-bottom " + Thread.CurrentThread.ManagedThreadId);
}

我通过反射器检查了ContinueWith 中的代码,我可以确认它正在尝试发现调用者使用的执行上下文。是的,不管你信不信,尽管你有相反的直觉,但这正是它正在做的事情。

更好的解决方案可能是有一个专用线程来执行硬件轮询。

【讨论】:

  • 哇,布莱恩,你这个人!啊,我没有意识到我是这样递归的。将其标记为答案,因为您已经能够回答为什么它不起作用(这是问题所在)。找到一个解决方案会很好,但我已经放弃并以另一种方式做了,所以现在更感兴趣:-)
  • 我觉得不错!感谢您查看答案:) 编辑:显然我需要等待 8 小时才能奖励您赏金,呵呵。
  • @Daniel:老实说,我不太关心赏金。我希望有一种方法可以让您自己保留,但如果我正确阅读规则,系统无论如何都会将您停靠 250。对不起:(
  • 别担心,我关心赢得声誉的唯一原因就是在我需要一些广告来解决问题时不小心扔掉它。
【解决方案2】:

Brian Gideon 关于问题的原因是正确的 - 递归创建的任务开始使用它们当前的任务调度程序设置为指定主线程的 SynchronizationContextTaskScheduler。在主线程中运行它们显然不是你想要的。

您可以通过使用TaskFactory.StartNew 的重载之一来解决此问题,该重载接受任务调度程序并将其传递给TaskScheduler.Default

【讨论】:

  • 我认为你的回答有点中肯,而且仍然正确。问题在于 SynchronizationContextTaskScheduler。
  • 我遇到了在 UI 上下文上运行 LongRunning 任务的完全相同的问题,TaskScheduler.Default 修复了它。谢谢!
猜你喜欢
  • 1970-01-01
  • 2011-03-27
  • 2012-06-21
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-10-31
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多