BackgroundWorkerThread доступ в потоке - PullRequest
0 голосов
/ 12 мая 2009

Я использую BackgroundWorker большую часть времени в выигрышных приложениях, чтобы показать прогресс по мере получения данных. У меня сложилось впечатление, что Work_completed гарантированно будет выполнено в потоке основного интерфейса, но это не так. Если мы создаем поток и вызываем worker.RunWorkerAsync внутри него, он разрывается, если мы пытаемся обновить какой-либо графический элемент управления. Вот пример

private void StartButton_Click(object sender, EventArgs e)
{
    Thread thread1 = new Thread(new ThreadStart(PerformWorkerTask));
    _worker = new BackgroundWorker();
    thread1.Start();
}

public void PerformWorkerTask()
{
    _worker.DoWork += delegate
    {
        for (int i = 0; i < 10; i++)
        {
            Thread.Sleep(100);
        }
    };

    _worker.RunWorkerCompleted += delegate
    {
        // this throws exception
        MessageLabel.Text = "Completed";
    };
    _worker.RunWorkerAsync();

 }

Как мы можем заставить фонового работника работать в этом случае?

Ответы [ 7 ]

1 голос
/ 13 мая 2009

Похоже, проблема в том, что вы хотите внести изменения в компонент GUI, и вы на самом деле не уверены, что вы находитесь в потоке GUI. Дэн опубликовал правильный метод безопасной установки свойства компонента GUI, но я считаю следующий простой способ:

            MessageLabel.Invoke(
                (MethodInvoker)delegate
                {
                    MessageLabel.Text = "Hello World";
                });

Если есть какие-либо проблемы с этим подходом, я хотел бы знать о них!

1 голос
/ 12 мая 2009

RunWorkerAsync выполняет свою магию синхронизации потоков, получая SynchronizationContext из потока, в котором он вызывается. Затем он гарантирует, что события будут выполнены в правильном потоке в соответствии с семантикой полученной им SynchronizationContext. В случае WindowsFormsSynchronizationContext, который автоматически используется при использовании WinForms, события синхронизируются путем отправки в очередь сообщений потока, запустившего операцию. Конечно, это все прозрачно для вас, пока не сломается.

EDIT: вы ДОЛЖНЫ вызывать RunWorkerAsync из потока пользовательского интерфейса, чтобы это работало. Если вы не можете сделать это каким-либо другим способом, вам лучше всего вызвать начало операции над элементом управления, чтобы рабочий запускался в потоке пользовательского интерфейса:

private void RunWorker()
{
    _worker = new BackgroundWorker();

    _worker.DoWork += delegate 
    {
        // do work
    };

    _worker.RunWorkerCompleted += delegate 
    {
        MessageLabel.Text = "Completed";
    };

    _worker.RunWorkerAsync();
}

// ... some code that's executing on a non-UI thread ...
{
    MessageLabel.Invoke(new Action(RunWorker));
}
1 голос
/ 12 мая 2009

Из вашего примера трудно понять, чем хорош поток (thread1), но если вам действительно нужен этот поток1, то я думаю, что ваш единственный вариант - использовать MainForm.Invoke() для выполнения RunWorkerAsync() (или небольшого метода вокруг это) в основной теме.

Добавлено: Вы можете использовать что-то вроде этого:

Action a = new Action(_worker.RunWorkerAsync);
this.Invoke(a);
0 голосов
/ 13 мая 2009

Лучший способ справиться с этими общими проблемами - решить их один раз. Здесь я публикую небольшой класс, который оборачивает поток backgroupdworker и гарантирует, что завершенная работа всегда выполняется в потоке UI.


</p>

<p>using System.Windows.Forms;
namespace UI.Windows.Forms.Utilities.DataManagment
{
    public class DataLoader
    {
        private BackgroundWorker _worker;
        private DoWorkEventHandler _workDelegate;
        private RunWorkerCompletedEventHandler _workCompleted;
        private ExceptionHandlerDelegate _exceptionHandler;
        public static readonly Control ControlInvoker = new Control();</p>

<pre><code>     public DoWorkEventHandler WorkDelegate
    {
        get { return _workDelegate; }
        set { _workDelegate = value; }
    }

    public RunWorkerCompletedEventHandler WorkCompleted
    {
        get { return _workCompleted; }
        set { _workCompleted = value; }
    }

    public ExceptionHandlerDelegate ExceptionHandler
    {
        get { return _exceptionHandler; }
        set { _exceptionHandler = value; }
    }

    public void Execute()
    {
        if (WorkDelegate == null)
        {
            throw new Exception(
                "WorkDelegage is not assinged any method to execute. Use WorkDelegate Property to assing the method to execute");
        }
        if (WorkCompleted == null)
        {
            throw new Exception(
                "WorkCompleted is not assinged any method to execute. Use WorkCompleted Property to assing the method to execute");
        }

        SetupWorkerThread();
        _worker.RunWorkerAsync();
    }

    private void SetupWorkerThread()
    {
        _worker = new BackgroundWorker();
        _worker.WorkerSupportsCancellation = true;
        _worker.DoWork += WorkDelegate;
        _worker.RunWorkerCompleted += worker_RunWorkerCompleted;

    }

    void worker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
    {
        if(e.Error !=null && ExceptionHandler != null)
        {
            ExceptionHandler(e.Error);
            return;
        }
        ControlInvoker.Invoke(WorkCompleted, this, e);
    }
}
</code>

}

</p> <p>And here is the usage. One thing to note is that it exposes a static property ControlInvoker that needs to be set only once (you should do it at the beginning of the app load)</p> <p>Let's take the same example that I posted in question and re write it</p> <p>DataLoader loader = new DataLoader(); loader.ControlInvoker.Parent = this; // needed to be set only once</p> <p>private void StartButton_Click(object sender, EventArgs e) {</p> <pre><code>Thread thread1 = new Thread(new ThreadStart(PerformWorkerTask)); _worker = new BackgroundWorker(); thread1.Start();

}

public void PerformWorkerTask () {

loader.WorkDelegate = делегат { // получить любые данные, которые вы хотите для (int i = 0; i

}

Приветствия

0 голосов
/ 12 мая 2009

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

this.Dispatcher.BeginInvoke(() => MessageLabel.Text = "Completed")

вместо

MessageLabel.Text = "Completed"

Возможно, у вас проблемы с доступом к нескольким потокам, поэтому вы должны убедиться, что вы обращаетесь к свойствам MessageLabel в своем потоке пользовательского интерфейса. Это один из способов сделать это. Некоторые из других предложений также действительны. Вопрос, который нужно задать себе: почему вы создаете поток, который не делает ничего, кроме создания потока BackgroundWorker? Если есть причина, то хорошо, но из того, что вы показали здесь, нет причины, по которой вы не могли бы создать и запустить поток BackgroundWorker из вашего обработчика событий, и в этом случае не возникнет проблема с межпоточным доступом, поскольку событие RunWorkerCompleted обработчик вызовет своих делегатов в потоке пользовательского интерфейса.

0 голосов
/ 12 мая 2009

Я считаю, что BackgroundWorker предназначен для автоматического использования нового потока. Поэтому создание нового потока просто для вызова RunWorkerAsync является излишним. Вы создаете поток, чтобы создать еще один поток. Что, вероятно, происходит, это:

  1. Вы создаете новый поток из потока 1 (поток с графическим интерфейсом); назовите эту тему 2.
  2. Из потока 2 вы запускаете RunWorkerAsync, который сам создает еще один поток; назовите эту тему 3.
  3. Код для RunWorkerCompleted выполняется в потоке 2, который является потоком с именем RunWorkerAsync.
  4. Поскольку поток 2 не совпадает с потоком GUI (поток 1), вы получаете недопустимое исключение вызова между потоками.

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

избавиться от посторонней новой нити; просто объявите _worker WithEvents, добавьте обработчики в _worker.DoWork и _worker.RunWorkerCompleted, а затем вызовите _worker.RunWorkerAsync вместо определения пользовательской функции PerformWorkerTask.

РЕДАКТИРОВАТЬ : Чтобы обновить средства управления графическим интерфейсом в поточно-ориентированном режиме, используйте код, подобный следующему (более или менее скопированный из этой статьи из MSDN ):

delegate void SetTextCallback(System.Windows.Forms.Control c, string t);

private void SafeSetText(System.Windows.Forms.Control c, string t)
{
  if (c.InvokeRequired)
  {
    SetTextCallback d = new SetTextCallback(SafeSetText);
    d.Invoke(d, new object[] { c, t });
  }

  else
  {
    c.Text = t;
  }
}
0 голосов
/ 12 мая 2009

В представленном здесь коде вы добавляете делегаты для событий BackgroundWorker в отдельном потоке из потока пользовательского интерфейса.

Попробуйте добавить обработчики событий в основной поток пользовательского интерфейса, и все будет в порядке.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...