Подтвердить что ты не робот

.Net Почему Threading.Task.Task все еще блокирует мой интерфейс?

В настоящее время я пишу приложение, которое будет контролировать позиционирование измерительного устройства. Из-за используемого оборудования мне нужно постоянно отслеживать текущее значение позиции при работе электродвигателя. Я пытаюсь построить класс, ответственный за это, чтобы он выполнял опрос в фоновом потоке и поднимет событие, когда будет достигнута желаемая позиция. Идея состоит в том, что опрос не будет блокировать остальную часть приложения или графический интерфейс. Я хотел использовать новый класс 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 не будет заблокирован. Но графический интерфейс по-прежнему блокируется. Это похоже на задачу, выполняемую по основному потоку. Любые идеи о том, что я здесь делаю неправильно?

Спасибо

4b9b3361

Ответ 1

Поскольку ваша задача продолжения (через 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);
}

Я просмотрел код в ContinueWith через Reflector, и я могу подтвердить, что он пытается обнаружить контекст выполнения, используемый вызывающим. Да, верьте или нет, и, несмотря на вашу естественную интуицию, наоборот, это именно то, что она делает.

Лучшим решением, вероятно, будет выделение выделенного потока для аппаратного опроса.

Ответ 2

Брайан Гидеон прав в отношении причины проблемы. Рекурсивно созданные задачи начинаются с текущего планировщика задач, установленного в SynchronizationContextTaskScheduler, который определяет основной поток. Выполнение их в основном потоке явно не то, что вы хотите.

Вы можете исправить это, используя одну из перегрузок для TaskFactory.StartNew, которая принимает планировщик задач и передает его TaskScheduler.Default.