async / await - когда вернуть Task void void?


503

По каким сценариям можно использовать

public async Task AsyncMethod(int num)

вместо

public async void AsyncMethod(int num)

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

Кроме того, в следующем методе ненужны ли ключевые слова async и await?

public static async void AsyncMethod2(int num)
{
    await Task.Factory.StartNew(() => Thread.Sleep(num));
}

20
Обратите внимание , что методы должны асинхронные всегда быть суффиксом с именем Async.Example Foo()стал бы FooAsync().
Фред

30
@ Фред В основном, но не всегда. Это всего лишь соглашение, и принятые исключения из этого соглашения относятся к классам на основе событий или контрактам интерфейса, см. MSDN . Например, вы не должны переименовывать обычные обработчики событий, такие как Button1_Click.
Бен

14
Просто примечание, которое вы не должны использовать Thread.Sleepс вашими задачами, вы должны await Task.Delay(num)вместо этого
Боб Вейл

45
@fred Я не согласен с этим, IMO, добавляющий асинхронный суффикс, должен использоваться только тогда, когда вы предоставляете интерфейс с параметрами синхронизации и асинхронности. Smurf называть вещи асинхронными, когда есть только одно намерение, бессмысленно. Дело Task.Delayне в этом, так Task.AsyncDelayкак все методы в задании являются асинхронными
не любили

11
У меня была интересная проблема этим утром с методом контроллера webapi 2, он был объявлен как async voidвместо этого async Task. Сбой метода из-за того, что он использовал объект контекста Entity Framework, объявленный как член контроллера, был удален до завершения выполнения метода. Фреймворк располагал контроллер до завершения его метода. Я изменил метод асинхронной задачи, и это сработало.
Коста

Ответы:


417

1) Обычно вы хотите вернуть Task. Основное исключение должно быть, когда вам нужно иметь voidтип возвращаемого значения (для событий). Если нет никаких причин, чтобы запретить вызывающему абоненту выполнять awaitвашу задачу, зачем это запрещать?

2) asyncметоды, которые возвращают, voidявляются специальными в другом аспекте: они представляют асинхронные операции верхнего уровня и имеют дополнительные правила, которые вступают в действие, когда ваша задача возвращает исключение. Проще всего показать разницу с помощью примера:

static async void f()
{
    await h();
}

static async Task g()
{
    await h();
}

static async Task h()
{
    throw new NotImplementedException();
}

private void button1_Click(object sender, EventArgs e)
{
    f();
}

private void button2_Click(object sender, EventArgs e)
{
    g();
}

private void button3_Click(object sender, EventArgs e)
{
    GC.Collect();
}

fисключение всегда "соблюдается". Исключение, которое оставляет асинхронный метод верхнего уровня, просто обрабатывается как любое другое необработанное исключение. gисключение никогда не наблюдается. Когда сборщик мусора приходит для очистки задачи, он видит, что задача вызвала исключение, и никто не обработал исключение. Когда это происходит, TaskScheduler.UnobservedTaskExceptionобработчик запускается. Никогда не позволяй этому случиться. Чтобы использовать ваш пример,

public static async void AsyncMethod2(int num)
{
    await Task.Factory.StartNew(() => Thread.Sleep(num));
}

Да, использовать asyncиawait здесь, они гарантируют, что ваш метод все еще работает правильно, если выброшено исключение.

Для получения дополнительной информации см .: http://msdn.microsoft.com/en-us/magazine/jj991977.aspx


10
Я имел в виду, fа не gв своем комментарии. Исключение из fпередается в SynchronizationContext. gподнимет UnobservedTaskException, но UTEбольше не завершит процесс, если он не обработан. В некоторых ситуациях допустимо иметь такие «асинхронные исключения», которые игнорируются.
Стивен Клири

3
Если у вас есть WhenAnyнесколько Tasks, что приводит к исключениям. Вам часто приходится иметь дело только с первым, и вы часто хотите игнорировать других.
Стивен Клири

1
@StephenCleary Спасибо, я думаю, это хороший пример, хотя это зависит от причины, по которой вы звоните WhenAnyв первую очередь, можно ли игнорировать другие исключения: основной сценарий использования, который у меня есть, все равно заканчивается ожиданием оставшихся задач, когда завершается любая с или без исключения.

10
Меня немного смущает вопрос, почему вы рекомендуете возвращать задание вместо void. Как вы сказали, f () будет генерировать исключение, а g () - нет. Не лучше ли быть осведомленным об этих исключениях фоновых потоков?
user981225

2
@ user981225 Действительно, но это становится обязанностью вызывающей стороны g: каждый метод, вызывающий g, должен быть асинхронным и также использовать await. Это руководство, а не жесткое правило, вы можете решить, что в вашей конкретной программе легче вернуть g return void.

40

Я сталкивалась с этим очень полезно статей о asyncи voidнаписала Жером Лавану: https://jaylee.org/archive/2012/07/08/c-sharp-async-tips-and-tricks-part-2-async-void .html

Суть в том, что это async+voidможет привести к сбою системы и обычно должно использоваться только в обработчиках событий на стороне пользовательского интерфейса.

Причиной этого является контекст синхронизации, используемый AsyncVoidMethodBuilder, который в этом примере отсутствует. При отсутствии окружающего контекста синхронизации любое исключение, которое не обрабатывается телом асинхронного пустого метода, повторно вызывается в ThreadPool. Хотя, по-видимому, нет другого логического места, где можно было бы генерировать такого рода необработанное исключение, неудачный эффект заключается в том, что процесс завершается, потому что необработанные исключения в ThreadPool эффективно завершают процесс начиная с .NET 2.0. Вы можете перехватить все необработанные исключения с помощью события AppDomain.UnhandledException, но нет способа восстановить процесс из этого события.

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


Это верно? Я думал, что исключения внутри асинхронных методов фиксируются в Задаче. Кроме того, всегда есть глухой SynchronizationContext
NM

30

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

  1. Асинхронные методы void имеют различную семантику обработки ошибок. Когда исключение выбрасывается из метода асинхронной задачи или асинхронной задачи, это исключение захватывается и помещается в объект задачи. При использовании асинхронных void-методов объект Task отсутствует, поэтому любые исключения, выбрасываемые из асинхронного void-метода, будут вызываться непосредственно в SynchronizationContext (SynchronizationContext представляет код местоположения «где» может выполняться.), Который был активен, когда асинхронный void-метод начал

Исключения из асинхронного метода Void нельзя обнаружить с помощью Catch

private async void ThrowExceptionAsync()
{
  throw new InvalidOperationException();
}
public void AsyncVoidExceptions_CannotBeCaughtByCatch()
{
  try
  {
    ThrowExceptionAsync();
  }
  catch (Exception)
  {
    // The exception is never caught here!
    throw;
  }
}

Эти исключения можно наблюдать с помощью AppDomain.UnhandledException или аналогичного события перехвата для приложений GUI / ASP.NET, но использование этих событий для регулярной обработки исключений является рецептом для невозможности обслуживания (он приводит к сбою приложения).

  1. Асинхронные методы void имеют различную семантику создания. Асинхронные методы, возвращающие Task или Task, могут быть легко составлены с использованием await, Task.WhenAny, Task.WhenAll и так далее. Асинхронные методы, возвращающие void, не обеспечивают простой способ уведомить вызывающий код о том, что они завершили. Легко запустить несколько асинхронных пустых методов, но нелегко определить, когда они закончили. Асинхронные методы void будут уведомлять свой SynchronizationContext при запуске и завершении, но пользовательский SynchronizationContext является сложным решением для обычного кода приложения.

  2. Метод Async Void полезен при использовании синхронного обработчика событий, потому что они вызывают свои исключения непосредственно в SynchronizationContext, что аналогично тому, как ведут себя синхронные обработчики событий.

Для получения более подробной информации перейдите по этой ссылке https://msdn.microsoft.com/en-us/magazine/jj991977.aspx


21

Проблема с вызовом async void заключается в том, что вы даже не получаете задачу обратно, у вас нет возможности узнать, когда задача функции была выполнена (см. Https://blogs.msdn.microsoft.com/oldnewthing/20170720-00/ ? p = 96655 )

Вот три способа вызова асинхронной функции:

async Task<T> SomethingAsync() { ... return t; }
async Task SomethingAsync() { ... }
async void SomethingAsync() { ... }

Во всех случаях функция превращается в цепочку задач. Разница в том, что возвращает функция.

В первом случае функция возвращает задачу, которая в конечном итоге производит t.

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

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

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


6

Я думаю, что вы также можете использовать async voidдля запуска фоновых операций, если вы будете осторожны, чтобы ловить исключения. Мысли?

class Program {

    static bool isFinished = false;

    static void Main(string[] args) {

        // Kick off the background operation and don't care about when it completes
        BackgroundWork();

        Console.WriteLine("Press enter when you're ready to stop the background operation.");
        Console.ReadLine();
        isFinished = true;
    }

    // Using async void to kickoff a background operation that nobody wants to be notified about when it completes.
    static async void BackgroundWork() {
        // It's important to catch exceptions so we don't crash the appliation.
        try {
            // This operation will end after ten interations or when the app closes. Whichever happens first.
            for (var count = 1; count <= 10 && !isFinished; count++) {
                await Task.Delay(1000);
                Console.WriteLine($"{count} seconds of work elapsed.");
            }
            Console.WriteLine("Background operation came to an end.");
        } catch (Exception x) {
            Console.WriteLine("Caught exception:");
            Console.WriteLine(x.ToString());
        }
    }
}

1
Проблема с вызовом async void заключается в том, что вы даже не получаете задачу обратно, у вас нет возможности узнать, когда задача функции была выполнена
user8128167

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

Исключения из метода асинхронной пустоты нельзя обнаружить с помощью Catch. msdn.microsoft.com/en-us/magazine/jj991977.aspx
Si Zi

3
@SiZi выгода - ВНУТРИ асинхронного пустого метода, как показано в примере, и он пойман.
bboyle1234

Как насчет того, когда ваши требования изменяются, чтобы требовать не выполнять работу, если вы не можете зарегистрировать это - тогда вам нужно изменить сигнатуры во всем API, а не просто изменить логику, которая в настоящее время имеет // Игнорировать исключение
Милни

0

Мой ответ прост, вы не можете ждать пустого метода

Error   CS4008  Cannot await 'void' TestAsync   e:\test\TestAsync\TestAsyncProgram.cs

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


-2

Согласно документации Microsoft , НИКОГДА не следует использоватьasync void

Не делайте этого: в следующем примере используется async voidвыполнение HTTP-запроса при достижении первого ожидания:

  • Что ВСЕГДА плохая практика в приложениях ASP.NET Core.

  • Получает доступ к HttpResponse после завершения HTTP-запроса.

  • Сбой процесса.

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.