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

204
18 марта 2018, 18:39

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

  • имеется окно с кнопкой 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 "результат";
    }
}
Answer 1
Решение для .NET 4.0 и более поздних версий

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

private async void button1_Click(object sender, EventArgs e)
{
    string result = await Task.Factory.StartNew<string>(
                                             () => 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<string>(s => label1.Text = s);
    string result = await Task.Factory.StartNew<string>(
                                             () => Worker.SomeLongOperation(progress),
                                             TaskCreationOptions.LongRunning);
    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)(() => this.label1.Text = result));
        })).Start();
}

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

READ ALSO
Откуда идут запросы GET /web_push_sw.js

Откуда идут запросы GET /web_push_sw.js

Всем привет! Загрузил первое приложение на сервер JelasticВсе работает, но кто-нибудь может мне объяснить что за запросы GET /web_push_sw

243
Получение сырых данных Audio в js

Получение сырых данных Audio в js

Допустим есть некий sampemp3

260
Vuetify отключить анимацию

Vuetify отключить анимацию

Тесты perfomance показывают что анимация переходов и окон, потребление взлетает до небесМожно ли из Vuetify вырезать анимации ? Или переопределить...

263