Чи не вказівки щодо використання асинхронізування / очікування використання в C # не суперечать поняттям гарної архітектури та шару абстракції?


103

Це питання стосується мови C #, але я очікую, що він охопить і інші мови, такі як Java або TypeScript.

Microsoft рекомендує кращі практики використання асинхронних дзвінків у .NET. Серед цих рекомендацій виберемо дві:

  • змінити підпис методів асинхронізації, щоб вони повернули Завдання або Завдання <> (в TypeScript це буде Обіцянкою <>)
  • змінити назви методів асинхронізації, щоб закінчити xxxAsync ()

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

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

Чи не асинхронізація / очікування кращих практик, що суперечать принципам "хорошої архітектури"?

Чи означає це, що кожен інтерфейс (скажімо, IEnumerable, IDataAccessLayer) потребує свого асинхронного аналога (IAsyncEnumerable, IAsyncDataAccessLayer) таким, щоб їх можна було замінити в стеці при переході на асинхронні залежності?

Якщо ми поставимо проблему трохи далі, чи не було б простіше вважати кожен метод асинхронним (повернути завдання <> або обіцяти <>), а також методи синхронізувати виклики асинхронізації, коли вони насправді не є асинхронізація? Чи варто цього чекати від майбутніх мов програмування?


5
Хоча це звучить як чудове обговорення, я думаю, що це занадто обґрунтоване думкою, щоб відповісти тут.
Ейфорія

22
@Euphoric: Я думаю, що тут вирішена проблема йде глибше, ніж керівництво C #, це лише симптом того, що зміна частин програми на асинхронну поведінку може мати не локальний вплив на загальну систему. Тож моя кишка каже мені, що для цього повинна бути невпевнена відповідь, заснована на технічних фактах. Отже, я закликаю всіх тут не закривати це питання занадто рано, натомість зачекайте, які відповіді прийдуть (і якщо вони занадто впевнені, ми все ще можемо проголосувати за закриття).
Док Браун

25
@DocBrown Я думаю, що більш глибоке питання тут: "Чи можна змінити частину системи з синхронної на асинхронну без частин, які залежать від неї, і доведеться змінювати?" Я думаю, що відповідь на це чітке "ні". У такому випадку я не бачу, як тут застосовуються "поняття хорошої архітектури та багатошаровості".
Ейфорія

6
@Euphoric: звучить як хороша основа для невпевненої відповіді ;-)
Doc Brown

5
@Gherman: оскільки C #, як і багато мов, не може робити перевантаження лише на основі типу повернення. Ви закінчите з методами асинхронізації, які мають такий самий підпис, що і їх аналоги для синхронізації (не всі можуть приймати CancellationToken, а ті, хто може, бажають надати за замовчуванням). Видалення існуючих методів синхронізації (і проактивне порушення всього коду) - очевидно, що не починається.
Джероен Мостерт

Відповіді:


111

Якого кольору ваша функція?

Можливо, вас зацікавить " Який колір вашої функції" Боба Ністрома 1 .

У цій статті він описує вигадану мову, де:

  • Кожна функція має колір: синій або червоний.
  • Червона функція може викликати синю або червону функції, жодних проблем.
  • Синя функція може викликати лише сині функції.

Хоча це вигадано, це відбувається досить регулярно в мовах програмування:

  • У C ++ метод "const" може викликати лише інші "const" методи this.
  • У Haskell функція, яка не має вводу-виводу, може викликати лише функції, які не мають вводу-виводу.
  • У C # функція синхронізації може викликати лише функції синхронізації 2 .

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

1 Боб Ністром, крім ведення блогів, також є частиною команди Dart і написав цю маленьку серію Crafting Interpreters; дуже рекомендується для будь-якої мови програмування / компілятора.

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

Мовне обмеження

Це, по суті, обмеження мови / часу.

Наприклад, мова з нанизуванням M: N, наприклад Erlang і Go, не має asyncфункцій: кожна функція потенційно асинхронізована, і її "волокно" просто буде призупинено, замінене та замінене назад, коли воно буде знову готове.

C # вийшов із моделлю різьблення 1: 1, тому вирішив надати поверхневу синхронність мовою, щоб уникнути випадкового блокування потоків.

За наявності мовних обмежень вказівки щодо кодування повинні адаптуватися.


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

16
Що ви маєте на увазі під різьбленням "M: N" та "1: 1"?
Людина капітана

14
@CaptainMan: 1: 1 нанизування означає зіставлення однієї нитки програми в одну нитку ОС, це так у таких мовах, як C, C ++, Java або C #. Навпаки, прорізування M: N означає відображення потоків M додатків у потоках N OS; у випадку Go, потік програми називається "goroutine", у випадку Ерланг - це "актор", і ви, можливо, також чули про них як "зелені нитки" або "волокна". Вони забезпечують паралельність, не вимагаючи паралелізму. На жаль, стаття Вікіпедії на цю тему є досить рідкою.
Матьє М.

2
Це дещо пов’язано, але я також вважаю, що ця ідея "кольорового кольору" також стосується функцій, які блокуються на вході від користувача, наприклад, модальних діалогових вікон, вікон повідомлень, деяких форм вводу / виводу консолі тощо, які є інструментами рамки існували з самого початку.
jrh

2
@MatthieuM. У C # немає одного потоку програми на один потік ОС і ніколи цього не робилося. Це дуже очевидно, коли ви взаємодієте з нативним кодом, і особливо очевидно при запуску в MS SQL. І звичайно, спільні процедури завжди були можливі (і навіть простіші async); Дійсно, це був досить поширений зразок для створення чуйного інтерфейсу користувача. Це було так красиво, як Ерланг? Ні. Але це все ще далеко від С :)
Луаан

82

Ви маєте рацію, тут є суперечність, але це не те, що «найкращі практики» є поганими. Це тому, що асинхронна функція робить по суті іншу річ, ніж синхронна. Замість того, щоб чекати результату від його залежностей (як правило, якийсь IO), він створює завдання, яке потрібно обробляти головним циклом подій. Це не різниця, яка може бути добре прихована під абстракцією.


27
Відповідь така ж проста, як і цей ІМО. Різниця між синхронним та асинхронним процесом не є деталізацією реалізації - це семантично інший контракт.
Мураха P

11
@AntP: Я не згоден, що це так просто; вона повертається мовою C #, але не, наприклад, мовою Go. Отже, це не властива властивості асинхронних процесів, це питання, як асинхронні процеси моделюються в даній мові.
Матьє М.

1
@MatthieuM. Так, але ви можете використовувати asyncметоди в C # для надання синхронних контрактів, якщо ви хочете. Єдина відмінність полягає в тому, що Go за замовчуванням асинхронний, а C # за замовчуванням синхронний. asyncдає вам другу модель програмування - async це абстракція (те, що вона насправді робить, залежить від часу виконання, планувальника завдань, контексту синхронізації, виконання очікування ...).
Луань

6

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

У певному сенсі це як метод, який викидає винятки та інший, який є "безпечним" і не викидає навіть у випадку помилки. У який момент кодер надмірний, щоб забезпечити ці методи, які в іншому випадку можуть бути перетворені один в інший?

У цьому є дві школи думки: одна - створити кілька методів, кожен з яких викликає інший, можливо, приватний метод, що передбачає можливість надання необов'язкових параметрів або незначних змін для поведінки, таких як асинхронність. Інша полягає в тому, щоб мінімізувати методи інтерфейсу, щоб вивести необхідні речі, залишаючи його абоненту для здійснення необхідних модифікацій.

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

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

Однак, як і багато речей у цій галузі, вона значною мірою є суб'єктивною. Обидва підходи мають свої плюси і мінуси. Microsoft також розпочала конвенцію про додавання літер, що вказують на тип на початку імені змінної, і "m_", щоб вказати, що вона є членом, що призводить до імен змінних на кшталт m_pUser. Моя думка полягає в тому, що навіть Microsoft не є непогрішним, і може також помилитися.

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


6
"Під час виконання перетворення асинхронного дзвінка в синхронний тривіально", я не впевнений, що це саме так. У .NET, використовуючи .Wait()метод тощо, може спричинити негативні наслідки, а в js, наскільки я знаю, це взагалі неможливо.
max630

2
@ max630 Я не говорив, що немає проблем, які слід розглядати, але якщо це було спочатку синхронним завданням, швидше за все, це не створення тупиків. Однак це банальне значення не означає "подвійне клацання тут для переходу в синхронний". У js ви повертаєте екземпляр Promise та вимагаєте вирішити його.
Ніл

2
так, це абсолютно біль у попці, щоб перетворити асинхрону назад на синхронізацію
Еван

4
@Neil У javascript, навіть якщо ви зателефонували Promise.resolve(x)та додали до неї зворотні дзвінки, ці зворотні виклики не будуть виконані негайно.
NickL

1
@Neil, якщо інтерфейс відкриває асинхронний метод, очікувати, що очікування на завдання не створить тупик, не є хорошим припущенням. Набагато краще, щоб інтерфейс показав, що він фактично синхронізований в підписі методу, ніж обіцянка в документації, яка може змінитися в більш пізній версії.
Карл Уолш

2

Давайте уявимо, що існує спосіб дозволити вам викликати функції асинхронним способом без зміни їх підпису.

Це було б дійсно круто, і ніхто не рекомендував би змінити їхні імена.

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

public class HTTPClient
{
    public HTTPResponse GET()
    {
        //send data
        while(!timedOut)
        {
            //check for response
            if(response) { 
                this.GotResponse(response); 
            }
            this.YouCanWait();
        }
    }

    //tell calling code that they should watch for this event
    public EventHander GotResponse
    //indicate to calling code that they can go and do something else for a bit
    public EventHander YouCanWait;
}

Це ті два біти інформації, які потрібні для виклику коду, щоб запустити код асинхронним способом, який подобається Taskта asyncінкапсулювати.

Існує більше одного способу виконання асинхронних функцій, async Taskце лише один візерунок, вбудований у компілятор через типи повернення, так що вам не доведеться вручну пов’язувати події


0

Я торкнуся головного моменту менш модною C # ness і більш загальною:

Чи не асинхронізація / очікування кращих практик, що суперечать принципам "хорошої архітектури"?

Я б сказав, що це просто залежить від вибору, який ви робите в дизайні свого API та того, що ви надаєте користувачеві.

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

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

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

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

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

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


2
@Miral Ви використовували "виклик методу асинхронізації методом синхронізації" в обох можливостях.
Адріан Врагг

@AdrianWragg Так я і зробив; мій мозок, мабуть, мав стан перегонів. Я це виправлю.
Міраль

Це навпаки; тривіально називати метод асинхронізації методом синхронізації, але неможливо викликати метод синхронізації з методу асинхронізації. (І коли речі повністю розпадаються - це коли хтось намагається зробити останнє в будь-якому випадку, що може призвести до тупиків.) Тож якщо вам довелося вибрати один, асинхронізація за замовчуванням є кращим вибором. На жаль, це також важчий вибір, оскільки реалізація асинхронізації може викликати лише методи асинхронізації.
Міраль

(І під цим я, звичайно, маю на увазі метод блокування синхронізації. Ви можете викликати те, що робить чистий обчислюваний процесором обчислення синхронно з методом асинхронізації - хоча вам слід намагатися уникати цього, якщо ви не знаєте, що перебуваєте в робочому контексті а не контекст інтерфейсу користувача, але блокування дзвінків, які чекають у режимі очікування в блокуванні, для вводу / виводу або для іншої операції - погана ідея.)
Міраль
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.