.Net, почему Threading.Task.Task по-прежнему блокирует мой пользовательский интерфейс? - PullRequest
16 голосов
/ 30 июля 2010

В настоящее время я пишу приложение, которое будет контролировать положение измерительного устройства. Из-за аппаратного обеспечения мне нужно постоянно запрашивать текущее значение положения при работе электродвигателя. Я пытаюсь построить класс, ответственный за это, чтобы он выполнял опрос в фоновом потоке и вызывал событие, когда желаемая позиция достигнута. Идея состоит в том, что опрос не будет блокировать остальную часть приложения или графический интерфейс. Я хотел использовать новый класс Threading.Task.Task, чтобы обрабатывать все фоновые потоки для меня.

У меня еще нет аппаратного обеспечения, но я создал тестовую заглушку для имитации этого поведения. Но когда я запускаю такое приложение, графический интерфейс по-прежнему блокируется. См. Упрощенный пример кода ниже (не завершенный и не использующий отдельный класс для управления устройством). Код имеет последовательность шагов измерения, приложение должно позиционировать, а затем измерять для каждого шага.

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

Спасибо

Ответы [ 2 ]

13 голосов
/ 28 октября 2010

Поскольку ваша задача продолжения (через 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 и могу подтвердить, что он пытается обнаружить контекст выполнения, используемый вызывающей стороной. Да, верьте или нет, и, несмотря на вашу естественную интуицию, это именно то, что он делает.

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

12 голосов
/ 28 октября 2010

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

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...