Ситуация следующая:
имеется окно с кнопкой 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
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
string result = await Task.Factory.StartNew
this.label1.Text = result;
}
class Worker
{
public static string SomeLongOperation(IProgress
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 потоке.
Комментариев нет:
Отправить комментарий