В чем разница между возвратом void и возвратом задачи?

128

Просматривая различные образцы C # Async CTP, я вижу, что одни асинхронные функции возвращают void, а другие возвращают неуниверсальные Task. Я могу понять, почему возвращение a Task<MyType>полезно для возврата данных вызывающей стороне после завершения асинхронной операции, но функции, которые я видел, которые имеют тип Taskвозврата, никогда не возвращают никаких данных. Почему бы не вернуться void?

Джеймс Кэдд
источник

Ответы:

214

Ответы SLaks и Killercam хороши; Я подумал, что просто добавлю немного больше контекста.

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

Метод, отмеченный как asyncможет возвращать void, Taskили Task<T>. В чем разница между ними?

Можно Task<T>ожидать возврата асинхронного метода, и когда задача завершится, он предложит T.

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

voidВозвращения метод асинхронной не может ожидаться; это метод «выстрелил и забыл». Он работает асинхронно, и вы не можете сказать, когда это будет сделано. Это более чем немного странно; как говорит SLaks, обычно вы делаете это только при создании асинхронного обработчика событий. Событие запускается, обработчик выполняет; никто не собирается «ждать» задачи, возвращаемой обработчиком событий, потому что обработчики событий не возвращают задачи, и даже если бы они это сделали, какой код будет использовать задачу для чего-то? Обычно это не код пользователя, который в первую очередь передает управление обработчику.

Ваш второй вопрос в комментарии по существу касается того, что можно awaitредактировать:

Какие методы можно использовать await? Можно ли редактировать метод, возвращающий пустоту await?

Нет, нельзя ожидать метода возврата void. Компилятор преобразует await M()в вызов M().GetAwaiter(), где GetAwaiterможет быть метод экземпляра или метод расширения. Ожидаемое значение должно быть таким, для которого вы можете получить ожидающего; очевидно, что метод, возвращающий пустоту, не дает значения, из которого вы можете получить ожидающего.

TaskМетоды возврата могут производить ожидаемые значения. Мы ожидаем, что третьи стороны захотят создать свои собственные реализации Task-подобных объектов, которые можно ожидать, и вы сможете их ожидать. Однако вы не сможете объявлять asyncметоды, возвращающие что-либо, кроме void, Taskили Task<T>.

(ОБНОВЛЕНИЕ: мое последнее предложение может быть фальсифицировано будущей версией C #; есть предложение разрешить типы возврата, отличные от типов задач, для асинхронных методов.)

(ОБНОВЛЕНИЕ: упомянутая выше функция вошла в C # 7.)

Эрик Липперт
источник
7
+1 Думаю, не хватает только разницы в том, как обрабатываются исключения в асинхронных методах с возвратом пустоты.
Жуан Анджело
10
@JamesCadd: предположим, что некоторая асинхронная работа вызывает исключение. Кто это ловит? Код, запускающий асинхронную задачу, больше не находится в стеке - он может даже не находиться в том же потоке - и исключения предполагают, что все блоки catch / finally находятся в стеке . Ну так что ты делаешь? Мы храним информацию об исключении в Задаче, чтобы вы могли проверить ее позже. Но если метод возвращает void, значит, для пользовательского кода нет Задачи. Как именно мы справляемся с этой ситуацией, было предметом некоторых разногласий, и в настоящий момент я не помню, что мы решили.
Эрик Липперт
8
Я задал этот вопрос Стивену Тубу из BUILD. В .NET 4.0 ненаблюдаемые необработанные исключения в задачах в конечном итоге приводят к сбою процесса, как только TPL обнаруживает, что они не наблюдаются. В 4.5 они изменили поведение по умолчанию, так что о ненаблюдаемых исключениях по-прежнему будет сообщаться через событие TaskScheduler :: UnobservedTaskException, но процесс больше не будет аварийным. Если вам нужно старое поведение 4.0, вы можете вернуться к нему с помощью <runtime> <ThrowUnobservedTaskExceptions enabled = "true" /> </runtime>. Скорее всего, изменение было сделано как раз для поддержки метода «запустил и забыл» для void async-методов.
Дрю Марш
4
async voidметоды вызывают свое исключение для того, SynchronizationContextчто было активным в момент начала выполнения. Это похоже на поведение (синхронных) обработчиков событий. @DrewMarsh: параметр UnobservedTaskExceptionвремени выполнения и применяется только к методам асинхронной задачи «выстрелил и забыл» , но не к async voidметодам.
Стивен Клири
1
Ссылка для цитирования для информации об обработке асинхронных исключений: blogs.msdn.com/b/pfxteam/archive/2012/04/12/10293335.aspx#11
Люк Пуплетт
23

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

Фактически, единственная причина для возврата void- это если вы не можете вернуться, Taskпотому что вы пишете обработчик событий.

SLaks
источник
Я думал, что можно ожидать методы, которые также возвращают тип void - не могли бы вы немного уточнить?
Джеймс Кэдд
1
Нет, не можешь. Если метод возвращается void, у вас нет возможности добраться до задачи, которую он генерирует. (На самом деле, я не уверен, генерирует ли он вообще a Task)
SLaks
18

Методы возвращаются Taskи Task<T>являются составными - это означает, что вы можете использовать awaitих внутри asyncметода.

asyncВозвращаемые методы voidнельзя компоновать, но у них есть два других важных свойства:

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

Второй момент важен, когда вы имеете дело с контекстом, который поддерживает количество невыполненных асинхронных операций.

Контекст ASP.NET - один из таких контекстов; если вы используете асинхронные Taskметоды, не ожидая их от асинхронного voidметода, то запрос ASP.NET будет выполнен слишком рано.

Другой контекст - это то, что AsyncContextя написал для модульного тестирования (доступно здесь ) - AsyncContext.Runметод отслеживает количество невыполненных операций и возвращает его, когда оно равно нулю.

Стивен Клири
источник
12

Тип Task<T>- это тип рабочей лошадки для параллельной библиотеки задач (TPL), он представляет собой концепцию «некоторой работы / задания, которая в будущем приведет к результату типа T». Концепция «работа, которая будет завершена в будущем, но не вернет результата» представлена ​​неуниверсальным типом Task.

Как именно Tдолжен быть получен результат типа, и детали реализации конкретной задачи; работа может быть передана другому процессу на локальной машине, другому потоку и т. д. Задачи TPL обычно передаются рабочим потокам из пула потоков в текущем процессе, но эта деталь реализации не является фундаментальной для Task<T>типа; скорее a Task<T>может представлять любую операцию с высокой задержкой, которая создает T.

На основании вашего комментария выше:

В awaitвыражение означает «вычислить это выражение , чтобы получить объект , представляющий работу , что в будущем будет производить результат. Зарегистрироваться остаток текущего метода в качестве обратного вызова , связанного с продолжением этой задачи. После того, как эта задача производится и обратного вызова зарегистрирован, немедленно верните управление моему абоненту ". Это противоположно / в отличие от обычного вызова метода, что означает «запомните, что вы делаете, запустите этот метод, пока он не будет полностью завершен, а затем продолжите с того места, где вы остановились, теперь зная результат метода».


Изменить: я должен процитировать статью Эрика Липперта в журнале MSDN за октябрь 2011 года, так как это было большим подспорьем для меня в понимании этого материала.

Дополнительную информацию и белые страницы можно найти здесь .

Я надеюсь, что это поможет.

Moonknight
источник