Почему не все функции должны быть асинхронными по умолчанию?

Шаблон async-await.net 4.5 - это изменение парадигмы. Это почти слишком хорошо, чтобы быть правдой.

Я переносил некоторый IO-тяжелый код в async-wait, потому что блокирование ушло в прошлое.

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

Изменение функций для async - несколько повторяющаяся и невообразимая работа. Бросьте ключевое слово async в объявление, оберните возвращаемое значение Task<>, и вы в значительной степени выполнили. Это довольно тревожно, насколько легко весь процесс, и довольно скоро текстовое замещение script автоматизирует большую часть "портирования" для меня.

И теперь вопрос.. Если весь мой код медленно поворачивает async, почему бы просто не сделать все асинхронным по умолчанию?

Очевидная причина, по которой я предполагаю, - это производительность. Async-await имеет свои служебные данные и код, который не должен быть асинхронным, предпочтительно не должен. Но если производительность является единственной проблемой, то некоторые умные оптимизации могут автоматически удалить служебные данные, когда это не понадобится. Я читал об оптимизации "быстрый путь" , и мне кажется, что только он должен позаботиться об этом.

Возможно, это сопоставимо с сдвигом парадигмы, вызванным сборщиками мусора. В первые дни GC освобождение собственной памяти было определенно более эффективным. Но массы по-прежнему выбрали автоматическую сборку в пользу более безопасного, более простого кода, который может быть менее эффективным (и даже это, возможно, больше не соответствует действительности). Может быть, это должно быть здесь? Почему не все функции должны быть асинхронными?

Ответ 1

Прежде всего, спасибо за ваши добрые слова. Это действительно потрясающая особенность, и я рад, что был ее небольшой частью.

Если весь мой код медленно меняет async, почему бы просто не сделать все асинхронным по умолчанию?

Ну, вы преувеличиваете; все ваш код не будет асинхронным. Когда вы добавляете два "простых" целых числа вместе, вы не ожидаете результата. Когда вы добавляете два будущих целых числа вместе, чтобы получить третье будущее целое число - поскольку это то, что Task<int>, это целое число, к которому вы собираетесь получить доступ в будущем - конечно, вы, скорее всего, ожидаете результата.

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

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

В теории теория и практика схожи. На практике они никогда не бывают.

Позвольте мне дать вам три момента против такого рода преобразований, за которыми следует пропуск оптимизации.

Первая точка снова: async в С#/VB/F # по существу является ограниченной формой продолжения передачи. Огромное количество исследований в сообществе функциональных языков стало выяснять способы определения того, как оптимизировать код, который сильно использует стиль продолжения прохождения. Команда компилятора, вероятно, должна будет решить очень похожие проблемы в мире, где "асинхронный" был значением по умолчанию, а не-асинхронные методы должны были быть идентифицированы и деасинхронными. Команда С# не очень заинтересована в том, чтобы принимать открытые исследовательские проблемы, так что там есть большие очки.

Второй момент - это то, что С# не имеет уровня "ссылочной прозрачности", который делает эти виды оптимизаций более доступными. Под "ссылочной прозрачностью" я подразумеваю свойство, что значение выражения не зависит от того, когда оно оценивается. Выражения типа 2 + 2 являются ссылочно прозрачными; вы можете выполнить оценку во время компиляции, если хотите, или отложить ее до времени выполнения и получить тот же ответ. Но выражение типа x+y не может перемещаться во времени, потому что x и y могут меняться со временем.

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().)

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

Третий момент против вас заключается в том, что вы должны спросить: "Почему такая асинхронная особенность?" Если вы собираетесь утверждать, что каждая операция должна быть Task<T>, тогда вам нужно ответить на вопрос "почему бы не Lazy<T>?" или "почему бы не Nullable<T>?" или "почему бы не IEnumerable<T>?" Потому что мы могли бы так же легко сделать это. Почему бы не так, чтобы каждая операция была отменена до нуля? Или каждая операция выполняется с ленивым вычислением, а результат кэшируется позже, или результат каждой операции представляет собой последовательность значений, а не только одно значение. Затем вам нужно попытаться оптимизировать ситуации, когда вы знаете "о, это никогда не должно быть нулевым, поэтому я могу генерировать лучший код" и т.д. (И на самом деле компилятор С# делает это для поднятой арифметики.)

Точка: мне непонятно, что Task<T> на самом деле является особенным, чтобы оправдать эту большую работу.

Если вас интересуют такие вещи, я рекомендую вам изучить функциональные языки, такие как Haskell, которые имеют гораздо более сильную ссылочную прозрачность и разрешают все виды оценки вне порядка и делают автоматическое кэширование. Haskell также имеет гораздо более сильную поддержку в своей системе типов для своих "монадических подъемов", о которых я упоминал.

Ответ 2

Почему не все функции должны быть async?

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

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

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

Кроме того, существует множество подпрограмм, которые по своей природе не асинхронны. Они имеют больший смысл в качестве синхронных операций. Принуждение Math.Sqrt к Task<double> Math.SqrtAsync было бы смешно, например, поскольку нет никакой причины для того, чтобы быть асинхронным. Вместо того, чтобы async проталкивать ваше приложение, вы можете повредить await.

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

Если вы выполняете большую работу по привязке к IO, вы обнаружите, что использование async повсеместно является отличным дополнением, многие из ваших подпрограмм будут async. Однако, когда вы начинаете выполнять работу с ЦП, в общем, сделать вещи async на самом деле не очень хорошо - это скрывает тот факт, что вы используете циклы процессора под API, который выглядит как асинхронный, но на самом деле не обязательно по-настоящему асинхронный.

Ответ 3

Производительность в стороне - Async может иметь производительность. На клиенте (WinForms, WPF, Windows Phone) это благо для производительности. Но на сервере или в других сценариях, отличных от UI, вы платите производительность. Вы, конечно же, не хотите, чтобы по умолчанию асинхронно. Используйте его, когда вам нужны преимущества масштабируемости.

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

Ответ 4

Я считаю, что есть веская причина сделать все методы асинхронными, если они не нужны - расширяемость. Выборочные методы создания async работают только в том случае, если ваш код никогда не развивается, и вы знаете, что метод A() всегда связан с ЦП (вы поддерживаете его синхронизацию), а метод B() всегда связан с I/O (вы отмечаете его как aync).

Но что, если что-то изменится? Да, A() выполняет вычисления, но в какой-то момент в будущем вам нужно было добавить туда журнал или отчет или пользовательский обратный вызов с реализацией, которая не может предсказать, или алгоритм был расширен и теперь включает в себя не только вычисления ЦП, но и также некоторые операции ввода-вывода? Вам нужно будет преобразовать метод в async, но это сломает API, и все вызывающие его элементы будут необходимы для обновления (и они могут даже быть разными приложениями от разных поставщиков). Или вам нужно добавить асинхронную версию вместе с версией синхронизации, но это не имеет большого значения - использование версии синхронизации блокируется и, следовательно, вряд ли приемлемо.

Было бы здорово, если бы можно было сделать существующий метод синхронизации async без изменения API. Но в действительности у нас нет такого варианта, я считаю, и использую асинхронную версию, даже если она в настоящее время не нужна, это единственный способ гарантировать, что вы никогда не столкнетесь с проблемами совместимости в будущем.