Прежде всего, спасибо за добрые слова. Это действительно потрясающая функция, и я рад, что был ее небольшой частью.
Если весь мой код медленно превращается в асинхронный, почему бы просто не сделать его асинхронным по умолчанию?
Что ж, вы преувеличиваете; весь ваш код не становится асинхронным. Когда вы складываете два «простых» целых числа вместе, вы не ждете результата. Когда вы складываете два будущих целых числа вместе, чтобы получить третье будущее целое число - потому что Task<int>
это целое число, к которому вы собираетесь получить доступ в будущем - конечно, вы, вероятно, будете ждать результата.
Основная причина не делать все асинхронным, заключается в том, что цель async / await - упростить написание кода в мире с множеством операций с высокой задержкой . Подавляющее большинство ваших операций не связаны с высокой задержкой, поэтому нет никакого смысла снижать производительность, уменьшающую эту задержку. Скорее, некоторые из ваших ключевых операций связаны с высокой задержкой, и эти операции вызывают заражение асинхронным кодом зомби.
если производительность является единственной проблемой, конечно, некоторые умные оптимизации могут автоматически удалить накладные расходы, когда они не нужны.
Теоретически теория и практика похожи. На практике это не так.
Позвольте мне дать вам три очка против такого рода преобразований с последующим этапом оптимизации.
Первый момент снова: async в C # / VB / F # по сути является ограниченной формой передачи продолжения . Огромное количество исследований в сообществе функциональных языков было направлено на выяснение способов определения того, как оптимизировать код, который интенсивно использует стиль передачи продолжения. Команде компилятора, вероятно, придется решать очень похожие проблемы в мире, где "асинхронный" был по умолчанию, а неасинхронные методы должны были быть идентифицированы и деасинхронизированы. Команда C # на самом деле не заинтересована в решении открытых исследовательских задач, так что это большой аргумент против.
Второй аргумент против - то, что C # не имеет уровня "ссылочной прозрачности", который делает такие виды оптимизации более управляемыми. Под «ссылочной прозрачностью» я подразумеваю свойство, от которого значение выражения не зависит, когда оно оценивается . Выражения вроде 2 + 2
ссылочно прозрачны; вы можете выполнить оценку во время компиляции, если хотите, или отложить ее до времени выполнения и получить тот же ответ. Но такое выражение, как x+y
нельзя перемещать во времени, потому что x и y могут меняться со временем .
Async значительно усложняет рассуждение о том, когда произойдет побочный эффект. Перед async, если вы сказали:
M();
N();
и M()
был void M() { Q(); R(); }
, и N()
был void N() { S(); T(); }
, и R
и S
вызывать побочные эффекты, то вы знаете , что побочный эффект R, случается , прежде чем побочный эффект S в. Но если да, async void M() { await Q(); R(); }
то вдруг это вылетает из окна. У вас нет гарантии, R()
произойдет ли это до или после S()
(если, конечно, этого M()
не ждут; но, конечно, этого Task
не нужно ждать до конца N()
).
Теперь представьте, что это свойство больше не знать, в каком порядке происходят побочные эффекты, применяется ко всем частям кода в вашей программе, кроме тех, которые оптимизатору удается деасинхронизировать. По сути, вы больше не имеете ни малейшего представления о том, какие выражения будут оцениваться в каком порядке, а это означает, что все выражения должны быть ссылочно прозрачными, что сложно в таком языке, как C #.
Третий аргумент против заключается в том, что вы должны спросить: «Почему асинхронный режим такой особенный?» Если вы собираетесь утверждать, что каждая операция должна быть на самом деле, Task<T>
тогда вы должны быть в состоянии ответить на вопрос «почему бы и нет Lazy<T>
?». или "почему бы и нет Nullable<T>
?" или "почему бы и нет IEnumerable<T>
?" Потому что мы могли бы сделать это так же легко. Почему не должно быть так, чтобы каждая операция была отменена до нулевого значения ? Либо каждая операция вычисляется лениво, а результат кэшируется для дальнейшего использования , либо результатом каждой операции является последовательность значений, а не одно значение . Затем вы должны попытаться оптимизировать те ситуации, когда вы знаете: «О, это никогда не должно быть нулевым, поэтому я могу сгенерировать лучший код» и так далее.
Дело в том, что мне не ясно, что Task<T>
на самом деле является настолько особенным, чтобы оправдать такую большую работу.
Если вас интересуют подобные вещи, я рекомендую вам изучить функциональные языки, такие как Haskell, которые имеют гораздо более сильную ссылочную прозрачность и допускают все виды неупорядоченной оценки и автоматическое кэширование. Haskell также имеет гораздо более сильную поддержку в своей системе типов для видов «монадических лифтингов», о которых я говорил.