Найчистіший спосіб викликати перехресні нитки


79

Я вважаю, що модель подій .NET така, що я часто буду піднімати подію в одному потоці та слухати її в іншому. Мені було цікаво, який найчистіший спосіб перенести подію з фонового потоку на мій інтерфейс користувача.

На основі пропозицій спільноти я використав це:

// earlier in the code
mCoolObject.CoolEvent+= 
           new CoolObjectEventHandler(mCoolObject_CoolEvent);
// then
private void mCoolObject_CoolEvent(object sender, CoolObjectEventArgs args)
{
    if (InvokeRequired)
    {
        CoolObjectEventHandler cb =
            new CoolObjectEventHandler(
                mCoolObject_CoolEvent);
        Invoke(cb, new object[] { sender, args });
        return;
    }
    // do the dirty work of my method here
}

Майте на увазі, що InvokeRequired може повернути значення false, коли існуючий керований елемент керування ще не має керованого дескриптора. Ви повинні проявляти обережність у випадках, які будуть підняті до того, як контроль буде повністю створений.
GregC

Відповіді:


28

Пара спостережень:

  • Не створюйте простих делегатів явно в такому коді, якщо ви не користуєтесь попередньою версією 2.0, щоб ви могли використовувати:
   BeginInvoke(new EventHandler<CoolObjectEventArgs>(mCoolObject_CoolEvent), 
               sender, 
               args);
  • Також вам не потрібно створювати та заповнювати масив об'єктів, оскільки параметр args має тип "params", тому ви можете просто передати його у списку.

  • Я б , ймовірно , в користь Invokeбільш , BeginInvokeяк останній результат буде в коді викликається асинхронно , який може або не може бути те , що ви після цього, але буде робити обробку наступних винятків важко поширюватися без виклику EndInvoke. Що могло б статися, так це те, що ваш додаток в кінцевому підсумку отримає TargetInvocationExceptionнатомість.


44

У мене є якийсь код для цього Інтернеті. Це набагато приємніше, ніж інші пропозиції; однозначно перевірити це.

Зразок використання:

private void mCoolObject_CoolEvent(object sender, CoolObjectEventArgs args)
{
    // You could use "() =>" in place of "delegate"; it's a style choice.
    this.Invoke(delegate
    {
        // Do the dirty work of my method here.
    });
}

Ви також можете змінити простір імен System.Windows.Formsу своєму розширенні. Таким чином ви уникаєте додавання власного простору імен кожного разу, коли вам це потрібно.
Джо Алмор

10

Я уникаю зайвих оголошень делегатів.

private void mCoolObject_CoolEvent(object sender, CoolObjectEventArgs args)
{
    if (InvokeRequired)
    {
        Invoke(new Action<object, CoolObjectEventArgs>(mCoolObject_CoolEvent), sender, args);
        return;
    }
    // do the dirty work of my method here
}

Для не подій можна використовувати System.Windows.Forms.MethodInvokerделегат або System.Action.

РЕДАГУВАТИ: Крім того, кожна подія має відповідного EventHandlerделегата, тому взагалі не потрібно повторно оголошувати одного.


1
Для мене це працювало так:Invoke(new Action<object, CoolObjectEventArgs>(mCoolObject_CoolEvent), sender, args);
Антоніо Алмейда

@ToniAlmeida Так, це була помилка в моєму коді. Дякуємо, що вказали на це.
Конрад Рудольф

4

Я зробив наступний 'універсальний' клас виклику між потоками для власних цілей, але я думаю, що варто поділитися ним:

using System;
using System.Collections.Generic;
using System.Text;
using System.Windows.Forms;

namespace CrossThreadCalls
{
  public static class clsCrossThreadCalls
  {
    private delegate void SetAnyPropertyCallBack(Control c, string Property, object Value);
    public static void SetAnyProperty(Control c, string Property, object Value)
    {
      if (c.GetType().GetProperty(Property) != null)
      {
        //The given property exists
        if (c.InvokeRequired)
        {
          SetAnyPropertyCallBack d = new SetAnyPropertyCallBack(SetAnyProperty);
          c.BeginInvoke(d, c, Property, Value);
        }
        else
        {
          c.GetType().GetProperty(Property).SetValue(c, Value, null);
        }
      }
    }

    private delegate void SetTextPropertyCallBack(Control c, string Value);
    public static void SetTextProperty(Control c, string Value)
    {
      if (c.InvokeRequired)
      {
        SetTextPropertyCallBack d = new SetTextPropertyCallBack(SetTextProperty);
        c.BeginInvoke(d, c, Value);
      }
      else
      {
        c.Text = Value;
      }
    }
  }

І ви можете просто використовувати SetAnyProperty () з іншого потоку:

CrossThreadCalls.clsCrossThreadCalls.SetAnyProperty(lb_Speed, "Text", KvaserCanReader.GetSpeed.ToString());

У цьому прикладі вищезазначений клас KvaserCanReader запускає власний потік і робить виклик для встановлення властивості тексту мітки lb_Speed ​​в основній формі.


3

Я думаю, що найчистішим способом, безумовно, є пройти маршрут AOP. Зробіть кілька аспектів, додайте необхідні атрибути, і вам більше ніколи не доведеться перевіряти спорідненість ниток.


Я не розумію вашої пропозиції. C # не є власною мовою, орієнтованою на аспекти. Ви маєте на увазі якийсь шаблон чи бібліотеку для реалізації аспектів, що реалізують маршовий марш-марш?
Ерік

Я використовую PostSharp, тому я визначаю поведінку потоків у класі атрибутів, а потім використовую, скажімо, атрибут [WpfThread] перед кожним методом, який потрібно викликати в потоці інтерфейсу користувача.
Дмитро Нестерук

3

Використовуйте контекст синхронізації, якщо потрібно надіслати результат потоку інтерфейсу користувача. Мені потрібно було змінити пріоритет потоку, тому я перейшов від використання потоків пулу потоків (прокоментував код) і створив новий власний потік. Я все ще міг використовувати контекст синхронізації, щоб повернути, вдалося скасувати базу даних чи ні.

    #region SyncContextCancel

    private SynchronizationContext _syncContextCancel;

    /// <summary>
    /// Gets the synchronization context used for UI-related operations.
    /// </summary>
    /// <value>The synchronization context.</value>
    protected SynchronizationContext SyncContextCancel
    {
        get { return _syncContextCancel; }
    }

    #endregion //SyncContextCancel

    public void CancelCurrentDbCommand()
    {
        _syncContextCancel = SynchronizationContext.Current;

        //ThreadPool.QueueUserWorkItem(CancelWork, null);

        Thread worker = new Thread(new ThreadStart(CancelWork));
        worker.Priority = ThreadPriority.Highest;
        worker.Start();
    }

    SQLiteConnection _connection;
    private void CancelWork()//object state
    {
        bool success = false;

        try
        {
            if (_connection != null)
            {
                log.Debug("call cancel");
                _connection.Cancel();
                log.Debug("cancel complete");
                _connection.Close();
                log.Debug("close complete");
                success = true;
                log.Debug("long running query cancelled" + DateTime.Now.ToLongTimeString());
            }
        }
        catch (Exception ex)
        {
            log.Error(ex.Message, ex);
        }

        SyncContextCancel.Send(CancelCompleted, new object[] { success });
    }

    public void CancelCompleted(object state)
    {
        object[] args = (object[])state;
        bool success = (bool)args[0];

        if (success)
        {
            log.Debug("long running query cancelled" + DateTime.Now.ToLongTimeString());

        }
    }

2

Я завжди дивувався, наскільки дорого завжди вважати, що потрібно викликати ...

private void OnCoolEvent(CoolObjectEventArgs e)
{
  BeginInvoke((o,e) => /*do work here*/,this, e);
}

1
Виконання BeginInvoke всередині потоку графічного інтерфейсу призведе до того, що розглянута дія буде відкладена до наступного разу, коли потік інтерфейсу обробляє повідомлення Windows. Насправді це може бути корисно зробити в деяких випадках.
supercat

2

Як цікаву побічну ноту, прив’язка WPF автоматично обробляє маршалінг, так що ви можете прив’язати інтерфейс користувача до властивостей об’єктів, які змінені у фонових потоках, не роблячи нічого особливого. Це виявилось чудовою економією часу для мене.

У XAML:

<TextBox Text="{Binding Path=Name}"/>

це не працює. як тільки ви встановите проп для потоку, що не є інтерфейсом користувача, ви отримаєте виняток .. тобто Name = "gbc" bang! невдача ... немає безкоштовного сирного партнера
Boppity Bop

Це не безкоштовно (це вимагає часу на виконання), але механізм прив'язки wpf, здається, автоматично обробляє міжпотокове маршування. Ми багато використовуємо це для реквізитів, які оновлюються мережевими даними, отриманими у фонових потоках. Тут є пояснення: blog.lab49.com/archives/1166
gbc

1
@gbc Aaaaі пояснення зникло 404.
Січень 'сплайт' К.

0

Ви можете спробувати розробити якийсь загальний компонент, який приймає SynchronizationContext як вхід і використовує його для виклику подій.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.