Почему не все функции должны быть асинхронными по умолчанию?
The async-await шаблон .net 4.5 меняется парадигма. Это слишком хорошо, чтобы быть правдой.
я портировал некоторый IO-тяжелый код на async-await, потому что блокировка осталась в прошлом.
довольно много людей сравнивают async-await с заражением зомби, и я обнаружил, что это довольно точно. Асинхронный код любит другой асинхронный код (вам нужна асинхронная функция для ожидания асинхронной функции). Таким образом, все больше и больше функций становятся асинхронными и это продолжает расти в коде.
изменение функций на асинхронные-это несколько повторяющаяся и лишенная воображения работа. Бросьте async ключевое слово в объявлении, оберните возвращаемое значение Task<> и вы в значительной степени сделали. Это довольно тревожно, насколько легко весь процесс, и довольно скоро сценарий замены текста автоматизирует большую часть "портирования" для меня.
а теперь вопрос.. Если весь мой код медленно превращается в асинхронный, почему бы просто не сделать все это асинхронным по умолчанию?
очевидная причина, которую я предполагаю, это производительность. Async-await имеет свои накладные расходы и код, который не должен быть асинхронным, предпочтительно не должен. но если производительность является единственной проблемой, конечно, некоторые умные оптимизации могут автоматически удалять накладные расходы, когда это не нужно. Я читал о "быстрый путь" оптимизация, и мне кажется, что он сам должен заботиться о большинстве из них.
возможно, это сопоставимо с изменением парадигмы, вызванным сборщики мусора. В первые дни GC освобождение собственной памяти было определенно более эффективным. Но массы все еще выбрали автоматическую сборку в пользу более безопасного, более простого кода, который может быть менее эффективным (и даже это, возможно, уже не так). Может быть, это должно быть здесь? Почему не все функции должны быть асинхронными?
4 ответов:
во-первых, спасибо за ваши добрые слова. Это действительно удивительная особенность, и я рад, что был небольшой частью этого.
если весь мой код медленно превращается в асинхронный, почему бы просто не сделать все это асинхронным по умолчанию?
Ну, ты преувеличиваешь; все ваш код не асинхронный. Когда вы добавляете два "простых" целых числа вместе, вы не ждете результата. Когда вы добавляете два будущие целые числа вместе, чтобы получить третий будущее целого числа -- потому что
Task<int>это целое число, к которому вы получите доступ в будущем-конечно, вы, вероятно, будете ждать результата.основная причина не делать все асинхронно, потому что цель async / await-упростить написание кода в мире с большим количеством операций с высокой задержкой. Подавляющее большинство ваших операций не высокая латентность, поэтому она не делает никакие смысл взять хит производительности, который смягчает эту задержку. Скорее,ключ несколько ваши операции имеют высокую задержку, и эти операции вызывают заражение зомби асинхронностью во всем коде.
если производительность является единственной проблемой, конечно, некоторые умные оптимизации могут удалить накладные расходы автоматически, когда это не нужно.
в теории теория и практика одинаковы. На практике они никогда являются.
позвольте мне дать вам три очка против такого рода трансформации с последующим оптимизация.
первый пункт снова: асинхронность в C#/VB / F# по существу является ограниченной формой продолжение прохождения. Огромное количество исследований в функциональном языковом сообществе ушло на выяснение способов определения того, как оптимизировать код, который интенсивно использует стиль передачи продолжения. Команда компилятора, вероятно, придется решать очень похожие проблемы в мире, где "асинхронный" был по умолчанию и не-async-методов должны быть выявлены и де-асинхронный-маньяков. Команда C# на самом деле не заинтересована в решении открытых исследовательских проблем, так что это большие очки против прямо там.
второй момент заключается в том, что C# не имеет уровня "ссылочной прозрачности", который делает эти виды оптимизации более сговорчивыми. Под "ссылочной прозрачностью" я подразумеваю свойство, которое значение выражения не зависит от того, когда он оценивается. Выражения типа
2 + 2являются ссылочно прозрачными; вы можете выполнить оценку во время компиляции, если хотите, или отложить ее до времени выполнения и получить тот же ответ. Но такое выражение какx+yне может быть перемещен во времени, потому что x и y могут меняться с течением времени.асинхронность делает его гораздо труднее рассуждать о том, когда будет побочный эффект. Перед асинхронностью, если вы сказали:
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>?"Потому что мы могли это сделать. Почему это не должно быть так, что каждая операция отменяется? Или каждая операция лениво вычисляется и кэшируется для последующего или результатом каждой операции является последовательность значений, а не только одно значение. Тогда вам придется попробуйте оптимизировать те ситуации, когда вы знаете: "о, это никогда не должно быть null, поэтому я могу генерировать лучший код" и так далее. (И на самом деле компилятор C# делает это для поднятой арифметики.)суть в том, что мне не ясно, что
Task<T>на самом деле это особенное, чтобы гарантировать такую большую работу.если вас интересуют такие вещи, я рекомендую вам исследовать функциональные языки, такие как Haskell, которые имеют гораздо более сильную ссылочную прозрачность и позволяют все виды внеплановой оценки и автоматического кэширования. Haskell также имеет гораздо более сильную поддержку в своей системе типов для видов "монадических подъемов", о которых я упоминал.
почему не все функции должны быть асинхронными?
производительность является одной из причин, как вы упомянули. Обратите внимание, что параметр" быстрый путь", с которым вы связаны, улучшает производительность в случае выполненной задачи, но по-прежнему требует гораздо больше инструкций и накладных расходов по сравнению с одним вызовом метода. Таким образом, даже с "быстрым путем" на месте, вы добавляете много сложности и накладных расходов с каждым вызовом асинхронного метода.
совместимость, как кроме того, совместимость с другими языками (включая сценарии взаимодействия) также станет проблематичной.
другой вопрос сложности и направленности. Асинхронные операции добавляют сложности - во многих случаях функции языка скрывают это, но есть много случаев, когда методы создания
asyncопределенно добавляет сложности в их использовании. Это особенно верно, если у вас нет контекста синхронизации, так как асинхронные методы могут легко вызвать проблемы с потоками это неожиданно.кроме того, существует множество процедур, которые по своей природе не являются асинхронными. Они имеют больше смысла как синхронные операции. Принуждение
Math.SqrtнаTask<double> Math.SqrtAsyncбыло бы смешно, например, так как нет никаких причин для того, чтобы быть асинхронным. Вместоasyncнажимаем через ваше приложение, вы бы в конечном итоге сawaitpropogating везде.это также полностью нарушило бы текущую парадигму, а также вызывают проблемы со свойствами (которые фактически являются просто парами методов.. они тоже будут асинхронными?), и имеют другие последствия на протяжении всего проектирования структуры и языка.
если вы делаете много работы, связанной с IO, вы, как правило, найдете это с помощью
asyncповсеместно является отличным дополнением, многие из ваших процедур будетasync. Однако, когда вы начинаете делать работу с привязкой к процессору, в общем, делая вещиasyncна самом деле не хороший - он скрывает тот факт, что вы используете циклы процессора под API, который появляется быть асинхронным, но на самом деле не обязательно действительно асинхронным.
производительность в сторону-асинхронность может иметь стоимость производительности. На клиенте (WinForms, WPF, Windows Phone) это благо для производительности. Но на сервере или в других сценариях, отличных от пользовательского интерфейса, вы оплатить
Я считаю, что есть веская причина сделать все методы асинхронными, если они не нужны для расширения. Выборочные методы создания асинхронности работают только в том случае, если ваш код никогда не развивается, и вы знаете, что метод A() всегда связан с процессором (вы сохраняете его синхронизацию), а метод B() всегда связан с вводом-выводом (вы отмечаете его асинхронность).
но что, если все меняется? Да, a () выполняет вычисления, но в какой-то момент в будущем вам пришлось добавить туда ведение журнала, или отчетность, или пользовательский обратный вызов с помощью реализация, которая не может предсказать, или алгоритм был расширен и теперь включает в себя не только вычисления процессора, но и некоторые операции ввода-вывода? Вам нужно будет преобразовать метод в асинхронный, но это нарушит API, и все вызывающие объекты в стеке также необходимо будет обновить (и они могут даже быть разными приложениями от разных поставщиков). Или вам нужно будет добавить асинхронную версию вместе с версией синхронизации, но это не имеет большого значения - использование версии синхронизации будет блокировать и, следовательно, вряд ли допустимый.
было бы здорово, если бы можно было сделать существующий метод синхронизации асинхронным без изменения API. Но в реальности у нас нет такого варианта, я считаю, и использование асинхронной версии, даже если она в настоящее время не нужна, является единственным способом гарантировать, что вы никогда не столкнетесь с проблемами совместимости в будущем.
Comments