Принудительное выполнение участка кода из главного потока приложения

Рейтинг: 4Ответов: 2Опубликовано: 14.02.2011

В приложении используются backgroundworker'ы для решения некоторых задач. Подскажите, как правильно сделать так, чтобы определенный фрагмент кода всегда выполнялся только из главного потока приложения, даже при обращении к ним из потока worker'a?

Ответы

▲ 9Принят

Для того чтобы возможно было выполнить код в некотором потоке, необходимо, чтобы сам этот поток был организован определённым образом, а именно в виде цикла обработки сообщений. В случае с WinAPI это достигается использованием цикла GetMessage - DispatchMessage. Если не использовать WindowsForms, подобный цикл можно организовать с помощью класса EventWaitHandle.

private readonly EventWaitHandle m_signal = new EventWaitHandle(false, EventResetMode.AutoReset);
private readonly Queue<Action> m_events = new Queue<Action>();
private volatile bool m_stop = false;

public void Start()
{
  // Создаём поток синхронизации. Все события будут обрабатываться в нём
  var th = new Thread(this.Run);
  th.Start();
}

private void Run()
{
  // Ожидаем сигнала о новом событии
  while (m_signal.WaitOne(Timeout.Infinite))
  {
    // Если пришёл сигнал о прекращении работы - выходим
    if (m_stop)
      break;

    // Пока обрабатываются сообщения из очереди, добавлять их нельзя
    lock(m_events)
    {
      while (m_events.Count > 0)
      {
        try
        {
          // Достаём из очереди делегат и выполняем его
          m_events.Dequeue()();
        }
        catch(Exception ex)
        {
          // Как-нибудь обрабатываем внешнее исключение
        }
      }
    }
  }
  // Освобождаем ресурсы
  m_signal.Close();
}

public void Invoke(Action action)
{
  if (action == null)
    throw new ArgumentNullException("action");

  // Добавляем событие в очередь и инициируем событие
  // Пока добавляем событие, приостанавливаем обработку
  lock(m_events)
  {
    m_events.Enqueue(action);
    m_signal.Set();
  }
}

public void Stop()
{
  // Инициируем сигнал о прекращении работы
  lock(m_events)
  {
    m_stop = true;
    m_signal.Set();
  }
}
▲ 3

Думаю, что стоит привести ещё один пример, на этот раз - как использовать интерфейс ISyncronizeInvoke. Этот интерфейс реализован в классе System.Windows.Forms.Control и предназначен специально для прокидывания любых делегатов в поток обработки цикла сообщений, в случае с контролами - цикла сообщений Windows.

Интерфейс содержит 4 члена.

Свойство InvokeRequired возвращает true, если текущий поток выполнения не является потоком обработки цикла сообщений. Если мы уже в потоке обработки цикла сообщений, свойство возвращает false.

Метод Invoke добавляет переданный ему делегат в цикл обработки сообщений и блокирует текущий поток до тех пор, пока цикл обработки сообщений не выполнит этот делегат. После того как делегат выполнен, метод возвращает управление со значением, которое вернул переданный ему делегат в процессе выполнения. Если делегат имел тип возвращаемого значения void, метод возвращает null.

Метод BeginInvoke добавляет переданный ему в качестве параметра делегат в цикл обработки сообщений и немедленно возвращает управление. Он возвращает объект типа IAsyncResult, который можно передать в качестве параметра последнему методу EndInvoke для ожидания завершения асинхронной операции. Простейший цикл обработки сообщений, который я привёл в предыдущем примере, обрабатывает события именно асинхронно, возвращая управление, не дожидаясь выполнения добавленного в очередь метода.

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

private readonly ISynchronizeInvoke m_invoker;
private readonly BindingList<MyEntry> m_entries = new BindingList<MyEntry>();

// Конструктор с Dependency Injection, которому передаётся контрол либо форма
public BackgroundHandler(ISynchronizeInvoke invoker)
{
  if (invoker == null)
    throew new ArgumentNullException("invoker");

  m_invoker = invoker;
}

// 
public bool Add(MyEntry entry)
{
  // Если мы не в потоке обработки сообщений, 
  // метод добавляет в очередь сообщений ссылку на самого себя, 
  // ждёт завершения обработки и возвращает управление
  if (m_invoker.InvokeRequired)
    return (bool)invoker.Invoke(new Func<MyEntry, bool>(this.Add), new object[] { entry });

  // Поскольку мы сюда попали, мы уже в потоке обработки сообщений, 
  // делаем полезную работу - добавляем переданный элемент в список.
  // Возвращаемое значение - чисто для демонстрации, как его возвращать
  m_entries.Add(entry);
  return true;
}

Помещение кода синхронизации прямо в начало метода перед полезной работой очень облегчает читабельность кода, если полезная работа содержит много логики.