C # Многопоточность - вызов без контроля - PullRequest
17 голосов
/ 16 ноября 2009

Я только немного знаком с многопоточностью, потому что я читал об этом, но никогда не использовал это на практике.

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

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

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

Мой класс получает событие, и обработчик запускается в потоке, отличном от пользовательского интерфейса. Я хочу обнаружить это условие (как с InvokeRequired), а затем выполнить эквивалент BeginInvoke, чтобы вернуть управление потоку пользовательского интерфейса. Тогда соответствующие уведомления могут быть отправлены вверх по иерархии классов, и все мои данные будут затронуты только одним потоком.

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

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

Может быть, есть способ сохранить ссылку на поток, который запустил конструктор, а затем есть что-то в пространстве имен Threading, которое будет выполнять команды Invoke?

Есть ли способ обойти это? Мой подход совершенно неверен?

Ответы [ 7 ]

18 голосов
/ 16 ноября 2009

Посмотрите на класс AsyncOperation. Вы создаете экземпляр AsyncOperation в потоке, для которого вы хотите вызвать обработчик, используя метод AsyncOperationManager.CreateOperation. Аргумент, который я использую для Create, обычно равен нулю, но вы можете установить его на что угодно. Чтобы вызвать метод в этом потоке, используйте метод AsyncOperation.Post.

13 голосов
/ 16 ноября 2009

Используйте SynchronizationContext.Current , который будет указывать на то, с чем вы можете синхронизироваться.

Это сделает правильные вещи ™ в зависимости от типа приложения. Для приложения WinForms оно будет запускаться в основном потоке пользовательского интерфейса.

В частности, используйте метод SynchronizationContext.Send , например:

SynchronizationContext context =
    SynchronizationContext.Current ?? new SynchronizationContext();

context.Send(s =>
    {
        // your code here
    }, null);
2 голосов
/ 16 ноября 2009

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

Опять же, только те элементы управления пользовательского интерфейса, которые вы хотите обновить, должны быть вызваны для обеспечения безопасности потоков. Некоторое время назад я написал запись в блоге о " простом решении незаконных вызовов между потоками в C # "

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

if (label1.InvokeRequired) {
  label1.Invoke(
    new ThreadStart(delegate {
      label1.Text = "some text changed from some thread";
    }));
} else {
  label1.Text = "some text changed from the form's thread";
}

Надеюсь, это поможет. InvokeRequired технически необязателен, но элементы управления Invoking довольно дороги, поэтому убедитесь, что он не обновляет label1.Text через invoke, если он не нужен.

1 голос
/ 16 ноября 2009

Если вы используете WPF:

Вам нужна ссылка на объект Dispatcher, который управляет потоком пользовательского интерфейса. Затем вы можете использовать метод Invoke или BeginInvoke для объекта диспетчера, чтобы запланировать операцию, которая выполняется в потоке пользовательского интерфейса.

Самый простой способ получить диспетчер - это использовать Application.Current.Dispatcher. Это диспетчер, ответственный за основной (и, вероятно, единственный) поток пользовательского интерфейса.

Собираем все вместе:

class MyClass
{
    // Can be called on any thread
    public ReceiveLibraryEvent(RoutedEventArgs e)
    {
        if (Application.Current.CheckAccess())
        {
            this.ReceiveLibraryEventInternal(e);
        }
        else
        {
            Application.Current.Dispatcher.Invoke(
                new Action<RoutedEventArgs>(this.ReceiveLibraryEventInternal));
        }
    }

    // Must be called on the UI thread
    private ReceiveLibraryEventInternal(RoutedEventArgs e)
    {
         // Handle event
    }
}
1 голос
/ 16 ноября 2009

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

0 голосов
/ 08 октября 2018

Я просто столкнулся с той же ситуацией. Однако в моем случае я не мог использовать SynchronizationContext.Current, потому что у меня не было доступа к каким-либо компонентам пользовательского интерфейса и обратного вызова для захвата текущего контекста синхронизации. Оказывается, что если код в данный момент не работает в насосе сообщений Windows Forms, SynchronizationContext.Current будет установлен на стандартный SynchronizationContext, который будет просто выполнять вызовы Send в текущем потоке и Post вызовов в ThreadPool.

Я нашел этот ответ , объясняющий различные типы контекстов синхронизации. В моем случае решение состояло в том, чтобы создать новый объект WindowsFormsSynchronizationContext в потоке, который позже запустил бы обработку сообщений с помощью Application.Run (). Этот контекст синхронизации может затем использоваться другими потоками для запуска кода в потоке пользовательского интерфейса, даже не затрагивая какие-либо компоненты пользовательского интерфейса.

0 голосов
/ 16 ноября 2009

Есть ли способ обойти это?

Да, обходной путь для вас - создать потокобезопасную очередь.

  • Ваш обработчик событий вызывается сторонним потоком
  • Ваш обработчик событий помещает что-то (данные события) в коллекцию (например, список), которой вы владеете
  • Ваш обработчик событий что-то сигнализирует вашей собственной команде, что в коллекции есть данные для удаления из очереди и обработки:
    • Ваш поток может ожидать что-то (мьютекс или что-то еще); когда его мьютекс сигнализируется обработчиком события, он просыпается и проверяет очередь.
    • В качестве альтернативы, вместо того, чтобы сигнализировать, он может периодически просыпаться (например, раз в секунду или как угодно) и опрашивать очередь.

В любом случае, поскольку ваша очередь записывается двумя разными потоками (сторонний поток ставится в очередь, а ваш поток удаляется из очереди), она должна быть защищенной от потоков защищенной очереди.

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