Чи використовується структура стека для процесів асинхронізації?


10

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

З його відповіді:

стек - це частина переробки продовження мовою без супротивників.

Зокрема, без супротивної частини цього мене цікавить.

Він пояснює тут трохи більше:

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

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

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

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

Відповіді:


14

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

В основному так.

Припустимо, у нас є

async void MyButton_OnClick() { await Foo(); Bar(); }
async Task Foo() { await Task.Delay(123); Blah(); }

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

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

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

Foo викликає Task.Delay, ставлячи зворотну адресу себе в стек.

Task.Delay виконує будь-яку магію, яку потрібно зробити, щоб негайно повернути завдання. Стек вискакує і ми знову в Фоо.

Foo перевіряє повернене завдання, щоб побачити, чи його завершено. Це не так. Продовження в AWAIT є виклик Ла (), тому Foo створює делегат , який викликає Л (), а також ознаки того, що делегат в якості продовження виконання цього завдання. (Я щойно зробив невелику помилкову заяву; ти її зловив? Якщо ні, ми розкриємо її через мить.)

Тоді Foo створює власний об’єкт Task, позначає його як незавершений та повертає його в стек обробнику кліків.

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

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

Тепер що відбувається? Ось хитрий шматочок. Продовження завдання затримки не викликає лише Blah (). Він також повинен викликати дзвінок у Bar () , але це завдання не знає про Bar!

Foo фактично створив делегата, який (1) викликає Blah (), а (2) викликає продовження завдання, яке створив Foo і передав обробнику події. Ось так ми називаємо делегата, який викликає Bar ().

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

Те, що ці сценарії занадто вдосконалені для стека, має ідеальний сенс, але що замінює стек?

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

Вправа: як ви гадаєте, що це все працює у світі без циклів повідомлень? Наприклад, консольні програми. чекати в консольному додатку зовсім інше; чи можна вивести, як це працює з того, що ви знаєте досі?

Коли я дізнався про це років тому, стек був там, тому що він був блискавичним і легким, шматок пам’яті, що виділявся при застосуванні подалі від купи, оскільки він підтримував високоефективне управління для виконання завдання (каламбур?). Що змінилося?

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

Стек - це лише масив кадрів, де кадри містять (1) покажчики на середину функцій (де відбувся виклик) та (2) значення локальних змінних та темпів. Продовження завдань - це одне і те ж: делегат - це вказівник на функцію, і він має стан, який посилається на конкретну точку в середині функції (там, де очікування відбулося), а у закриття є поля для кожної локальної змінної чи тимчасової . Кадри просто не утворюють приємного акуратного масиву, але вся інформація однакова.


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

3
@ jdl134679: Я б запропонував вам позначити щось як відповідь, якщо ви вважаєте, що на ваше питання відповіли; який посилає сигнал, що люди повинні прийти сюди, якщо вони хочуть прочитати гарну відповідь, а не написати одну. (Звичайно, завжди рекомендується писати хороші відповіді.) Мені все одно, хто отримає галочку.
Ерік Ліпперт

8

Апель написав, що старий збір сміття з паперу може бути швидшим, ніж розподіл стека . Прочитайте також його книгу " Збірник із продовженнями" та посібник зі збору сміття . Деякі методи GC є (ненавмисно) дуже ефективними. Проходження стиль продовження визначає канонічне ціле програми перетворення (СУЗ перетворення ) , щоб позбутися від стопок (концептуально замінюють кадри викликів з купою виділяються затворами , іншими слова «матеріалізація» окремі кадри виклику в вигляді окремих «цінностей» або «об'єкти» ).

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

Практично кажучи, стек дзвінків все ще є тут. Але зараз у нас їх багато, і іноді стек викликів розбивається на багато менших сегментів (наприклад, на кілька сторінок по 4 Кбайт кожна), які іноді збирають сміття або виділяють купу. Ці сегменти стека можуть бути організовані в деякому пов'язаному списку (або в більш складній структурі даних, коли це необхідно). Так , наприклад, GCC компілятори мають в -fsplit-stackваріант (особливо корисно для Go, і його «goroutines» і його «асинхронних процесів»). З розділеними стеками у вас може бути багато тисяч стеків (а спільні підпрограми стають легшими у застосуванні), виготовлені з мільйонів невеликих сегментів стека, і "розмотування" стека може бути швидшим (або принаймні майже таким же швидким, як з однократкою стек).

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

Дивіться також це і те та багато робіт (наприклад, це ), що обговорюють трансформацію CPS. Читайте також про ASLR & call / cc . Прочитайте (& STFW) докладніше про продовження .

Реалізації .CLR & .NET можуть не мати сучасних перетворень GC і CPS з багатьох прагматичних причин. Це компроміс, пов'язаний з трансформацією цілої програми (та простота використання підпрограм C низького рівня, а також час виконання кодується в C або C ++).

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


Читайте також SICP , Прагматика мови програмування , Книга Драконів , Слухай маленькими шматочками .


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