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

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

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

Ответов (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";
}

Как и 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. Вы не можете выполнять вызовы элементов управления пользовательского интерфейса из потока, отличного от того, в котором они были созданы (поток формы).
  2. Вызовы делегатов (то есть перехватчики событий) запускаются в том же потоке, что и объект, запускающий событие.

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

На помощь приходит 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, потому что теперь мы находимся в правильном потоке, и все довольны.

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

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

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

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

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

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

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; }
        }
    }
}