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