Как сделать обратные вызовы событий в моей поточной форме безопасными?

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

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

8.08.2008 17:32:41
6 ОТВЕТОВ
РЕШЕНИЕ

Чтобы немного упростить код Саймона, вы можете использовать встроенный универсальный делегат Action. Это экономит ваш код с кучей типов делегатов, которые вам не нужны. Кроме того, в .NET 3.5 они добавили параметр params в метод Invoke, чтобы вам не приходилось определять временный массив.

void SomethingHappened(object sender, EventArgs ea)
{
   if (InvokeRequired)
   {
      Invoke(new Action<object, EventArgs>(SomethingHappened), sender, ea);
      return;
   }

   textBox1.Text = "Something happened";
}
34
12.08.2008 15:59:51

Вот основные моменты:

  1. Вы не можете управлять вызовами пользовательского интерфейса из потока, отличного от того, в котором они были созданы (поток формы).
  2. Вызовы делегатов (т. Е. Перехватчики событий) запускаются в том же потоке, что и объект, инициирующий событие.

Итак, если у вас есть отдельный поток «движок», выполняющий некоторую работу, и у вас есть некоторый пользовательский интерфейс, отслеживающий изменения состояния, которые могут быть отражены в пользовательском интерфейсе (например, индикатор выполнения или что-то еще), у вас есть проблема. Пожар двигателя - это событие изменения объекта, которое было перехвачено формой. Но делегат обратного вызова, что Форма, зарегистрированная в движке, вызывается в потоке движка ... а не в потоке Формы. И поэтому вы не можете обновить какие-либо элементы управления из этого обратного вызова. Doh!

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

private delegate void EventArgsDelegate(object sender, EventArgs ea);

void SomethingHappened(object sender, EventArgs ea)
{
   //
   // Make sure this callback is on the correct thread
   //
   if (this.InvokeRequired)
   {
      this.Invoke(new EventArgsDelegate(SomethingHappened), new object[] { sender, ea });
      return;
   }

   //
   // Do something with the event such as update a control
   //
   textBox1.Text = "Something happened";
}

Это довольно просто на самом деле.

  1. Используйте InvokeRequired, чтобы узнать, произошел ли этот обратный вызов в правильном потоке.
  2. Если нет, то повторно вызовите обратный вызов в правильном потоке с теми же параметрами. Вы можете повторно вызвать метод, используя методы Invoke (блокирующий) или BeginInvoke (неблокирующий).
  3. В следующий раз, когда вызывается функция, InvokeRequired возвращает false, потому что мы находимся в правильном потоке, и все довольны.

Это очень компактный способ решения этой проблемы и защиты ваших форм от многопоточных обратных вызовов событий.

17
8.08.2008 17:35:40
Я обычно предпочитаю BeginInvoke Invoke, но есть одна оговорка: нужно избегать слишком большого количества событий. Я использую переменную updateRequired, которая устанавливается в 1, когда происходит BeginInvoke, и выполняю BeginInvoke только в том случае, если она была нулевой (используя Interlocked.Exchange). Обработчик отображения имеет цикл while, который очищает updateRequired и, если он не равен нулю, выполняет обновление и циклы. В некоторых случаях добавляется таймер для дальнейшего ограничения частоты обновления (чтобы код не тратил все свое время на обновление показаний прогресса вместо выполнения реальной работы), но это сложнее.
supercat 19.10.2010 15:30:41
@Supercat ... регулирование событий - важная тема для многих приложений, но она не должна быть частью уровня пользовательского интерфейса. Следует создать отдельную прокси-шину событий для получения, помещения в очередь, объединения и повторной отправки событий через соответствующие интервалы. Любой подписчик на шину событий не должен знать, что происходит регулирование событий.
Simon Gillbee 19.10.2010 18:28:24
Я вижу места, где отдельная «шина событий» для обработки синхронизации может быть полезной, но во многих случаях конечному пользователю может показаться, что классу индикатора прогресса проще всего, если этот класс просто предоставляет свойство MinimumUpdateInterval.
supercat 19.10.2010 20:47:10

Во многих простых случаях вы можете использовать делегат MethodInvoker и избежать необходимости создавать свой собственный тип делегата.

0
8.08.2008 17:41:18

Я часто использую анонимные методы в этом сценарии:

void SomethingHappened(object sender, EventArgs ea)
{
   MethodInvoker del = delegate{ textBox1.Text = "Something happened"; }; 
   InvokeRequired ? Invoke( del ) : del(); 
}
9
21.10.2008 18:31:58

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

Вот краткий пример, который позволяет только один параллельный вызов; поддержка нескольких вызовов / событий требует немного больше сантехники.

using System;
using System.ComponentModel;
using System.Threading;
using System.Windows.Forms;

namespace WindowsFormsApplication1
{
    public class MainForm : Form
    {
        private TypeWithAsync _type;

        [STAThread()]
        public static void Main()
        {
            Application.EnableVisualStyles();
            Application.Run(new MainForm());
        }

        public MainForm()
        {
            _type = new TypeWithAsync();
            _type.DoSomethingCompleted += DoSomethingCompleted;

            var panel = new FlowLayoutPanel() { Dock = DockStyle.Fill };

            var btn = new Button() { Text = "Synchronous" };
            btn.Click += SyncClick;
            panel.Controls.Add(btn);

            btn = new Button { Text = "Asynchronous" };
            btn.Click += AsyncClick;
            panel.Controls.Add(btn);

            Controls.Add(panel);
        }

        private void SyncClick(object sender, EventArgs e)
        {
            int value = _type.DoSomething();
            MessageBox.Show(string.Format("DoSomething() returned {0}.", value));
        }

        private void AsyncClick(object sender, EventArgs e)
        {
            _type.DoSomethingAsync();
        }

        private void DoSomethingCompleted(object sender, DoSomethingCompletedEventArgs e)
        {
            MessageBox.Show(string.Format("DoSomethingAsync() returned {0}.", e.Value));
        }
    }

    class TypeWithAsync
    {
        private AsyncOperation _operation;

        // synchronous version of method
        public int DoSomething()
        {
            Thread.Sleep(5000);
            return 27;
        }

        // async version of method
        public void DoSomethingAsync()
        {
            if (_operation != null)
            {
                throw new InvalidOperationException("An async operation is already running.");
            }

            _operation = AsyncOperationManager.CreateOperation(null);
            ThreadPool.QueueUserWorkItem(DoSomethingAsyncCore);
        }

        // wrapper used by async method to call sync version of method, matches WaitCallback so it
        // can be queued by the thread pool
        private void DoSomethingAsyncCore(object state)
        {
            int returnValue = DoSomething();
            var e = new DoSomethingCompletedEventArgs(returnValue);
            _operation.PostOperationCompleted(RaiseDoSomethingCompleted, e);
        }

        // wrapper used so async method can raise the event; matches SendOrPostCallback
        private void RaiseDoSomethingCompleted(object args)
        {
            OnDoSomethingCompleted((DoSomethingCompletedEventArgs)args);
        }

        private void OnDoSomethingCompleted(DoSomethingCompletedEventArgs e)
        {
            var handler = DoSomethingCompleted;

            if (handler != null) { handler(this, e); }
        }

        public EventHandler<DoSomethingCompletedEventArgs> DoSomethingCompleted;
    }

    public class DoSomethingCompletedEventArgs : EventArgs
    {
        private int _value;

        public DoSomethingCompletedEventArgs(int value)
            : base()
        {
            _value = value;
        }

        public int Value
        {
            get { return _value; }
        }
    }
}
2
4.12.2008 20:08:40
Я думаю, что немного вводить в заблуждение говорить «это гарантирует, что события всегда вызываются из потока пользовательского интерфейса». Разве не было бы точнее сказать, что он гарантирует, что обработчик событий выполняется в том же SynchronizationContext / thread, в котором была создана задача? (Который может не быть потоком интерфейса пользователя / SynchronizationContext)
jspaey 7.08.2012 10:49:54

Как lazy programmer, у меня есть очень ленивый способ сделать это.

Что я делаю, так это просто.

private void DoInvoke(MethodInvoker del) {
    if (InvokeRequired) {
        Invoke(del);
    } else {
        del();
    }
}
//example of how to call it
private void tUpdateLabel(ToolStripStatusLabel lbl, String val) {
    DoInvoke(delegate { lbl.Text = val; });
}

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

Помните, что вы можете передавать функции непосредственно в метод DoInvoke.

private void directPass() {
    DoInvoke(this.directInvoke);
}
private void directInvoke() {
    textLabel.Text = "Directly passed.";
}
1
30.05.2012 21:57:58
Я все для ленивого программирования :) Если вы используете .NET 3.5 или выше, вы можете использовать Actionили Action<object, EventArgs>вместе с лямбда-выражениями:Doinvoke(() => textLabel.Text = "Something")
Simon Gillbee 31.05.2012 01:55:53