.Net为什么Threading.Task.Task仍会阻止我的UI?

时间:2010-07-30 00:04:59

标签: multithreading .net-4.0

我正在编写一个可以控制测量设备定位的应用程序。由于涉及硬件,我需要在运行电动机时不断轮询当前位置值。我正在尝试构建对此负责的类,以便它在后台线程上进行轮询,并在达到所需位置时引发事件。这个想法是轮询不会阻止应用程序的其余部分或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);
    }
}

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

由于

2 个答案:

答案 0 :(得分:13)

因为你的继续任务(通过ContinueWith)指定了一个TaskScheduler TPL使用它来执行所有其他任务,而不管你是否实际指定了它。换句话说,默认情况下,来自Task.Factory.StartNew中指定的Action委托的ContinueWith来电将自动使用指定的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);
}

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

更好的解决方案可能是使用专用线程进行硬件轮询。

答案 1 :(得分:12)

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

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