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

322

У меня есть asyncметод, который не возвращает данных:

public async Task MyAsyncMethod()
{
    // do some stuff async, don't return any data
}

Я вызываю это из другого метода, который возвращает некоторые данные:

public string GetStringData()
{
    MyAsyncMethod(); // this generates a warning and swallows exceptions
    return "hello world";
}

Вызов MyAsyncMethod()без ожидания вызывает предупреждение « Поскольку этот вызов не ожидается, текущий метод продолжает выполняться до завершения вызова » в Visual Studio. На странице этого предупреждения говорится:

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

Я уверен, что не хочу ждать завершения звонка; Мне не нужно или у меня нет времени. Но вызов может вызвать исключения.

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

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

Обновить:

Для людей, которые предполагают, что я просто жду результата, это код, который отвечает на веб-запрос в нашем веб-сервисе (ASP.NET Web API). Ожидание в контексте пользовательского интерфейса сохраняет поток пользовательского интерфейса свободным, но ожидание при вызове веб-запроса будет ожидать завершения задачи, прежде чем отвечать на запрос, тем самым увеличивая время ответа без причины.

Джордж Пауэлл
источник
Почему бы просто не создать метод завершения и просто игнорировать его там? Потому что, если он работает в фоновом потоке. Тогда это не остановит вашу программу в любом случае.
Яхья
1
Если вы не хотите ждать результата, единственный вариант - игнорировать / подавлять предупреждение. Если вы делаете хотите дождаться результата / исключения, тогдаMyAsyncMethod().Wait()
Питер Ричи
1
О вашем редактировании: это не имеет смысла для меня. Скажем, ответ отправляется клиенту через 1 секунду после запроса, а через 2 секунды ваш асинхронный метод вызывает исключение. Что бы вы сделали с этим исключением? Вы не можете отправить его клиенту, если ваш ответ уже отправлен. Что бы ты еще сделал с этим?
2
@Romoku Достаточно справедливо. Предполагая, что кто-то смотрит на журнал, в любом случае. :)
2
Вариант сценария ASP.NET Web API - это самодостаточный веб-API в долгоживущем процессе (например, в службе Windows), где запрос создает длинную фоновую задачу для выполнения чего-то дорогостоящего, но все же хочет получить ответ быстро с HTTP 202 (принято).
Давид Рубин

Ответы:

187

Если вы хотите получить исключение «асинхронно», вы можете сделать:

  MyAsyncMethod().
    ContinueWith(t => Console.WriteLine(t.Exception),
        TaskContinuationOptions.OnlyOnFaulted);

Это позволит вам иметь дело с исключением в потоке, отличном от «основного» потока. Это означает, что вам не нужно «ждать» вызова MyAsyncMethod()от потока, который вызывает MyAsyncMethod; но все же позволяет вам делать что-то с исключением - но только если происходит исключение.

Обновить:

технически, вы можете сделать что-то подобное с await :

try
{
    await MyAsyncMethod().ConfigureAwait(false);
}
catch (Exception ex)
{
    Trace.WriteLine(ex);
}

... что было бы полезно, если бы вам нужно было специально использовать try/ catch(или using), но я считаю, что ContinueWithэто немного более явно, потому что вы должны знать, что ConfigureAwait(false)значит.

Питер Ричи
источник
7
Я превратил это в метод расширения Task: public static class AsyncUtility {public static void PerformAsyncTaskWithoutAwait (эта задача Task, Action <Task> exceptionHandler) {var dummy = task.ContinueWith (t => exceptionHandler (t), TaskContinuationOptions.OnlyOnFapted); }} Использование: MyAsyncMethod (). PerformAsyncTaskWithoutAwait (t => log.ErrorFormat («Произошла ошибка при вызове MyAsyncMethod: \ n {0}», t.Exception));
Марк Авениус
2
downvoter. Комментарии? Если что-то не так в ответе, я хотел бы знать и / или исправить.
Питер Ричи
2
Эй, я не понизил голос, но ... Можете ли вы объяснить свое обновление? Звонок не должен был ожидаться, поэтому, если вы сделаете это таким образом, тогда вы действительно ждете звонка, вы просто не продолжаете в захваченном контексте ...
Бартош
1
«ждать» не является правильным описанием в этом контексте. Строка после ConfiguratAwait(false)не выполняется до тех пор, пока задача не завершится, но текущий поток не «ждет» (то есть блокирует) этого, следующая строка вызывается асинхронно для вызова await. Без ConfigureAwait(false)этой следующей строки будет выполняться в исходном контексте веб-запроса. С ConfigurateAwait(false)он выполняется в том же контексте, что и метод асинхронной (задача), освободив исходный контекст / нить продолжить ...
Питер Ритчи
15
Версия ContinueWith отличается от версии try {await} catch {}. В первой версии все после ContinueWith () будет выполнено немедленно. Начальная задача уволена и забыта. Во второй версии все после catch {} будет выполняться только после завершения начальной задачи. Вторая версия эквивалентна «ОЖИДАНИЕ MyAsyncMethod () ContinueWith (т => Console.WriteLine (t.Exception), TaskContinuationOptions.OnlyOnFaulted) .ConfigureAwait (Fals);.
Thanasis Ioannidis
67

Вы должны сначала рассмотреть вопрос о внесении GetStringDataв asyncметод и он awaitзадача вернулся изMyAsyncMethod .

Если вы абсолютно уверены, что вам не нужно обрабатывать исключения MyAsyncMethod или знать, когда они завершаются, то вы можете сделать это:

public string GetStringData()
{
  var _ = MyAsyncMethod();
  return "hello world";
}

Кстати, это не «общая проблема». Очень редко хочется выполнить какой-то код и не заботиться о том, завершается ли он, и не заботиться о том, успешно ли он завершается.

Обновить:

Поскольку вы работаете в ASP.NET и хотите вернуться раньше, мой блог на эту тему может оказаться полезным . Тем не менее, ASP.NET не был разработан для этого, и нет никаких гарантий что ваш код будет работать после возврата ответа. ASP.NET сделает все возможное, чтобы запустить его, но не может этого гарантировать.

Итак, это прекрасное решение для чего-то простого, например, для добавления события в журнал, где не имеет значения, потеряете ли вы кое-где. Это не очень хорошее решение для любых критически важных для бизнеса операций. В этих ситуациях необходимо принять более сложную архитектуру с постоянным способом сохранения операций (например, очереди Azure, MSMQ) и отдельным фоновым процессом (например, роль рабочего Azure, служба Win32) для их обработки.

Стивен Клири
источник
2
Я думаю, что вы, возможно, неправильно поняли. Мне все равно, если он генерирует исключения и сбои, но я не хочу ждать метода перед возвратом моих данных. Также посмотрите мои изменения в контексте, в котором я работаю, если это что-то меняет.
Джордж Пауэлл
5
@ GeorgePowell: очень опасно иметь код, запущенный в контексте ASP.NET без активного запроса. У меня есть запись в блоге, которая может вам помочь , но, не зная больше вашей проблемы, я не могу сказать, рекомендую ли я такой подход или нет.
Стивен Клири
@ StefhenCleary У меня есть аналогичная потребность. В моем примере у меня есть / нужен механизм пакетной обработки для запуска в облаке, я буду «пинговать» конечную точку, чтобы запустить пакетную обработку, но я хочу немедленно вернуться. Так как он запускается, он может обрабатывать все оттуда. Если бы возникали исключения, то они просто регистрировались бы в моих таблицах "BatchProcessLog / Error" ...
ganders
8
В C # 7 вы можете заменить var _ = MyAsyncMethod();на _ = MyAsyncMethod();. Это по-прежнему позволяет избежать предупреждения CS4014, но делает его более явным, если вы не используете переменную.
Брайан
48

Ответом Питера Ричи было то, что я хотел, и статья Стивена Клири о раннем возвращении в ASP.NET была очень полезной.

Однако в качестве более общей проблемы (не специфичной для контекста ASP.NET) следующее консольное приложение демонстрирует использование и поведение ответа Питера с использованием Task.ContinueWith(...)

static void Main(string[] args)
{
  try
  {
    // output "hello world" as method returns early
    Console.WriteLine(GetStringData());
  }
  catch
  {
    // Exception is NOT caught here
  }
  Console.ReadLine();
}

public static string GetStringData()
{
  MyAsyncMethod().ContinueWith(OnMyAsyncMethodFailed, TaskContinuationOptions.OnlyOnFaulted);
  return "hello world";
}

public static async Task MyAsyncMethod()
{
  await Task.Run(() => { throw new Exception("thrown on background thread"); });
}

public static void OnMyAsyncMethodFailed(Task task)
{
  Exception ex = task.Exception;
  // Deal with exceptions here however you want
}

GetStringData()возвращается рано, не дожидаясь, MyAsyncMethod()а MyAsyncMethod()возникающие исключения рассматриваются в, OnMyAsyncMethodFailed(Task task)а не в try/ catchоколоGetStringData()

Джордж Пауэлл
источник
9
Удалите Console.ReadLine();и добавьте немного сна / задержки, MyAsyncMethodи вы никогда не увидите исключения.
тымтам
17

Я в конечном итоге с этим решением:

public async Task MyAsyncMethod()
{
    // do some stuff async, don't return any data
}

public string GetStringData()
{
    // Run async, no warning, exception are catched
    RunAsync(MyAsyncMethod()); 
    return "hello world";
}

private void RunAsync(Task task)
{
    task.ContinueWith(t =>
    {
        ILog log = ServiceLocator.Current.GetInstance<ILog>();
        log.Error("Unexpected Error", t.Exception);

    }, TaskContinuationOptions.OnlyOnFaulted);
}
Filimindji
источник
8

Это называется огнем и забудь, и для этого есть расширение .

Потребляет задачу и ничего с ней не делает. Полезно для вызова «забывай и забывай» асинхронных методов внутри асинхронных методов.

Установите пакет nuget .

Использование:

MyAsyncMethod().Forget();
еси
источник
12
Это ничего не делает, это просто хитрость, чтобы убрать предупреждение. См github.com/microsoft/vs-threading/blob/master/src/...
Paolo Fulgoni
2

Я предполагаю, что возникает вопрос, зачем вам это нужно? Причина asyncв C # 5.0 заключается в том, что вы можете ждать результата. Этот метод на самом деле не асинхронный, а просто вызывается за раз, чтобы не слишком сильно мешать текущему потоку.

Возможно, лучше начать поток и оставить его закончить самостоятельно.

rhughes
источник
2
asyncэто немного больше, чем просто «ожидание» результата. «await» подразумевает, что строки, следующие за «await», выполняются асинхронно в том же потоке, который вызвал «await». Конечно, это можно сделать без «ожидания», но в итоге у вас будет куча делегатов и вы потеряете последовательный внешний вид кода (а также возможность использовать usingи try/catch...
Питер Ричи
1
@PeterRitchie Вы можете иметь метод, который является функционально асинхронным, не использует awaitключевое слово и не использует asyncключевое слово, но нет никакого смысла в использовании asyncключевого слова без его использования awaitв определении этого метода.
Serv
1
@PeterRitchie Из заявления: « asyncэто немного больше, чем просто« ожидание »результата». asyncКлючевые слова (вы подразумевал это ключевое слово, заключив его в обратных кавычках) означает ничего более , чем в ожидании результата. Это асинхронность, как и общие концепции CS, означает больше, чем просто ожидание результата.
Serv
1
@Servy asyncсоздает конечный автомат, который управляет любыми ожиданиями в асинхронном методе. Если awaitв методе нет s, он все равно создает этот конечный автомат, но метод не асинхронный. И если asyncметод возвращается void, ждать нечего. Таким образом, это больше, чем просто ожидание результата.
Питер Ричи
1
@PeterRitchie Пока метод возвращает задачу, вы можете ее ждать. Там нет необходимости для конечного автомата, или asyncключевое слово. Все, что вам нужно сделать (и все, что действительно происходит в конечном итоге с конечным автоматом в этом особом случае), - это то, что метод запускается синхронно, а затем включается в завершенную задачу. Полагаю, технически вы не просто удаляете конечный автомат; вы удаляете конечный автомат и затем звоните Task.FromResult. Я предположил, что вы (а также авторы компиляторов) можете добавить приложение самостоятельно.
Serv
0

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

public void WaitForTask(Task task)
{
    DispatcherFrame frame = new DispatcherFrame();
    task.ContinueWith(t => frame.Continue = false));
    Dispatcher.PushFrame(frame);
}

Этот подход аналогичен блокировке на ShowDialog и по-прежнему поддерживает отзывчивость пользовательского интерфейса.

haimb
источник
0

Я опаздываю на вечеринку, но я использую потрясающую библиотеку, на которую я не видел ссылок в других ответах

https://github.com/brminnick/AsyncAwaitBestPractices

Если вам нужно «выстрелить и забыть», вы вызываете метод расширения для задачи.

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

В вашем примере вы бы использовали это так:

   public string GetStringData()
    {
        MyAsyncMethod().SafeFireAndForget(onException: (exception) =>
                    {
                      //DO STUFF WITH THE EXCEPTION                    
                    }); 
        return "hello world";
    }

Это также дает ожидаемые AsyncCommands, реализующие ICommand из коробки, которая отлично подходит для моего решения MVVM Xamarin

Адам Диамент
источник
-1

Решение - запустить HttpClient в другую задачу выполнения без контекста sincronization:

var submit = httpClient.PostAsync(uri, new StringContent(body, Encoding.UTF8,"application/json"));
var t = Task.Run(() => submit.ConfigureAwait(false));
await t.ConfigureAwait(false);
Эрнесто Гарсия
источник
-1

Если вы действительно хотите это сделать. Просто для того, чтобы обратиться к «Вызов асинхронного метода в C # без ожидания», вы можете выполнить асинхронный метод внутри a Task.Run. Этот подход будет ждать до MyAsyncMethodконца.

public string GetStringData()
{
    Task.Run(()=> MyAsyncMethod()).Result;
    return "hello world";
}

awaitасинхронно разворачивает Resultвашу задачу, тогда как простое использование Result блокирует, пока задача не будет завершена.

CharithJ
источник
-1

Обычно асинхронный метод возвращает класс Task. Если вы используете Wait()метод или Resultсвойство и код генерирует исключение - в него включается тип исключения AggregateException- тогда вам нужно выполнить запрос, Exception.InnerExceptionчтобы найти правильное исключение.

Но это также можно использовать .GetAwaiter().GetResult()вместо этого - оно также будет ожидать асинхронную задачу, но не будет переносить исключение.

Итак, вот короткий пример:

public async Task MyMethodAsync()
{
}

public string GetStringData()
{
    MyMethodAsync().GetAwaiter().GetResult();
    return "test";
}

Возможно, вы захотите также иметь возможность возвращать некоторый параметр из асинхронной функции - это может быть достигнуто путем предоставления дополнительного Action<return type>в асинхронную функцию, например, так:

public string GetStringData()
{
    return MyMethodWithReturnParameterAsync().GetAwaiter().GetResult();
}

public async Task<String> MyMethodWithReturnParameterAsync()
{
    return "test";
}

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

TarmoPikaro
источник