Получение значений из потока пользовательского интерфейса из перекрестного потока

#c# #multithreading

#c# #многопоточность

Вопрос:

Я пытаюсь добавить потоковую обработку в свою программу таким образом, чтобы я не замораживал весь основной поток пользовательского интерфейса при выполнении «дорогостоящей» вычислительной работы.

В настоящее время моя программа выполняет асинхронный запуск, Task указывающий на функцию с именем startWork() при нажатии кнопки следующим образом:

     async void startParse_Click(object sender, EventArgs e)
    {
        await Task.Run(() => startWork());
    }
  

Обычно для установки значений я делаю следующее:

 niceButton.BeginInvoke(new MethodInvoker(() =>
{
      niceButton.Text = "new text";
}));
  

Однако при получении данных из элементов управления и использовании этих данных за пределами MethodInvoker у меня возникают небольшие проблемы. Моя цель — выполнить foreach цикл вокруг моего listView1.Items , за пределами потока пользовательского интерфейса.

Вот содержимое startWork() :

     void startWork()
    {
        // Naturally I cannot reference my listView1 control because it is in a
        // different thread and is blocked the the "illegal" cross-thread check

        int overallProgress = 0;
        ListView.ListViewItemCollection items = null;

        // This unfortunately doesn't work (MethodInvoker is in a different scope?)
        listView1.BeginInvoke( new MethodInvoker(() => {
            items = listView1.Items;
        }));

        int totalItems = items.Count; // "items" isn't recognized

        foreach (ListViewItem files in items )
        {
            // slowwww work
        }
    }
  

Я также пытался передать ListView.ListViewItemCollection в качестве аргумента функции безрезультатно.

Продолжаем получать Cross-thread operation not valid: accessed from a thread other than the thread it was created on

Примечание: Целевой платформой является .NET 4.7 — возможно, в более новых версиях .NET есть лучший / более эффективный метод?

Возможно, мне просто не хватает фундаментального понимания async / tasks, но я предполагаю, что я упускаю из виду что-то важное.

Комментарии:

1. где вычислительная работа, также зачем вообще использовать async в любом из этого, вы все равно просто выгружаете

2. Это то, что вы называете вычислительной работой? в foreach? вы обновляете пользовательский интерфейс в foreach?

3. Вы обновляете пользовательский интерфейс в foreach?

4. Какова природа вашего foreach, можете ли вы обновить свой код тем, что находится в этом цикле

Ответ №1:

Элементы пользовательского интерфейса, включая ListView.ListViewItemCollection и ListViewItem , являются «потокоподобными». Это означает, что к ним можно получить доступ только в потоке пользовательского интерфейса.

Для выполнения фоновой работы вам следует передавать только объекты, не связанные с потоком. Например, a List<string> , а не a ListViewItemCollection или a List<ListViewItem> .

 async void startParse_Click(object sender, EventArgs e)
{
  var items = listView1.Items;
  var data = /* extract List<string> from items */
  await Task.Run(() => startWork(data));
}

void startWork(List<string> files)
{
  int overallProgress = 0;
  foreach (var file in files)
  {
    // slowwww work
  }
}
  

Кроме того, вы не должны использовать BeginInvoke . Вместо этого используйте IProgress<T> with Progress<T> .

Ответ №2:

Вам не нужно перебирать элементы в рабочем потоке, поскольку переключение с одного элемента в коллекции на другой происходит довольно быстро и не приводит к зависанию пользовательского интерфейса. Просто перенесите «дорогостоящую» вычислительную работу в рабочий поток:

 private async void StartParseButtonClick(object sender, EventArgs e)
{
    // disable button (we are on UI thread)
    var startParseButton = sender as Button;
    startParseButton.Enabled = false;

    try
    {
        // copy just in case if someone will add new item while we iterating over
        var items = listView1.Items.OfType<ListViewItem>().ToList();
        foreach (var item in items)
            await Parse(item); // this will be invoked in worker thread
    }
    finally
    {
        // enable button finally (we are on UI thread)
        startParseButton.Enabled = true;
    }
}

private async Task Parse(ListViewItem item)
{
    // slowwww work (we are on worker thread)
    await Task.Delay(500);
}