Как выполнить синхронный метод асинхронно и дождаться результата

Мое приложение подключено ко многим внешним устройствам через протокол TCP / IP и COM-порты. Основная логика находится в классе MainController (реализованном как конечный автомат), который прослушивает сигналы от внешних устройств и отправляет им команды.

Каждый раз, когда MainController получает внешний сигнал, он уведомляет графический интерфейс с событием, например OnSensor1Received, _4 _, ... так что я могу отобразить значок или сообщение или что-то еще. Та же логика применима и для другого направления - когда MainController отправляет сигнал на внешнее устройство, возникает событие и что-то отображается в графическом интерфейсе.

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

Но теперь мне нужно добавить новое устройство, в котором есть только один общедоступный метод int Process(string input, out string output). Выполнение метода может занять до пары секунд, а пока весь другой код ожидает - например, если во время выполнения этого метода я получаю сигнал от другого внешнего устройства, графический интерфейс будет обновлен только после блокировки. метод Write выполняется. Как я могу заставить метод Write выполняться асинхронно, чтобы графический интерфейс отображал все другие сигналы в реальном времени?

Пример кода, это вызывается где-то в MainController, когда я пытаюсь использовать новое устройство:

// Notify GUI that the blocking plugin was activated.
OnBlockingPluginActive(this, EventArgs.Empty);

string result;
// Here is the problem - during the execution of this method the GUI is not responding for other signals.
var status = blockingPlugin.Process(input, out result);
if (status == 0)
{
    // OK, notify GUI and fire a BlockingPluginOK command in finite state machine.
    OnBlockingPluginOK(this, EventArgs.Empty);
}
else
{
    // Error, notify GUI and fire a BlockingPluginError command in finite state machine.
    OnBlockingPluginError(this, EventArgs.Empty);
}

Также обратите внимание, что я использую .net 4.0 и не могу перейти на 4.5, поэтому нет встроенной поддержки async / await.


person sventevit    schedule 24.07.2016    source источник
comment
Попробуйте это: https://www.nuget.org/packages/Microsoft.Bcl.Async/1.0.16   -  person Fabio    schedule 24.07.2016
comment
Если вы все еще используете .NET 4.0, просто используйте Task.Factory.StartNew и используйте .ContinueWith   -  person    schedule 24.07.2016
comment
пара секунд поведение не разумное. Вы можете переместить вызов в рабочий поток, но вы просто активируете его и все равно заблокируете. Либо эта библиотека сильно испорчена, либо она просто сейчас работает некорректно из-за проблем с окружающей средой. Вы не собираетесь улучшать его, пытаясь обойти проблему, обратитесь к автору библиотеки за советом.   -  person Hans Passant    schedule 24.07.2016


Ответы (2)


Вы можете использовать старомодный асинхронный вызов делегата через BeginInvoke / EndInvoke с AsyncCallback и захваченными переменными:

// Notify GUI that the blocking plugin was activated.
OnBlockingPluginActive(this, EventArgs.Empty);

string result;
Func<int> processAsync = () => blockingPlugin.Process(input, out result);
processAsync.BeginInvoke(ar =>
{
    var status = processAsync.EndInvoke(ar);
    if (status == 0)
    {
        // OK, notify GUI and fire a BlockingPluginOK command in finite state machine.
        OnBlockingPluginOK(this, EventArgs.Empty);
    }
    else
    {
       // Error, notify GUI and fire a BlockingPluginError command in finite state machine.
       OnBlockingPluginError(this, EventArgs.Empty);
    }
}, null);
person Ivan Stoev    schedule 24.07.2016
comment
Спасибо, именно то, что я искал. - person sventevit; 24.07.2016

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

...

Пока здесь все коммуникации асинхронны ...

Это уже асинхронно. Он просто использует события для уведомления о завершении, а не Task или IObservable. Обычные события делают код более запутанным (т.е. ваш код должен быть распределен по множеству методов и явно поддерживать свои собственные объекты состояния), но на самом деле они асинхронны.

Как я могу заставить метод Write выполняться асинхронно, чтобы графический интерфейс отображал все другие сигналы в реальном времени?

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

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

Если вы умеете использовать Microsoft.Bcl.Async, то можете написать такой код:

try
{
  var result = await TaskEx.Run(() =>
  {
    string result;
    if (blockingPlugin.Process(input, out result) != 0)
      throw new Exception("Blocking plugin failed.");
    return result;
  });
  OnBlockingPluginOK(this, EventArgs.Empty);
}
catch
{
  OnBlockingPluginError(this, EventArgs.Empty);
}

В противном случае вам придется делать это по-старому:

var ui = TaskScheduler.FromCurrentSynchronizationContext();
var task = Task.Factory.StartNew(() =>
{
  string result;
  if (blockingPlugin.Process(input, out result) != 0)
    throw new Exception("Blocking plugin failed.");
  return result;
}, CancellationToken.None, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default);
task.ContinueWith(t => OnBlockingPluginOK(this, EventArgs.Empty),
    TaskContinuationOptions.DenyChildAttach | TaskContinuationOptions.OnlyOnRanToCompletion,
    ui);
task.ContinueWith(t => OnBlockingPluginError(this, EventArgs.Empty),
    TaskContinuationOptions.DenyChildAttach | TaskContinuationOptions.NotOnRanToCompletion,
    ui);
person Stephen Cleary    schedule 24.07.2016
comment
Спасибо за объяснение. Можете ли вы прокомментировать решение @Ivan Stoev (stackoverflow.com/a/38551022/113858)? Я сначала попробовал, и похоже, что он работает нормально. - person sventevit; 24.07.2016
comment
@sventevit: BeginInvoke также будет использовать поток пула потоков; однако этот пример кода вызовет OnBlockingPluginOK и OnBlockingPluginError в потоке пула потоков, а не в потоке пользовательского интерфейса. - person Stephen Cleary; 25.07.2016
comment
@sventevit: Обычно я бы так не сказал. - person Stephen Cleary; 25.07.2016