Ситуация следующая:
имеется окно с кнопкой 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 "результат";
}
}
Ответы
Ответ 1
Решение для .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 потоке.
Ответ 2
Еще есть пример для 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 метод контрола
Комментариев нет:
Отправить комментарий