58

Ситуация следующая:

  • имеется окно с кнопкой button1 и меткой label1.
  • по кнопке запускается какая-то долгая операция, в отдельном потоке.
  • по завершению операции нужно вывести результат label1.

При попытке поменять значение label1.Text код падает с исключением InvalidOperationException:

WinForms:

Cross-thread operation not valid: Control 'label1' accessed from a thread other than the thread it was created on.

Недопустимая операция в нескольких потоках: попытка доступа к элементу управления 'label1' не из того потока, в котором он был создан.

WPF:

The calling thread cannot access this object because a different thread owns it.

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

Пример кода:

private void button1_Click(object sender, EventArgs e)
{
    (new Thread((s) =>
        {
            var result = Worker.SomeLongOperation();
        // следующая строчка падает c InvalidOperationException:
        this.label1.Text = result;
    })).Start();

}

class Worker { public static string SomeLongOperation() { Thread.Sleep(1000); return "результат"; } }

  • ...в рамках перевода каноничных вопросов и ответов из EnSO –  Apr 22 '15 at 14:02
  • А где ссылка на оригинальный вопрос? Если это перевод, то не стоит несколько ответов приводить, достаточно один общий собрать из существующих ответов. – jfs Apr 22 '15 at 19:34
  • Ответы из http://stackoverflow.com/questions/661561/how-to-update-the-gui-from-another-thread-in-c/18033198. Оригинальный вопрос не ищется по тексту ошибки, так что я написал как можно более общий случай. Если хотите оформить одним ответом - редактируйте, ответы общие. Я оформлял по аналогии с http://ru.stackoverflow.com/questions/416584/ и http://ru.stackoverflow.com/questions/417453/. –  Apr 22 '15 at 20:06
  • Если вопрос достаточно полезен, чтобы его перевести, то перевод должен делать человек, который может выбрать лучшее. Механический перевод, без понимая смысла, существующих ответов на английском -- менее полезен. – jfs Apr 22 '15 at 20:28
  • @jfs, вы думаете что я не достаточно компетентен и не могу "выбрать лучшее" между BeginInvoke и async? А вам не приходило в голову, что это два независимых решения, каждое из которых применимо в определенном контексте (который зависит от версии фреймворка и от масштабов проблемы). Тут наоборот, не хватает еще одного подробного ответа про background worker. –  Apr 22 '15 at 22:38
  • @jfs лучшее, кстати - async/await. и он ни разу не упоминался ни в одном из ответов на схожие вопросы на ruSO. Только потому, что это не самое понятное и простое решение. За Invoke в .NET 4.5 нужно бить по рукам. Но Invoke - это простой костыль. Погуглил, вставил, все работает. Знаете C# лучше меня и можете выбрать - feel free to edit. –  Apr 22 '15 at 22:50
  • Я не сужу компетентность людей (иногда могу высказаться о качестве вопроса / ответа). Конкретная тема не важна, важен общий подход к вопросам-переводам. Если вы можете выбрать лучшее, то напишите один ответ как я предложил в самом первом комментарии. Кстати, сравнение различных подходов лучше в этот ответ вставить, а не в комментариях к вопросу оставлять. – jfs Apr 22 '15 at 23:03
  • Ок, попробую объединить –  Apr 23 '15 at 06:52
  • Ответ который реально помог в этой ситуации: https://ru.stackoverflow.com/a/106720/219224 – Роман Арсеньев Jan 10 '19 at 17:34
  • @РоманАрсеньев он там есть ниже, в части "Решение для .NET 3.5 и более ранних версий". в 4.0+ - лучше использовать нормальное решение. –  Jan 10 '19 at 17:39

2 Answers2

71

Решение для .NET 4.0 и более поздних версий

Использовать Асинхронную модель на основе задач (TAP) и ключевые слова async-await:

private async void button1_Click(object sender, EventArgs e)
{
    string result = await Task.Run(() => Worker.SomeLongOperation());
this.label1.Text = result;

}

Преимущества:

  • Код значительно короче других вариантов, вызовы записаны в той последовательности, в которой они выполняются.
  • Никаких коллбеков и ручной работы с потоками.
  • async не дает обработчику события завершиться, но при этом не блокирует UI.

Встроенная поддержка ключевых слова async/await появились в .NET 4.5 и Visual Studio 2013.

Данное решение также может быть использовано для .NET 4.0 и Silverlight 5, если используется версия Visual Studio не ниже 2012. Для этого нужно установить пакет Microsoft.Bcl.Async из NuGet.

Решение с отображением прогресса выполнения

Если в процессе выполнения нужно отображать прогресс или промежуточные результаты из второго потока, то можно использовать класс Progress:

private async void button1_Click(object sender, EventArgs e)
{
    var progress = new Progress<string>(s => label1.Text = s);
string result = await Task.Run(() =&gt; Worker.SomeLongOperation(progress));

this.label1.Text = result;

}

class Worker { public static string SomeLongOperation(IProgress<string> progress) { // Perform a long running work... for (var i = 0; i < 10; i++) { Task.Delay(500).Wait(); progress.Report(i.ToString()); } return "результат"; } }

Progress захватывает SynchronizationContext в момент создания, и использует его для выполнения операций, избавляя от ручных вызовов Invoke.

Решение для .NET 3.5 и более ранних версий

Использовать Invoke/BeginInvoke:

// WinForms:
this.label1.BeginInvoke((MethodInvoker)(() => this.label1.Text = result));

// WPF: Dispatcher.BeginInvoke((Action)(() => this.label1.Content = result));

Для .NET 2.0, в котором еще не было лямбд, эквивалентный код записывается с помощью анонимных делегатов:

// WinForms:
this.label1.BeginInvoke((MethodInvoker)(delegate { this.label1.Text = result; });

// WPF: Dispatcher.BeginInvoke((Action)(delegate { this.label1.Content = result; });

Полный код:

private void button1_Click(object sender, EventArgs e)
{
    (new Thread((s) =>
        {
            var result = Worker.SomeLongOperation();
        this.label1.BeginInvoke((MethodInvoker)(() =&gt; this.label1.Text = result));

    })).Start();

}

BeginInvoke поставит код на выполнение в тот поток, в котором был создан label1 и продолжит выполнение фонового потока. При использовании Invoke вместо BeginInvoke фоновый поток будет приостановлен до завершения выполнения кода в UI потоке.

aepot
  • 49,560
  • 2
    О, это правильный ответ. А зачем вы сделали его общим? – VladD Apr 22 '15 at 14:10
  • @VladD это перевод http://stackoverflow.com/a/18033198/1985167. Видимо, автор решил не зарабатывать на этом репутацию :). – andreycha Apr 22 '15 at 14:13
  • @andreycha: Намёк понял. – VladD Apr 22 '15 at 14:14
  • 1
    Потому что это перевод http://stackoverflow.com/a/18033198/1988244 с адаптацией. Причем вопрос оттуда переведен, но он негуглибелен по тексту ошибки, и правильных подробных ответов в нем нет. Я попробовал сформулирвоать в более общем виде. –  Apr 22 '15 at 14:14
  • @andreycha репутация, кстати, все равно капает за плюсы на вопрос :) –  Apr 22 '15 at 14:17
  • Дополнил ответ указанием, как можно использовать шаблон TAP на .NET 4.0. Может тогда и заголовок исправить? – Memoizer Apr 23 '15 at 07:12
  • подправил, не уверен насколько удачно –  Apr 23 '15 at 07:16
  • "{ this.label1.Text = result; }" -- из-за этого будет неприятный сюрприз с замыканием. см. Почему делегат добавляет неправильные данные? – Stack Jan 07 '16 at 19:50
  • 3
    @Stack не "будет", а "возможен" – Pavel Mayorov Jan 17 '16 at 08:20
  • 2
    Можно добавить еще и SynchronizationContext который можно использовать и без UI

    if (SynchronizationContext.Current == null) SynchronizationContext.SetSynchronizationContext(new WindowsFormsSynchronizationContext());

            Sc = SynchronizationContext.Current;
    
    

    используя Send или Post

    – Serginio Apr 26 '16 at 08:44
  • ассоциация:http://stackoverflow.com/a/18033198/276994 – VladD Jan 15 '17 at 14:21
  • Зачем Task.Delay(500).Wait(), почему не просто Thread.Sleep(500)? И лямбы -- это фича компилятора C# 3.0, который доступен начиная с VS2008, а не фича .NET Framework. – Raider Jan 18 '17 at 18:42
  • @Raider это общий ответ. считаете что в нем что-то не так, и хотите это исправить - исправляйте. –  Jan 18 '17 at 19:57
  • @Raider Task.Delay(500).Wait() пришел из вопроса-оригинала на enSO вместе с остальным кодом. –  Jan 18 '17 at 20:02
0

Еще есть пример для UI для безопасного вызова из не главного потока.

Чтобы взаимодействовать с элементами интерфейса из другого thread

  void SomeAsyncMethod()
  {
      // Do some work             

      if (this.InvokeRequired)
      {
          this.Invoke((MethodInvoker)(() =>
              {
                  DoUpdateUI();

              }
          ));
      }
      else
      {
          DoUpdateUI();
      }
  }

  void DoUpdateUI()
  {
      // Your UI update code here
  }

Здесь this.Invoke метод контрола

  • Про Invoke сказано в ответе выше :) сам Invoke внутри делает проверку на this.InvokeRequired, так что она избыточна в вызывающем коде. –  Nov 29 '18 at 13:00
  • @PashaPash согласен, но как вариант пойдет:) Давно еще по забытым причинам приходилось использовать именно такую конструкцию. – DiMa Makarov Nov 29 '18 at 13:10