Чим відрізняється асинхронне програмування від багатопотокового?


234

Я подумав, що вони в основному те саме - писати програми, які розділяють завдання між процесорами (на машинах, які мають 2+ процесори). Потім я читаю це , що говорить:

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

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

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

Тепер я розумію ідею асинхронних завдань, таких як приклад на pg. 467 C # у глибині Джона Скіта , Третє видання

async void DisplayWebsiteLength ( object sender, EventArgs e )
{
    label.Text = "Fetching ...";
    using ( HttpClient client = new HttpClient() )
    {
        Task<string> task = client.GetStringAsync("http://csharpindepth.com");
        string text = await task;
        label.Text = text.Length.ToString();
    }
}

У asyncключове слово означає « Ця функція, коли вона називається, не буде викликатися в контексті , в якому його завершення потрібно для всього після того, як його заклик називати.»

Іншими словами, написати це посеред якогось завдання

int x = 5; 
DisplayWebsiteLength();
double y = Math.Pow((double)x,2000.0);

, оскільки не DisplayWebsiteLength()має нічого спільного з xабо y, призведе DisplayWebsiteLength()до виконання "у фоновому режимі", наприклад

                processor 1                |      processor 2
-------------------------------------------------------------------
int x = 5;                                 |  DisplayWebsiteLength()
double y = Math.Pow((double)x,2000.0);     |

Очевидно, що це дурний приклад, але я виправданий чи я зовсім збентежений чи що?

(Крім того, я плутаюся з приводу того, чому senderі eніколи не використовуються в тілі зазначеної вище функції.)



senderі eприпускають, що це насправді обробник подій - майже єдине місце, де async voidбажано. Швидше за все, це викликається натисканням кнопки або чимось подібним - в результаті цього ця дія відбувається повністю асинхронно щодо решти програми. Але все це все на одній нитці - потоці інтерфейсу користувача (з невеликим проміжком часу на потоці IOCP, який розміщує зворотний виклик у потоці інтерфейсу користувача).
Луаан


3
Дуже важлива примітка щодо DisplayWebsiteLengthзразка коду: Ви не повинні використовувати HttpClientу usingвиписці - Під великим навантаженням код може вичерпати кількість наявних сокетів, що призводить до помилок SocketException. Більше інформації про неналежну інстанцію .
Ган

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

Відповіді:


589

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

Зазвичай допомагає аналогія. Ви готуєте в ресторані. Приходить замовлення на яйця та тости.

  • Синхронно: ви варите яйця, потім готуєте тост.
  • Асинхронний, однопоточний: ви починаєте приготування яєць і встановлюєте таймер. Ви починаєте приготування тостів і встановлюєте таймер. Поки вони обидва готують, ви прибираєте кухню. Коли таймери вийдуть, ви знімаєте яйця з тепла і тостів з тостеру і подаєте їх.
  • Асинхронний, багатопоточний: ви наймаєте ще двох кухарів, одного для варіння яєць та одного для приготування тостів. Тепер у вас є проблема координувати кухарів, щоб вони не конфліктували між собою на кухні при обміні ресурсами. І ви повинні їх заплатити.

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

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

Тож давайте розглянемо приклад Йона детальніше. Що станеться?

  • Хтось викликає DisplayWebSiteLength. ВООЗ? Нам все одно.
  • Він встановлює мітку, створює клієнта і просить клієнта щось взяти. Клієнт повертає об'єкт, що представляє завдання щось отримати. Це завдання виконується.
  • Чи йдеться про іншу нитку? Напевно, ні. Прочитайте статтю Стівена про те, чому немає нитки.
  • Тепер ми чекаємо завдання. Що станеться? Ми перевіряємо, чи виконано завдання між часом, коли ми його створили, і ми його очікували. Якщо так, то ми отримуємо результат і продовжуємо працювати. Припустимо, він не завершився. Ми реєструємо залишок цього методу як продовження цього завдання і повертаємося .
  • Тепер контроль повернувся до абонента. Що це робить? Що хоче.
  • Тепер припустимо, що завдання виконане. Як це зробили? Можливо, він працював на іншій потоці, або, можливо, той, хто щойно повернувся, дозволив йому запуститись до завершення поточного потоку. Незалежно від цього, у нас зараз виконане завдання.
  • Виконане завдання запитує правильний потік - знову ж, мабуть, єдиний потік - запустити продовження завдання.
  • Контроль переходить негайно назад до методу, який ми лише залишили на місці очікування. Тепер це результат доступний , щоб ми могли призначити textі запустити іншу частину методу.

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


8
@ user5648283: апаратне забезпечення - це неправильний рівень для роздуму над завданнями. Завдання - це просто об'єкт, який (1) представляє, що значення стане доступним у майбутньому і (2) може запустити код (у правильному потоці), коли це значення буде доступне . Те, як будь-яке окреме завдання отримує результат у майбутньому, залежить від нього. Деякі використовуватимуть для цього спеціальне обладнання, наприклад "диски" та "мережеві карти"; деякі використовуватимуть таке обладнання, як процесори.
Ерік Ліпперт

13
@ user5648283: Ще раз подумайте про мою аналогію. Коли хтось попросить вас приготувати яйця і тости, ви використовуєте спеціальне обладнання - плиту і тостер - і ви можете прибирати кухню, поки обладнання виконує свою роботу. Якщо хтось попросить вас яєць, тостів та оригінальної критики останнього фільму про Хоббіта, ви можете написати свій відгук, поки готуються яйця та тости, але для цього вам не потрібно використовувати обладнання.
Ерік Ліпперт

9
@ user5648283: Що стосується вашого питання про "перестановку коду", врахуйте це. Припустимо, у вас є метод P, який має прибутковість, і метод Q, який робить випередження щодо результату P. Крок через код. Ви побачите, що ми запускаємо трохи Q, потім трохи P, потім трохи Q ... Ви розумієте сенс цього? очікувати - це по суті повернення прибутків у модному вбранні . Тепер це зрозуміліше?
Ерік Ліпперт

10
Тостер - це обладнання. Програмному забезпеченню не потрібна нитка для його обслуговування; диски та мережеві картки і те, що не працює на рівні, значно нижчому від потоку ОС.
Ерік Ліпперт

5
@ShivprasadKoirala: Це зовсім не так . Якщо ви вірите в це, то у вас є дуже помилкові переконання щодо асинхронії . Вся суть асинхронії в C # в тому, що вона не створює нитку.
Ерік Ліпперт

27

В браузері Javascript - прекрасний приклад асинхронної програми, яка не має потоків.

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

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

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

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

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

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

Поки async/awaitключові слова не були додані, C # виявився набагато більш очевидним щодо того, як викликається код зворотного виклику, оскільки ці зворотні виклики були у формі делегатів, які ви пов’язали із завданням. Щоб все-таки надати вам користь від використання ...Async()операції, уникаючи складності в коді, async/awaitабстрагує створення цих делегатів. Але вони все ще там у складеному коді.

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


Існує лише одна «нитка» JavaScript - це вже не відповідає версії Web Workers .
олексій

6
@oleksii: Це технічно правда, але я не збирався займатися цим, оскільки сам API Web Workers є асинхронним, а веб-працівникам заборонено безпосередньо впливати на значення javascript або DOM на веб-сторінці, на яку вони викликаються. з, що означає, що вирішальний другий абзац цієї відповіді все ще справедливий. З точки зору програміста, невелика різниця між викликом Web Worker і викликом запиту AJAX.
Стриптинг-воїн
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.