.NET BackgroundWorker — исключение InvalidOperationException: недопустимая операция между потоками

#c# #winforms #multithreading

#c# #winforms #многопоточность

Вопрос:

У меня есть проект, закодированный в .NET Winforms. Мне нужно реализовать операцию интеллектуального анализа данных, распечатать текст в текстовое поле и обновить ход выполнения.

Я пытался использовать BackgroundWorker для выполнения, но он выдает исключение InvalidOperationException (недопустимая операция между потоками: управление ‘xxxxx’, доступ к которому осуществляется из потока, отличного от потока, в котором он был создан)

Чтобы сузить возможные причины проблемы, я запустил новый проект, включающий следующее: Кнопка — для запуска ярлыка BackgroundWorker — для печати текста. И индикатор выполнения.

Однако результат тот же. Я искал в SOF, и мне сказали использовать делегат, но я с ним не знаком.

Это пример кода, который выдает ошибку:

 using System;
using System.Collections.Generic;
using System.ComponentModel;

namespace TestProject
{
    public partial class Form1 : Form
    {
        private readonly BackgroundWorker _bw = new BackgroundWorker();

        public Form1()
        {
            InitializeComponent();
            _bw.DoWork  = RosterWork;
            _bw.ProgressChanged  = BwProgressChanged;
            _bw.RunWorkerCompleted  = BwRunWorkerCompleted;
            _bw.WorkerReportsProgress = true;
            _bw.WorkerSupportsCancellation = false;
        }

        private void RosterWork(object sender, DoWorkEventArgs doWorkEventArgs)
        {
            for (int i = 0; i < 1000; i  )
            {
                label1.Text = i.ToString();
                _bw.ReportProgress(Convert.ToInt32((i * (100 / 1000))));
            }
        }

        private void BwProgressChanged(object sender, ProgressChangedEventArgs e)
        {
            progressBar1.Value = e.ProgressPercentage;
        }

        private void btnStart_Click(object sender, EventArgs e)
        {
            progressBar1.Show();
            _bw.RunWorkerAsync();
        }

        private void BwRunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
        {
            progressBar1.Hide();
        }

    }
}
 

Обновление:
Я следую ответу Джона Скита, он действительно работает в моем тестовом проекте, но вернемся к моему реальному проекту,

Макет моей формы:

Form — TabControl — Tab1 -Tab1Panel -TextBox1

При достижении этой строки :

 TextBox txtbox1 = new TextBox();
Tab1Panel.Controls.Add(txtbox1);
 

Ошибка по-прежнему возникает, когда я программно добавляю текстовое поле в панель управления.

Наконец, я заменяю на это:

  if (Tab1Panel.InvokeRequired)
     Tab1Panel.Invoke((MethodInvoker)delegate { Tab1Panel.Controls.Add(txtbox1); });
 else
     Tab1Panel.Controls.Add(txtbox1);
 

Все работает. Как определить, требуется ли элемент управления InvokeRequired, указан ли он в качестве элемента управления?

Ответ №1:

Это проблема:

 label1.Text = i.ToString();
 

Вы пытаетесь изменить текст метки внутри BackgroundWorker , который не выполняется в потоке пользовательского интерфейса. Смысл BackgroundWorker в том, чтобы выполнять всю работу, не связанную с пользовательским интерфейсом, используя ReportProgress для периодического «возврата» к потоку пользовательского интерфейса и обновления пользовательского интерфейса в соответствии с достигнутым прогрессом.

Так что либо вам label1.Text также нужно изменить BwProgressChanged , либо вам нужно использовать Control.Invoke / BeginInvoke так же, как и в любом другом фоновом потоке:

 // Don't capture a loop variable in a lambda expression...
int copy = i;
Action updateLabel = () => label1.Text = copy.ToString();
label1.BeginInvoke(updateLabel);
 

Подробнее о части копирования см. Сообщение в блоге Эрика Липперта «Закрытие переменной цикла считается вредным». В данном конкретном случае это проблема только потому, что я использую BeginInvoke . Это можно изменить на просто:

 Action updateLabel = () => label1.Text = i.ToString();
label1.Invoke(updateLabel);
 

… но теперь фоновый рабочий всегда будет ждать, пока пользовательский интерфейс догонит, прежде чем он продолжит работу, что в реальной жизни обычно не то, что вы хотите. Я обычно предпочитаю BeginInvoke over Invoke .

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

1. Спасибо, Джон Скит! Я ищу в Google about . СЕТЕВАЯ потоковая обработка. Я прочитал какую-то статью о потоке / ThreadStart / ThreadPool / Delegate . Кажется, что все они больше похожи на ожидаемые стандарты программирования потоков. Так правильно ли использовать BackgroundWorker?

2. @Shiba: Боюсь, я не совсем понимаю ваш комментарий, особенно «все они больше похожи на ожидаемые стандарты»…

Ответ №2:

используйте этот код, который будет работать

  BeginInvoke((MethodInvoker)delegate
               {
                   TextBox1.Text  = "your text here";

               });
 

Ответ №3:

Вы получаете доступ к своей метке, которая была создана в потоке GUI, из вашего потока backgroundworker. Доступ к элементу управления Windows из потока, отличного от того, в котором был создан элемент управления, запрещен; поэтому вам выдается исключение.

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

Ответ №4:

Вы должны выполнить методы элемента управления из потока, который их создал. Чтобы обновить их из другого потока, используйте Invoke . Пример того, как это можно сделать, можно найти здесь . Вы также должны прочитать многопоточность с формами и элементами управления в MSDN.