Чому всі функції не повинні бути асинхронізованими за замовчуванням?


107

Асинхронного Await модель .net 4.5 це парадигма змінюється. Це майже занадто добре, щоб бути правдою.

Я пересилаю деякий IO-важкий код до асинхронного очікування, оскільки блокування - це минуле.

Дуже небагато людей порівнюють асинхронні очікування із зараженням зомбі, і я виявив це досить точно. Асинхронний код подобається іншому коду асинхронізації (вам потрібна функція асинхронізації, щоб очікувати на функцію асинхронізації). Отже, все більше і більше функцій стають асинхронними, і це постійно зростає у вашій кодовій базі.

Зміна функцій асинхронізації - дещо повторювана та немислена робота. Вкиньте asyncв декларацію ключове слово, оберніть повернене значення Task<>і ви вже майже все зробили. Це досить невдало, наскільки простий весь процес, і досить скоро сценарій, що замінює текст, автоматизує більшу частину "перенесення" для мене.

А тепер питання .. Якщо весь мій код повільно перетворюється на асинхронізацію, чому б просто не зробити все це асинхронічним за замовчуванням?

Очевидна причина, яку я припускаю, - це продуктивність. У Async-await є накладні витрати та код, який не потрібно асинхронізувати, бажано, не повинен. Але якщо продуктивність є єдиною проблемою, безумовно, деякі розумні оптимізації можуть видалити накладні витрати автоматично, коли це не потрібно. Я читав про оптимізацію "швидкого шляху" , і мені здається, що тільки вона повинна піклуватися про більшу частину цього.

Можливо, це можна порівняти зі зміною парадигми, спричиненою збирачами сміття. На початку GC звільнення власної пам’яті було, безумовно, більш ефективним. Але маси все ж обирали автоматичний збір на користь більш безпечного, простішого коду, який може бути менш ефективним (і навіть це, мабуть, вже не вірно). Можливо, так і має бути тут? Чому всі функції не повинні бути асинхронні?


8
Надайте команді C # кредит на маркування карти. Як це було зроблено сотні років тому, "тут лежать дракони". Ви можете оснастити корабель і поїхати туди, ймовірно, ви переживете його, коли світить сонце та вітер у спину. А іноді ні, вони ніколи не поверталися. Так само, як асинхронізація / очікування, SO наповнюється питаннями користувачів, які не розуміли, як вони вийшли з карти. Хоча вони отримали досить гарне попередження. Тепер це їхня проблема, а не проблема команди C #. Вони позначили драконів.
Ганс Пасант

1
@ Скажіть, навіть якщо ви видалите різницю між функціями синхронізації та асинхронізації, функції виклику, які реалізуються синхронно, все одно будуть синхронні (як ваш приклад WriteLine)
talkol

2
"Оберніть повернене значення на завдання <>" Якщо ваш метод не повинен бути async(виконати якийсь контракт), це, мабуть, погана ідея. Ви отримуєте недоліки async (підвищена вартість викликів методів; необхідність використання awaitв коді виклику), але жодна з переваг.
svick

1
Це дійсно цікаве запитання, але, можливо, не дуже підходить для SO. Ми можемо розглянути можливість їх переміщення до програмістів.
Ерік Ліпперт

1
Можливо, мені чогось не вистачає, але у мене точно таке питання. Якщо асинхронізація / очікування завжди є парами, і код все-таки доведеться чекати, коли він завершить виконання, то чому б не просто оновити існуючі рамки .NET, а зробити ті методи, які потрібно асинхронізувати - асинхронувати за замовчуванням БЕЗ складання додаткових ключових слів? Мова вже перетворюється на те, що було розраховано - спагеті за ключовим словом. Я думаю, вони повинні були перестати робити це після того, як було запропоновано "вар". Тепер у нас є "динамічний", асин / очікування ... і т.д. ... Чому у вас хлопці просто. NET-ify javascript? ;))
monstro

Відповіді:


127

По-перше, дякую за добрі слова. Це дійсно дивовижна особливість, і я радий, що я був невеликою частиною цього.

Якщо весь мій код повільно перетворюється на асинхронізацію, чому б просто не зробити це все асинхронічним за замовчуванням?

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

Основна причина не робити все асинхроністикою полягає в тому, що мета асинхронізації / очікування - полегшити введення коду у світі з багатьма операціями з високою затримкою . Переважна більшість ваших операцій не відрізняється високою затримкою, тому не має сенсу приймати показник ефективності, який зменшує затримку. Швидше за все, кілька ключових ваших операцій - це висока затримка, і ці операції викликають зараження асинхронією зомбі у всьому коді.

якщо продуктивність є єдиною проблемою, безумовно, деякі розумні оптимізації можуть видалити накладні витрати автоматично, коли це не потрібно.

У теорії теорія та практика схожі. На практиці вони ніколи не бувають.

Дозвольте навести три бали проти такого перетворення з подальшим оптимізаційним проходом.

Перший момент знову: асинхронізація в C # / VB / F # по суті є обмеженою формою проходження продовження . Величезна кількість досліджень у функціональному мовному співтоваристві розробила способи визначення способів оптимізації коду, що використовує стилі передачі продовження. Команді компілятора, ймовірно, доведеться вирішувати дуже схожі проблеми у світі, де за замовчуванням було встановлено "асинхронізацію", а методи, які не мають асинхронізацію, повинні бути визначені та де-асинхронізовані. Команда C # насправді не зацікавлена ​​у вирішенні проблем відкритих досліджень, тому це дуже важливо.

Другий момент проти - це те, що C # не має рівня "референтної прозорості", що робить такі види оптимізації більш простежуваними. Під "референтною прозорістю" я маю на увазі властивість, від якої значення виразу не залежить, коли воно оцінюється . Вирази на зразок 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().)

Тепер уявіть, що ця властивість більше не знати, які побічні ефекти в порядку трапляються, стосується кожного коду вашої програми, за винятком тих, які оптимізатору вдається де-асинхронізувати. В основному ви більше не маєте поняття, які вирази будуть оцінюватися в якому порядку, а це означає, що всі вирази повинні бути референтно прозорими, що складно на такій мові, як C #.

Третій момент проти - це тоді, що ви повинні запитати "чому асинхроніка така особлива?" Якщо ви збираєтесь стверджувати, що кожна операція насправді повинна бути такою, Task<T>вам потрібно мати можливість відповісти на питання "чому ні Lazy<T>?" або "чому ні Nullable<T>?" або "чому ні IEnumerable<T>?" Тому що ми могли так само легко зробити це. Чому не повинно бути так, що кожна операція піднімається до нульової ? Або кожна операція ліниво обчислюється, а результат кешується на потім , або результат кожної операції - це послідовність значень, а не лише одне значення . Тоді вам доведеться спробувати оптимізувати ті ситуації, коли ви знаєте "о, це ніколи не повинно бути нульовим, тому я можу генерувати кращий код" тощо.

Справа в тому, що: мені незрозуміло, що Task<T>насправді є особливим для того, щоб вимагати такої роботи.

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


Здійснення функцій асинхронізації, викликаних без очікування, для мене не має сенсу (у звичайному випадку). Якби ми видалили цю функцію, компілятор міг би вирішити, чи функція асинхронізована чи ні (чи викликає її очікування?). Тоді ми можемо мати однаковий синтаксис для обох випадків (асинхронізація та синхронізація), і використовувати лише функцію очікування у викликах як диференціатор. Зараження зомбі вирішено :)
талькол

Я продовжував обговорення в програмістах на ваш запит: programmers.stackexchange.com/questions/209872/…
talkol

@EricLippert - Дуже приємна відповідь, як завжди :) Мені було цікаво, чи можете ви уточнити "високу затримку"? Чи є тут загальний діапазон у мілісекундах? Я просто намагаюся з'ясувати, де розташована нижня межа для використання асинхронізу, оскільки я не хочу її зловживати.
Travis J

7
@TravisJ: Посібник: не блокуйте потік інтерфейсу більше 30 мс. Більше того, і ви ризикуєте помітити користувач паузу.
Ерік Ліпперт

1
Мене кидає виклик те, що чи ні щось робиться синхронно чи асинхронно, це деталізація, яка може змінитися. Але зміна цієї реалізації змушує ефект пульсації через код, який його викликає, код, який викликає це тощо. Ми закінчуємо зміну коду через код, від якого залежить, що ми зазвичай намагаємося уникати. Або ми використовуємо, async/awaitтому що щось заховане під шарами абстракції може бути асинхронним.
Скотт

23

Чому всі функції не повинні бути асинхронні?

Як ви вже згадували, продуктивність - одна з причин. Зауважте, що опція "швидкий шлях", з якою ви пов’язані, покращує ефективність у випадку виконаного завдання, але це все ж потребує набагато більше інструкцій та накладних витрат у порівнянні з одним викликом методу. Таким чином, навіть при встановленні "швидкого шляху" ви додаєте багато складності та накладних витрат при кожному виклику методу асинхронізації.

Зворотна сумісність, а також сумісність з іншими мовами (включаючи сценарії інтеропа) також стали б проблематичними.

Інше - питання складності та наміру. Асинхронні операції додають складності - у багатьох випадках мовні особливості приховують це, але є багато випадків, коли виготовлення методів asyncбезумовно додає складності їх використанню. Це особливо актуально, якщо у вас немає контексту синхронізації, оскільки методи асинхронізації можуть легко призвести до появи несподіваних проблем з ниткою.

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

Це також повністю порушить поточну парадигму, а також спричинить проблеми з властивостями (які фактично є лише парами методів. Чи вони б також переходили в асинхронізацію?), А також матимуть інші наслідки для проектування рамки та мови.

Якщо ви робите багато роботи, пов'язаної з IO, ви схиляєтесь до того, що asyncшироке використання - це чудове доповнення, багато ваших процедур будуть async. Однак, коли ви починаєте виконувати роботи, пов'язані з процесором, загалом, робити речі asyncнасправді не добре - це приховує той факт, що ви використовуєте цикли процесора під API, який здається асинхронним, але насправді не обов'язково є справді асинхронним.


Точно те, що я збирався писати (продуктивність), сумісність у зворотному стані може бути іншою справою, DLL використовуватимуться зі старими мовами, які також не підтримують async / очікують
Sayse,

Робити sqrt async не смішно, якщо ми просто видалимо різницю між функціями синхронізації та асинхронізації
talkol

@talkol Я думаю, я б це розвернув - чому кожен виклик функції повинен сприймати складність асинхронії?
Рід Копсей

2
@talkol Я заперечую, що це не обов'язково правда - асинхронія може додавати самі помилки, які гірші, ніж блокування ...
Reed Copsey

1
@talkol Як await FooAsync()простіше Foo()? І замість невеликого ефекту доміно якийсь час у вас є весь час величезний ефект доміно, і ви це називаєте поліпшенням?
svick

4

Ефективність убік - async може мати витрати на продуктивність. Для клієнта (WinForms, WPF, Windows Phone) це користь для продуктивності. Але на сервері або в інших сценаріях, що не користуються інтерфейсом, ви платите за продуктивність. Ви, звичайно, не хочете, щоб асинхронізація там за замовчуванням там. Використовуйте його, коли вам потрібні переваги масштабування.

Використовуйте його, коли на солодкому місці. В інших випадках не варто.


2
+1 - Проста спроба скласти ментальну карту коду, що виконує 5 асинхронних операцій паралельно з випадковою послідовністю завершення, для більшості людей доставить достатньо болю на день. Розмірковувати про поведінку асинхронного (а отже, і притаманного йому паралельного) коду набагато важче, ніж старий добрий синхронний код ...
Олексій Левенков

2

Я вважаю, що є вагома причина зробити всі методи асинхронізованими, якщо вони не потрібні - розширюваність. Вибіркові методи створення асинхронізування працюють лише в тому випадку, якщо ваш код ніколи не розвивається, і ви знаєте, що метод A () завжди пов'язаний з процесором (ви підтримуєте його синхронізацію), а метод B () завжди пов'язаний введення-виводу (ви позначаєте його як асинхронізацію).

Але що робити, якщо все зміниться? Так, A () робить обчислення, але в якийсь момент у майбутньому вам довелося додати туди вхід, або звітування, або визначений користувачем зворотний виклик із реалізацією, яку не можна передбачити, або алгоритм розширений і тепер включає не лише обчислення процесора, але й також деякі I / O? Вам потрібно буде перетворити метод на асинхронізацію, але це порушить API, і всі викликаючі стеки потрібно буде також оновити (і вони можуть бути навіть різними додатками від різних постачальників). Або вам потрібно буде додати версію async поряд із версією синхронізації, але це не має великого значення - використання версії синхронізації заблокує і, таким чином, навряд чи прийнятно.

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


Хоча це здається крайнім коментарем, у цьому є багато правди. По-перше, через цю проблему: "це порушило б API і всі викликаючі сторони стека", асинхронізація / очікування робить додаток більш щільно пов'язаним. Це може легко порушити принцип заміщення Ліскова, якщо, наприклад, підклас хоче використовувати асинхрон / очікувати. Крім того, важко уявити архітектуру мікросервісів, де більшість методів не потребували асинхронізації / очікування.
ItsAllABadJoke
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.