По-перше, дякую за добрі слова. Це дійсно дивовижна особливість, і я радий, що я був невеликою частиною цього.
Якщо весь мій код повільно перетворюється на асинхронізацію, чому б просто не зробити це все асинхронічним за замовчуванням?
Ну, ти перебільшуєш; весь ваш код не перетворюється на асинхронізацію. Коли ви додаєте два "прості" цілі числа разом, ви не чекаєте результату. Коли ви додасте два майбутніх цілих числа разом, щоб отримати третє майбутнє ціле число - адже це те, що 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 також має набагато сильнішу підтримку в своїй системі типів для різновидів "монадичних підйомів", на які я нагадав.