C#: зачем нужны InvokeRequired и Invoke? Печать
Добавил(а) microsin   

Вы наверное знаете, что нельзя просто так обращаться к графическим элементам управления интерфейса пользователя (GUI формы) из других потоков, таких как BackgroundWorker [2]. Это связано с безопасностью данных потоков, когда конкурентное обращение разными потоками к одним и тем же данным на запись может привести к непредсказуемому поведению программы.

Предотвращают такие неприятные ситуации специальные программные решения. В потоке BackgroundWorker, например, этот функционал инкапсулирован в вызове ReportProgress и обработчике ProgressChanged. Для обращения к элементам интерфейса управления пользователя (к экземпляру класса System.Windows.Forms.Control, и следовательно экземпляру формы окна программы System.Windows.Forms.Form как частности) из сторонних потоков как раз и нужны свойство InvokeRequired и метод Invoke класса Control.

Методы Invoke/BeginInvoke позволяют Вам перепоручить (делегировать, delegate) классу GUI вызов некоторых связанных с интерфейсом пользователя методов. Передаваемые в форму данные (такие как значения параметров) помещаются в некую очередь, поддерживаемую потоком GUI. Поток GUI берет из этой очереди элементы и выполняет реальные вызовы синхронно с другими вызовами обработки GUI. С помощью Invoke вызывающий сторонний поток (не поток GUI) переводится в состояние ожидания, пока вызов не будет завершен потоком GUI. BeginInvoke сделает возврат немедленно в момент реального вызова.

Итак, invoke-методы реализованы в классе Control (и также в экземпляре формы Form1 приложения), и полезно их использовать только тогда, когда Вы вызываете методы формы или обращаетесь к её свойствам (т. е. к классу Control) из потока, который не относится к потоку обработки графического интерфейса формы (GUI).

[InvokeRequired]

Чтобы было понятно, давайте разберемся подробнее. InvokeRequired делает проверку, в каком контексте работает текущий код. Проверка InvokeRequired нужна только тогда, когда вызов/обращение к форме происходит из стороннего потока, не относящегося к GUI.

InvokeRequired всегда вернет true, если это работает контекст чужого потока (не потока GUI), тогда необходимо вызывать Invoke со ссылкой на текущий выполняемый код (как это делается, будет показано на примере ниже). Если это не сделать, то произойдет ошибка времени выполнения:

Недопустимая операция в нескольких потоках: попытка доступа к элементу управления
'имя_элемента_GUI' не из того потока, в котором он был создан.

InvokeRequired всегда вернет false, если работает поток GUI, и в этом случае вызывать Invoke не требуется.

[Invoke]

Добавим ясности: разберем простой конкретный пример с компонентом USB HID (библиотека UsbLibrary), код которого работает в отдельном потоке. Экземпляр класса UsbHidPort myUSB сделан дочерним по отношению к классу формы приложения, и в его обработчике приема данных необходимо отображать принятые данные в ListBox формы. Обработчик приема данных UsbHidPort работает в отдельном потоке, не в потоке GUI формы, поэтому без invoke-функционала добавить данные в ListBox формы формы нельзя. Для потокобезопасной работы в обработчике приема данных UsbHidPort нужно добавить код наподобие следующего:

private void usb_OnDataRecieved(object sender, DataRecievedEventArgs args)
{
   try
   {
      string DataSt = null;
      if (ParentForm.InvokeRequired)
      {
         ParentForm.Invoke(new DataRecievedEventHandler(usb_OnDataRecieved),
                           new object[] { sender, args });
      }
      else
      {
         // Показываем, что приняли:
         for (int i = 4; i < args.data.Length; i++)
            DataSt += (char)(args.data[i]);
         ParentForm.lbConsole.Items.Add(DataSt);
         logfile.write(DataSt);
      }
   }
   catch (Exception ex)
   {
      logfile.write(ex.Message);
   }
}

Обратите внимание, что вызов InvokeRequired и Invoke формы приложения происходит через переменную ParentForm, добавленную к классу myUSB, которая была предварительно инициализирована в коде загрузки формы после вызова конструктора экземпляра класса mySUB.

namespace WindowsFormsApplication1
{
   public partial class FormMain : Form
   {
      log logfile;
      public myUSB myusb;
 
      public FormMain()
      {
         InitializeComponent();
      }
 
      void Form1_Load(object sender, EventArgs e)
      {
         myusb = new myUSB();
         myusb.ParentForm = this;
... }
 
      //Другие методы формы:
      ...
   }
}

namespace WindowsFormsApplication1
{
   public class myUSB
   {
      byte[] rxdata;
      int rxidx;
      public UsbHidPort usb;
      const int WAIT_ANSWER_TIMEOUT = 500;
      log logfile;
      public FormMain ParentForm;
 
      public void RxReset()
      {
         rxidx = 0;
         rxdata[0] = 0;
      }
 
      //Конструктор
      public pkrc()
      {
         if (null == logfile)
            logfile = new log(this.GetType().ToString());
         if (null == rxdata)
            rxdata = new byte[256];
         RxReset();
         if (null == usb)
            usb = new UsbHidPort();
         usb.VendorId = 0x03EB;
         usb.ProductId = 0x6201;
      }
 
      ~pkrc()
      {
         Close();
      }
 
      #region Обработка событий USB библиотеки
      /// < summary>
      /// Запустится при подключении любого USB устройства
      /// < /summary>
      /// < param name="sender">< /param>
      /// < param name="e">< /param>
      private void usb_OnDeviceArrived(object sender, EventArgs e)
      {
         logfile.write("usb_OnDeviceArrived");
      }
 
      /// < summary>
      /// При отключении любого устройства USB
      /// < /summary>
      /// < param name="sender">< /param>
      /// < param name="e">< /param>
      private void usb_OnDeviceRemoved(object sender, EventArgs e)
      {
         logfile.write("usb_OnDeviceRemoved");
      }
 
      /// < summary>
      /// При подключении определенного устройства USB
      /// < /summary>
      /// < param name="sender">< /param>
      /// < param name="e">< /param>
      private void usb_OnSpecifiedDeviceArrived(object sender, EventArgs e)
      {
         logfile.write("usb_OnSpecifiedDeviceArrived");
      }
      /// < summary>
      /// При отключении определенного устройства USB
      /// < /summary>
      /// < param name="sender">< /param>
      /// < param name="e">< /param>
      private void usb_OnSpecifiedDeviceRemoved(object sender, EventArgs e)
      {
         if (ParentForm.InvokeRequired)
         {
            ParentForm.Invoke(new EventHandler(usb_OnSpecifiedDeviceRemoved),
            new object[] { sender, e });
         }
         else
         {
            logfile.write("UsbHidPort closed");
         }
      }
 
      /// < summary>
      /// ////////////////////////////////////////////////////////////////////////////////
      /// Приём данных
      /// < /summary>
      /// < param name="sender">< /param>
      /// < param name="args">Report, принятый от устройства USB HID, содержится
      /// в args.data< /param>
      private void usb_OnDataRecieved(object sender, DataRecievedEventArgs args)
      {
         try
         {
            if (ParentForm.InvokeRequired)
            {
               ParentForm.Invoke(new EventHandler(usb_OnDataRecieved),
                                 new object[] { sender, e });
            }
            else
            {
               string DataSt = null;
               //Простой вывод в лог данных:
               for (int i = 0; i < args.data.Length; i++)
                  DataSt += args.data[i].ToString("X2") + " ";
               logfile.write(DataSt);
               //Вывод данных в ListBox формы:
               ParentForm.lbConsole.Items.Add(DataSt);
            }
         }
         catch (Exception ex)
         {
            logfile.write(ex.Message);
         }
      }
 
      /// < summary>
      /// При передаче данных в устройство USB
      /// < /summary>
      /// < param name="sender">< /param>
      /// < param name="e">< /param>
      private void usb_OnDataSend(object sender, EventArgs e)
      {
         //logfile.write("usb_OnDataSend");
      }
      #endregion
      
      public void Open()
      {
         //Инициализация библиотеки USB HID.
         usb.OnDataRecieved += new DataRecievedEventHandler(this.usb_OnDataRecieved);
         usb.OnDataSend += new System.EventHandler(this.usb_OnDataSend);
         usb.OnDeviceArrived += new System.EventHandler(this.usb_OnDeviceArrived);
         usb.OnDeviceRemoved += new System.EventHandler(this.usb_OnDeviceRemoved);
         usb.OnSpecifiedDeviceArrived += new System.EventHandler(this.usb_OnSpecifiedDeviceArrived);
         usb.OnSpecifiedDeviceRemoved += new System.EventHandler(this.usb_OnSpecifiedDeviceRemoved);
         try
         {
            usb.Open(false);
            if (0 != usb.GetLastError)
            {
               logfile.write(String.Format("cannot open USB HID device VID:0x{0:X4} PID:0x{1:X4}",
                             usb.VendorId, usb.ProductId));
            }
         }
         catch (Exception ex)
         {
            logfile.write(ex.Message);
         }
      }
 
      public void Close()
      {
         usb.Close();
      }
 
      public String Received()
      {
         string rx = Encoding.GetEncoding(1251).GetString(rxdata).Substring(0, rxidx);
         if (rx.Contains(DBGMSG))
         {
            if (rx.Contains("\r\n"))
            {
               int pos = rx.IndexOf("\r\n");
               int len = pos - DBGMSG.Length;
               logfile.write(rx.Substring(DBGMSG.Length, len));
               rx = rx.Substring(pos+2);
            }
         }
         return rx;
      }
 
      public bool SendHID(UInt32 cmdcode, byte [] data, int len)
      {
         bool sendOK = false;
         byte[] Report = new byte[usb.SpecifiedDevice.OutputReportLength];
         int idx = 0;
         Report[idx++] = 0;  //report ID
         //Добавление передаваемых данных в Report (полезная нагрузка):
         Report[idx++] = 'D';
         Report[idx++] = 'E';
         Report[idx++] = 'A';
         Report[idx++] = 'D';
         Report[idx++] = 'B';
         Report[idx++] = 'E';
         Report[idx++] = 'E';
         Report[idx++] = 'F';
         while (idx < usb.SpecifiedDevice.OutputReportLength)
            Report[idx++] = 0;            //остаток буфера обнуляем
         try
         {
            usb.SpecifiedDevice.SendData(Report);
            sendOK = true;
         }
         catch (Exception ex)
         {
            logfile.write(ex.Message);
         }
         return sendOK;
      }
   }
}

[Ссылки]

1. invoke in other class site:codeproject.com.
2. Visual Studio C++ 2010 Express: BackgroundWorker Class.
3Идеология C#: события и делегаты.