Страницы

Поиск по вопросам

воскресенье, 30 сентября 2018 г.

Работа с контролами из фонового потока

Ситуация следующая:
имеется окно с кнопкой 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 "результат"; } }


Ответ

Решение для .NET 4.0 и более поздних версий
Использовать Асинхронную модель на основе задач (TAP) и ключевые слова async-await
private async void button1_Click(object sender, EventArgs e) { string result = await Task.Factory.StartNew( () => Worker.SomeLongOperation(), TaskCreationOptions.LongRunning);
this.label1.Text = result; }
Преимущества:
Код значительно короче других вариантов, вызовы записаны в той последовательности, в которой они выполняются. Никаких коллбеков и ручной работы с потоками. async не дает обработчику события завершиться, но при этом не блокирует UI. TaskCreationOptions.LongRunning подсказывает планировщику, что операция будет действительно долгой, и для ее выполнения не стоит использовать пул потоков.
Встроенная поддержка ключевых слова 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(s => label1.Text = s);
string result = await Task.Factory.StartNew( () => Worker.SomeLongOperation(progress), TaskCreationOptions.LongRunning);
this.label1.Text = result; }
class Worker { public static string SomeLongOperation(IProgress 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)(() => this.label1.Text = result));
})).Start(); }
BeginInvoke поставит код на выполнение в тот поток, в котором был создан label1 и продолжит выполнение фонового потока. При использовании Invoke вместо BeginInvoke фоновый поток будет приостановлен до завершения выполнения кода в UI потоке.

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

Отправить комментарий