Как сделать обратные вызовы событий в моей поточной форме безопасными? - PullRequest
33 голосов
/ 08 августа 2008

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

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

Ответы [ 6 ]

31 голосов
/ 08 августа 2008

Чтобы немного упростить код Саймона, вы можете использовать встроенный универсальный делегат 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";
}
16 голосов
/ 08 августа 2008

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

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

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

9 голосов
/ 01 сентября 2008

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

void SomethingHappened(object sender, EventArgs ea)
{
   MethodInvoker del = delegate{ textBox1.Text = "Something happened"; }; 
   InvokeRequired ? Invoke( del ) : del(); 
}
2 голосов
/ 04 декабря 2008

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

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

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; }
        }
    }
}
1 голос
/ 31 мая 2012

Как 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.";
}
0 голосов
/ 08 августа 2008

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

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