Якщо async-await не створює додаткових потоків, то як вони роблять програми реагуючими?


242

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

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

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


17
Завдання IO не пов'язані з процесором, і тому вони не потребують потоку. Основний момент асинхронізації - не блокувати потоки під час завдань, пов'язаних з IO.
juharr

24
@jdweng: Ні, зовсім не так. Навіть якщо це створило нові теми , це дуже відрізняється від створення нового процесу.
Джон Скіт

8
Якщо ви розумієте асинхронне програмування на основі зворотного виклику, то ви розумієте, як await/ asyncпрацює без створення жодних потоків.
користувач253751

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

6
@RubberDuck: Так, для продовження він може використовувати нитку з пулу потоків. Але це не починає нитку так, як уявляє тут ОП - це не так, як каже "Візьміть цей звичайний метод, тепер запустіть його в окремій нитці - там, це асинхроніка". Це набагато тонкіше за це.
Джон Скіт

Відповіді:


299

Насправді, асинхронізація / очікування не така вже й магічна. Повна тема досить широка, але для швидкого, але досить повного відповіді на ваше запитання, я думаю, ми можемо впоратися.

Давайте розглянемо просту подію натискання кнопки у додатку Windows Forms:

public async void button1_Click(object sender, EventArgs e)
{
    Console.WriteLine("before awaiting");
    await GetSomethingAsync();
    Console.WriteLine("after awaiting");
}

Я зараз явно не буду говорити про те, що воно GetSomethingAsyncповертається. Скажімо, це щось, що завершиться через, скажімо, 2 секунди.

У традиційному, несинхронному світі обробник подій натискання кнопки виглядатиме приблизно так:

public void button1_Click(object sender, EventArgs e)
{
    Console.WriteLine("before waiting");
    DoSomethingThatTakes2Seconds();
    Console.WriteLine("after waiting");
}

Після натискання кнопки у формі програма з’явиться, що заморозиться приблизно на 2 секунди, поки ми будемо чекати завершення цього методу. Що відбувається, так це те, що "насос повідомлення", в основному цикл, блокується.

Цей цикл постійно запитує вікна "Хто-небудь щось робив, як-небудь перемістив мишу, натиснув щось? Чи потрібно щось перефарбовувати? Якщо так, скажіть мені!" а потім обробляє те, що "щось". Цей цикл отримав повідомлення про те, що користувач натиснув "button1" (або еквівалентний тип повідомлення від Windows), і в кінцевому підсумку зателефонував наш button1_Clickметод вище. Поки цей метод не повернеться, ця петля тепер застрягла в очікуванні. Це займає 2 секунди, і протягом цього часу жодне повідомлення не обробляється.

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

Отже, якщо в першому прикладі async/awaitне створюються нові теми, то як це робити?

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

  1. Весь код, що веде до await, включаючи дзвінок доGetSomethingAsync
  2. Весь код наступний await

Ілюстрація:

code... code... code... await X(); ... code... code... code...

Впорядковано:

code... code... code... var x = X(); await X; code... code... code...
^                                  ^          ^                     ^
+---- portion 1 -------------------+          +---- portion 2 ------+

В основному метод виконується так:

  1. Він виконує все до await
  2. Він викликає GetSomethingAsyncметод, який робить свою справу, і повертає щось, що завершиться за 2 секунди в майбутньому

    Поки що ми все ще знаходимось у початковому виклику до кнопки1_Click, що відбувається в основному потоці, викликаному з циклу повідомлень. Якщо код, який веде до awaitпотрібного часу, займає багато часу, інтерфейс користувача все одно застигне. У нашому прикладі не так багато

  3. Що awaitключове слово, разом з яким - то розумним магії компілятора, робить те , що це в основному що - щось на зразок «Добре, ви знаєте , що я збираюся просто повернутися з клацання кнопки обробника подій тут. Якщо ви (як, речей ми» знову чекаю) обійдіть завершення, повідомте мене, тому що у мене ще є якийсь код, який потрібно виконати ".

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

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

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

  5. Через 2 секунди річ, на яку ми чекаємо, завершується, і те, що відбувається зараз, - це те, що (ну, контекст синхронізації) розміщується повідомлення в черзі, на яке дивиться цикл повідомлень, кажучи: "Ей, я отримав ще код для Ви повинні виконати ", і цей код є всім кодом після очікування.
  6. Коли цикл повідомлень потрапить до цього повідомлення, він, в основному, "знову введе" цей метод там, де він припинився, відразу після awaitі продовжуючи виконувати решту методу. Зауважте, що цей код знову викликається з циклу повідомлень, тому, якщо цей код буде робити щось тривале без async/awaitналежного використання , він знову блокує цикл повідомлень

Є багато рухомих частин під капотом тут , так що тут деякі посилання на додаткову інформацію, я збирався сказати «вам це потрібно», але ця тема є досить широким , і це досить важливо знати деякі з цих рухомих частин . Незмінно ви зрозумієте, що асинхронізація / очікування все ще є пропускною концепцією. Деякі основні обмеження та проблеми все ще просочуються до оточуючого коду, і якщо вони не виконуються, вам, як правило, доведеться налагоджувати програму, яка розбивається випадковим чином, здавалося б, без вагомих причин.


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

У .NET є багато речей, які не обов'язково обертають нитку самостійно, але все ще залишаються асинхронними:

  • Веб-запити (та багато інших речей, пов’язаних із мережею, що потребує часу)
  • Асинхронне читання та запис файлів
  • та багато іншого, хороша ознака - якщо у розглянутого класу / інтерфейсу є методи, названі SomethingSomethingAsyncабо BeginSomethingі, EndSomethingі якщо вони IAsyncResultзадіяні.

Зазвичай для цих речей не використовується нитка під кришкою.


Гаразд, значить, ви хочете деякі з цих "широких тем теми"?

Що ж, давайте запитаємо Спробуйте Рослін про наше натискання кнопки:

Спробуйте Рослін

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


11
Так це в основному те, що в ОП описано як " Моделювання паралельного виконання шляхом планування завдань і перемикання між ними ", чи не так?
Бергі

4
@Bergi Не зовсім. Виконання справді паралельне - завдання асинхронного вводу / виводу триває і не вимагає жодних потоків (це те, що було використано задовго до появи Windows - MS DOS також використовував асинхронний ввід / вивід, хоча він не мати багатопотокові!). Звичайно, await можна використовувати так, як ви це описуєте, але, як правило, це не так. Заплановані лише зворотні виклики (у пулі потоків) - між зворотним дзвінком та запитом, потоки не потрібні.
Луань

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

6
@ LasseV.Karlsen - Я вживаю вашу чудову відповідь, але я все ще повісив одну деталь. Я розумію, що обробник подій існує, як на кроці 4, який дозволяє насосному повідомленню продовжувати перекачувати, але коли і де "річ, яка займає дві секунди" продовжує виконуватись, якщо не на окремому потоці? Якби це було виконано на потоці користувальницького інтерфейсу, то воно все одно блокувало б насос повідомлень під час його виконання, оскільки він повинен виконати деякий час у тому ж потоці .. [продовження] ...
rory.ap

3
Мені подобається ваше пояснення із повідомленням насоса. Як ваше пояснення відрізняється, коли немає повідомлення про насос, наприклад у консольній програмі чи веб-сервері? Яким чином досягається відстеження методу?
Пучач

95

Я пояснюю це повністю у своєму дописі в блозі Там немає теми .

Підсумовуючи це, сучасні системи вводу / виводу широко використовують DMA (прямий доступ до пам'яті). Існують спеціальні, виділені процесори на мережевих картах, відеокартах, HDD-контролерах, послідовних / паралельних портах і т. Д. Ці процесори мають прямий доступ до шини пам'яті і керують читанням / записом повністю незалежно від процесора. Процесор повинен просто повідомити пристрій про місцезнаходження в пам'яті, що містить дані, а потім може зробити своє, поки пристрій не зробить перерву, сповістивши ЦП про те, що читання / запис завершено.

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


Просто для того, щоб зрозуміти .. Я розумію високий рівень того, що відбувається під час використання функції async-wait. Щодо створення потоку без потоку - немає жодної нитки лише у запитах вводу / виводу на пристрої, які, як ви сказали, мають власні процесори, які обробляють сам запит? Чи можемо ми припустити, що ВСІ запити вводу / виводу обробляються на таких незалежних процесорах, що означає використання Task.Run ТОЛЬКО для дій, пов'язаних з процесором?
Йонатан Нір

@YonatanNir: справа не лише в окремих процесорах; будь-яка реакція, спричинена подією, природно асинхронна. Task.Runє найбільш підходящим для дій , пов'язаних з процесором , але він також має декілька інших цілей.
Стівен Клірі

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

6
Це зниклий фрагмент у тисячах пояснень !!! Насправді хтось виконує роботу у фоновому режимі з операціями вводу / виводу. Це не потік, а інший спеціальний апаратний компонент, який виконує свою роботу!
the_dark_destructor

2
@PrabuWeerasinghe: Компілятор створює структуру, яка містить змінну стану та локальних змін. Якщо очікуючий повинен поступитися (тобто повернутися до свого абонента), ця структура є коробкою і живе на купі.
Стівен Клірі

87

єдиний спосіб, за яким комп'ютер може здатися робити більше ніж одну річ одночасно, - це (1) насправді робити більше однієї речі одночасно, (2) моделювати її шляхом планування завдань та перемикання між ними. Тож якщо async-wait не робить жодного з них

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

Також, я думаю, вам не вистачає третього варіанту. Ми, старі люди - діти сьогодні зі своєю реп-музикою повинні зійти з мого газону тощо - пам'ятайте світ Windows на початку 1990-х. Не було багатопроцесорних машин і не було планувальників потоків. Ви хотіли запустити дві програми Windows одночасно, вам довелося поступатися . Багатозадачність була кооперативної . ОС повідомляє процес, який він запускає, і якщо він недоброзичливий, він голодує від усіх інших процесів від його обслуговування. Він працює до тих пір, поки він не дасть урожай, і якимось чином він повинен знати, як забрати місце, де він зупинився, коли наступного разу руки ОС повернуться до нього. Однопоточний асинхронний код дуже схожий на це, з "очікувати" замість "вихід". Очікування означає: "Я пам’ятаю, де я зупинився тут, і нехай хтось інший побіжить на деякий час; передзвоніть мені, коли завдання, на яке я чекаю, завершиться, і я підберу туди, де я зупинився". Я думаю, ви можете побачити, як це робить додатки більш чуйними, як і в Windows 3 дні.

виклик будь-якого методу означає очікування завершення методу

Є ключ, який вам не вистачає. Метод може повернутися до завершення його роботи . У цьому суть асинхронності. Метод повертається, він повертає завдання, що означає "ця робота триває; скажіть мені, що робити, коли вона завершена". Робота методу не виконана, хоча вона повернулася .

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


Інші сучасні мови високого рівня також підтримують подібну поведінку, яка явно співпрацює (тобто функція виконує деякі речі, дає [можливо, надсилаючи якесь значення / об'єкт абоненту], продовжується там, де вона припиняється, коли управління передається назад [можливо, з додатковим вводом] ). Генератори є досить великими в Python, для одного.
JAB

2
@JAB: Абсолютно. У C # генератори називаються "блоками ітераторів" і використовують yieldключове слово. І asyncметоди, і ітератори в C # - це форма кореневої програми , що є загальним терміном для функції, яка знає, як призупинити свою поточну операцію для відновлення пізніше. Ці низки мов мають супутні процедури або потоки управління, подібні до програмного забезпечення.
Ерік Ліпперт

1
Аналогія врожайності хороша - це спільна багатозадачність в рамках одного процесу. (і тим самим уникати питань щодо стабільності системи багатозадачності в рамках загальної системи)
користувач253751

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

@EricLippert Асинхронний метод WebClient фактично створює додатковий потік, дивіться тут stackoverflow.com/questions/48366871 / ...
KevinBui

28

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

Після дослідження моїх, я нарешті -то знайшов відсутню частину: select(). В Зокрема, IO мультиплексування, реалізовані різними ядрами під різними назвами: select(), poll(), epoll(), kqueue(). Це системні виклики, які, хоча деталі реалізації відрізняються, дозволяють передати набір дескрипторів файлів для перегляду. Потім ви можете зробити ще один дзвінок, який блокується, поки не зміниться один із дескрипторів переглянутого файлу.

Таким чином, можна чекати на наборі подій вводу-виводу (основний цикл подій), обробляти першу подію, яка завершується, а потім повернути контроль назад до циклу подій. Промийте і повторіть.

Як це працює? Ну, коротка відповідь - це магія на рівні ядра та апаратного забезпечення. Окрім процесора, в комп'ютері є багато компонентів, і ці компоненти можуть працювати паралельно. Ядро може керувати цими пристроями та безпосередньо спілкуватися з ними для отримання певних сигналів.

Ці системні виклики мультиплексування IO є основоположним складовим блоком однопотокових циклів подій, таких як node.js або Tornado. Коли ви awaitфункціонуєте, ви переглядаєте певну подію (завершення цієї функції), а потім повертаєтесь до керування головної петлі події. Коли подія, яку ви спостерігаєте, завершується, функція (зрештою) вибирається з того місця, де вона припинилася. Функції, які дозволяють призупинити та відновити обчислення на кшталт цього, називаються супрограмами .


25

awaitі asyncвикористовуйте Завдання не нитки.

Рамка має пул потоків, готових виконати якусь роботу у вигляді об'єктів Task ; подання Завдання в пул означає вибір вільного, вже існуючого 1 потоку для виклику методу дії завдання.
Створення завдання - це питання створення нового об'єкта, набагато швидшого, ніж створення нової нитки.

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

Оскільки async/awaitвикористовують Task s, вони не створюють новий потік.


Хоча методика програмування переривань широко використовується у всіх сучасних ОС, я не думаю, що вони тут актуальні.
Ви можете мати два завдання, пов'язані з процесором, паралельно виконуючись (перемежованими фактично) в одному процесорі, використовуючи aysnc/await.
Це не можна було пояснити просто тим фактом, що ОС підтримує чергу IORP .


Востаннє я перевіряв, як компілятор трансформував asyncметоди в DFA , робота поділяється на етапи, кожен з яких закінчується awaitінструкцією. Починає свою задачу і приєднати його продовження , щоб виконати наступний крок.
await

Як приклад поняття, ось приклад псевдокоду.
Речі спрощуються задля ясності і тому, що я не пам’ятаю точно всіх деталей.

method:
   instr1                  
   instr2
   await task1
   instr3
   instr4
   await task2
   instr5
   return value

Це перетворюється на щось подібне

int state = 0;

Task nextStep()
{
  switch (state)
  {
     case 0:
        instr1;
        instr2;
        state = 1;

        task1.addContinuation(nextStep());
        task1.start();

        return task1;

     case 1:
        instr3;
        instr4;
        state = 2;

        task2.addContinuation(nextStep());
        task2.start();

        return task2;

     case 2:
        instr5;
        state = 0;

        task3 = new Task();
        task3.setResult(value);
        task3.setCompleted();

        return task3;
   }
}

method:
   nextStep();

1 Насправді пул може мати свою політику створення завдань.


16

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

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

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


3
Це не конкуренція в першу чергу; це співпраця!
Ерік Ліпперт

16

Ось як я бачу все це, це може бути не надто технічно точно, але мені це допомагає, принаймні :).

В основному є два типи обробки (обчислення), які відбуваються на машині:

  • обробка, що відбувається в процесорі
  • обробку, що відбувається з іншими процесорами (GPU, мережева карта тощо), назвемо їх IO.

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

Деякі приклади:

  • якщо я використовую метод запису FileStreamоб'єкта (який є потоком), обробка буде, скажімо, 1% пов'язана з процесором і 99% пов'язана з IO.
  • якщо я використовую метод запису NetworkStreamоб'єкта (який є потоком), обробка буде, скажімо, 1% пов'язана з процесором і 99% пов'язана з IO.
  • якщо я використовую метод запису Memorystreamоб'єкта (який є потоком), обробка буде на 100% пов'язана з процесором.

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

Тепер, щоб оптимізувати речі, іноді корисно мати можливість паралельно запускати код (зауважте, я не використовую слово асинхронний), якщо це можливо та / або необхідно.

Деякі приклади:

  • У настільному додатку я хочу надрукувати документ, але не хочу його чекати.
  • Мій веб-сервер одночасно обслуговує багато клієнтів, кожен отримує свої сторінки паралельно (не серіалізується).

Перш ніж асинхронізувати / очікувати, ми по суті мали два рішення для цього:

  • Нитки . Це було відносно просто у використанні з класами Thread і ThreadPool. Нитки призначені лише для процесора .
  • "Стара" модель асинхронного програмування Begin / End / AsyncCallback . Це просто модель, вона не говорить вам, чи будете ви прив'язані до процесора чи IO. Якщо ви подивитеся на класи Socket або FileStream, це пов'язаний IO, що круто, але ми рідко його використовуємо.

Асинхронізація / очікування - це лише звичайна модель програмування, заснована на концепції завдань . Це трохи простіше у використанні, ніж потоки або пули потоків для завдань, пов'язаних з процесором, і набагато простіше у використанні, ніж стара модель Begin / End. Таємницю, однак, це "просто" надзвичайно витончена функція, обов`язкова оболонка.

Таким чином, реальна виграш - це здебільшого завдання , пов'язані з IO , завдання, яке не використовує процесор, але асинхронізація / очікування залишається лише моделлю програмування, це не допоможе вам визначити, як / де відбудеться обробка в підсумку.

Це означає, що це не тому, що у класу є метод "DoSomethingAsync", який повертає об'єкт Task, і ви можете припустити, що він буде пов'язаний з процесором (це означає, що він може бути марним , особливо якщо у нього немає параметра маркера скасування) або IO Bound (а це означає, що це, мабуть, обов'язково ) або поєднання обох (оскільки модель досить вірусна, зв’язки та потенційні переваги можуть бути зрештою супер мішаними та не настільки очевидними).

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


1
Це досить хороша відповідь, використовуючи theadpool для роботи з процесором, це погано, тому що нитки TP повинні використовуватися для вивантаження операцій з виводу даних. Робочий процес imo, пов'язаний з процесором, звичайно повинен блокувати застереженнями, і ніщо не перешкоджає використанню декількох потоків.
davidcarr

3

Узагальнення інших відповідей:

Асинхронізація / очікування створена насамперед для завдань, пов’язаних з IO, оскільки, використовуючи їх, можна уникнути блокування потоку виклику. Основне їх використання - з потоками інтерфейсу користувача, де не бажано, щоб нитка блокувалася під час операції, пов'язаної з IO.

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


Хороший підсумок, але я думаю, він повинен відповісти на ще 2 питання, щоб дати повне уявлення: 1. На якій нитці виконується очікуваний код? 2. Хто контролює / налаштовує згаданий пул потоків - розробник або середовище виконання?
stojke

1. У цьому випадку в основному очікуваний код - це операція, пов'язана з IO, яка не використовує потоки процесора. Якщо потрібно використовувати функцію очікування для операцій, пов'язаних з процесором, може бути породжена окрема задача. 2. Потоком у пулі потоків керує Планувальник завдань, який є частиною рамки TPL.
вайбхав кумар

2

Я намагаюся пояснити це знизу вгору. Можливо, хтось вважає це корисним. Я був там, робив це, винаходив це, коли робив прості ігри в DOS на Паскалі (старі добрі часи ...)

Отже ... У кожній програмі, керованій подією, є цикл подій всередині, що є приблизно таким:

while (getMessage(out message)) // pseudo-code
{
   dispatchMessage(message); // pseudo-code
}

Рамки зазвичай приховують від вас цю деталь, але вона є. Функція getMessage зчитує наступну подію з черги подій або чекає, поки не відбудеться подія: переміщення миші, клавіша, клавіша, клацання тощо. Потім dispatchMessage посилає подію до відповідного обробника подій. Потім чекає наступної події і так далі, поки не настане подія, яка закриває, яка закриває цикл і закінчує додаток.

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

void expensiveOperation()
{
    for (int i = 0; i < 1000; i++)
    {
        Thread.Sleep(10);
    }
}

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

Отже, ви зміните це на:

void expensiveOperation()
{
    doIteration(0);
}

void doIteration(int i)
{
    if (i >= 1000) return;
    Thread.Sleep(10); // Do a piece of work.
    postFunctionCallMessage(() => {doIteration(i + 1);}); // Pseudo code. 
}

У цьому випадку запускається лише перша ітерація, після чого вона розміщує повідомлення в черзі подій для запуску наступної ітерації та повертається. У нашому прикладі postFunctionCallMessageпсевдофункція ставить події "виклик цієї функції" в чергу, тому диспетчер подій зателефонує їй, коли вона досягне. Це дозволяє обробляти всі інші події GUI під час безперервного виконання фрагментів тривалої роботи.

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

Таким чином, ми можемо виконувати довгі виконання завдань в одному потоці паралельно GUI, розбиваючи їх на невеликі шматки та надсилаючи події продовження. Це загальна ідея Taskкласу. Він являє собою роботу, і коли ви зателефонуєте .ContinueWithна нього, ви визначаєте, яку функцію викликати як наступну частину, коли поточний фрагмент закінчується (і його повернене значення передається у продовження). TaskКлас використовує пул потоків, де є цикл обробки подій в кожному потоці , чекаючи , щоб зробити частини роботи , подібні хочу я показав на початку. Таким чином, ви можете мати мільйони завдань, які виконуються паралельно, але лише кілька потоків для їх виконання. Але це було б так само добре з однією ниткою - доки ваші завдання належним чином розбиті на невеликі шматочки, кожна з них видається паралельною.

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


0

Власне, async awaitланцюги - це державна машина, породжена компілятором CLR.

async await однак використовує потоки, які TPL використовує пул потоків для виконання завдань.

Причина, що додаток не заблоковано, полягає в тому, що державна машина може вирішити, який порядок виконувати, повторити, перевірити та прийняти рішення ще раз.

Подальше читання:

Що створює асинхроніка та очікування?

Асинхрон чекайте і генерований державний апарат

Асинхронний C # і F # (III.): Як це працює? - Томаш Петричек

Редагувати :

Добре. Здається, моя розробка невірна. Однак я маю зазначити, що державні машини є важливим активом для async awaits. Навіть якщо ви приймаєте асинхронний ввід / вивід, вам все одно потрібен помічник, щоб перевірити, чи завершена операція, тому нам все-таки потрібна державна машина і визначити, яку процедуру можна виконати асинхронно разом.


0

Це не відповідає безпосередньо на питання, але я вважаю, що це цікава інформація, яка є додатковою:

Асинхронізація і очікування не створює нових потоків сама по собі. АЛЕ залежно від того, де ви використовуєте асинхрон очікування, синхронна частина ПЕРЕЖДА очікування може працювати на іншому потоці, ніж синхронна частина ПІСЛЯ очікування (наприклад, ядро ​​ASP.NET та ASP.NET поводяться по-різному).

У програмах, що базуються на UI-Thread (WinForms, WPF), ви будете знаходитись в одній нитці до і після. Але коли ви використовуєте async у потоці пулу Thread, нитка до та після очікування може бути не однаковою.

Чудове відео на цю тему

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.