Отменить все oop в задаче извне - PullRequest
1 голос
/ 26 мая 2020

В WinForm мне нужно постоянно читать данные из источника (в моем случае USB-устройство) и отображать данные в ярлыках. Чтение должно начинаться по команде (button_click) и останавливаться при нажатии другой кнопки или в методе form_closing. Тем временем я обнаружил, что для этого мне нужно использовать Task.Factory, поскольку я могу создать там CancellationToken. Вот мой (примерный) код:

public partial class Form1 : Form
{
    CancellationTokenSource m_CancellationSource;
    Task m_USBReaderTask;

    public Form1()
    {
        InitializeComponent();
    }

    private void button1_Click(object sender, EventArgs e)
    {
        m_CancellationSource = new CancellationTokenSource();
        m_USBReaderTask = Task.Factory.StartNew(() => doAsync(m_CancellationSource.Token), m_CancellationSource.Token);
    }

    private void doAsync(CancellationToken ct)
    {
        InitUSBReader();
        while (!ct.IsCancellationRequested)
        {
            int[] data=ReadUSB();
            this.Invoke((MethodInvoker)delegate
            {
                lbOut1.Text = data[0].ToString();
                lbOut2.Text = data[1].ToString();
                lbOut3.Text = data[2].ToString();
                //... and so on...
            });
        }
        CleanupUSBReader(); //this is never happening
    }

    private void Form1_FormClosing(object sender, FormClosingEventArgs e)
    {
        if (null != m_CancellationSource)
        {
            m_CancellationSource.Cancel();
            m_USBReaderTask.Wait(); // this always hangs.
        }
    }

}

Очевидно, я столкнулся с двумя проблемами:

  1. Когда установлен CancellationToken, задача прерывается, но мне нужно убирайся, я просто хочу закончить 'while' l oop. (или он трескает sh и сообщение об ошибке отсутствует?)
  2. В событии FormClosing мне нужно подождать, пока очистка не закончится, но она блокируется бесконечно.

Помимо двух моих проблем, мой подход вообще верен или есть более элегантный способ достижения моих целей? Спасибо

Ответы [ 2 ]

1 голос
/ 26 мая 2020

По поводу вашего первого номера. Когда токен отменяется, действие While-l oop завершается, и следует запускать метод CleanupUSBReader(). Предполагается, что ReadUSB возвращается регулярно, в противном случае вам понадобится способ отменить чтение. Если вы отменяете задачу только при закрытии формы, возможно, проблема в тупике, см. Второй абзац. Если ReadUSB возвращается, вы не блокируете и все еще не достигаете метода очистки, должна быть какая-то другая проблема, например, где-то исключение.

Что касается вашей второй проблемы. Проблема в том, что вы вызываете this.Invoke, это синхронно, т.е. он запускает код в основном потоке и ждет его завершения. Поэтому, когда форма закрывается, основной поток запрашивает отмену задачи и ожидает ее завершения, но задача ожидает, пока основной поток обновит пользовательский интерфейс. Это приводит к тупику классификации. Одним из решений должно быть использование this.BeginInvoke, поскольку при этом основной поток запрашивает обновление пользовательского интерфейса, но не ожидает результата. Подробнее см. Invoke vs BeginInvoke

Существует общая рекомендация избегать использования task.Wait(), так как это очень легко приводит к таким взаимоблокировкам. Было бы неплохо не ждать выполнения задачи, если форма закрывается. Или отменить закрытие, await задачу, и закрыть форму после ожидания ..

0 голосов
/ 26 мая 2020

Вот два способа рефакторинга кода, чтобы избежать использования уродливой техники this.Invoke((MethodInvoker)delegate. Если USB-считыватель имеет свободный поток, вы можете вызывать каждый из его методов в разных ThreadPool потоках, например:

async Task LoopAsync(CancellationToken ct)
{
    await Task.Run(() => InitUSBReader(), ct);
    while (!ct.IsCancellationRequested)
    {
        int[] data = await Task.Run(() => ReadUSB(), ct);
        lbOut1.Text = data[0].ToString();
        lbOut2.Text = data[1].ToString();
        lbOut3.Text = data[2].ToString();
        await Task.Delay(100);
    }
    await Task.Run(() => CleanupUSBReader(), ct);
}

Но если USB-считыватель требует привязки потоков, то описанный выше метод не будет работать . Чтобы заставить его работать в одном потоке, вы можете использовать описанную ниже технику, которая также предлагает преимущество отделения кода пользовательского интерфейса от кода чтения USB:

Task LoopAsync(IProgress<int[]> progress, CancellationToken ct)
{
    return Task.Factory.StartNew(() =>
    {
        InitUSBReader();
        while (!ct.IsCancellationRequested)
        {
            int[] data = ReadUSB();
            progress.Report(data);
            Thread.Sleep(100);
        }
        CleanupUSBReader();
    }, TaskCreationOptions.LongRunning);
}

... и запускать задачу, подобную этой :

m_USBReaderTask = LoopAsync(new Progress<int[]>(data =>
{
    lbOut1.Text = data[0].ToString();
    lbOut2.Text = data[1].ToString();
    lbOut3.Text = data[2].ToString();
}), m_CancellationSource.Token);

Класс Progress обычно используется для отчета о прогрессе, но он может сообщать любые данные.

Для очистки, когда форма закрывается, вы можете обработать событие FormClosing следующим образом:

private async void Form_FormClosing(object sender, FormClosingEventArgs e)
{
    if (!m_USBReaderTask.IsCompleted)
    {
        e.Cancel = true;
        this.Enabled = false;
        m_CancellationSource.Cancel();
        await m_USBReaderTask;
        await Task.Yield(); // Ensure the asynchronous completion before Close
        this.Close();
    }
}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...