Базовый CS с нуля
Зачем нужен async
Из Блока 03 ты знаешь, что CPU выполняет цикл выборки–декодирования–исполнения, и делает это быстро — одна инструкция примерно за миллиардную долю секунды. С такой скоростью внутри процессора происходят арифметика, сравнения и переходы.
Но программа делает больше, чем арифметику. Она читает файл с диска. Она спрашивает у другого компьютера через сеть какие-то данные. Это не инструкции, которые CPU выполняет сам, — это запросы, отправленные медленному оборудованию, живущему вне CPU. И медленно здесь не значит «чуть-чуть медленнее». Это значит в миллионы раз медленнее.
Так что у программы есть проблема. Она попросила у диска файл, и ответ придёт ещё не скоро. Что CPU должен делать тем временем? Этот урок отвечает на этот вопрос — и ответ есть причина, по которой вообще существует целое семейство приёмов под названием async.
После этого урока ты сможешь сказать, сколько занимает инструкция CPU по сравнению с запросом к диску или сети, объяснить, что значит «CPU простаивает» и почему это расточительно, дать определение async как идеи, что CPU делает другую полезную работу, пока медленное устройство отвечает, и противопоставить её синхронной альтернативе — просто ждать.
CPU быстрый: цикл — примерно наносекунда. Вспомни цикл выборки–декодирования–исполнения из Блока 03. Современный CPU выполняет этот цикл миллиарды раз в секунду. Полезное круглое число: одна инструкция занимает примерно одну наносекунду — одну миллиардную долю секунды.
Наносекунду слишком мало, чтобы представить напрямую, поэтому используй масштаб. Если один цикл CPU растянуть до одной секунды, то CPU выполняет миллиард «секунд» работы за каждую реальную секунду. В этом воображаемом масштабе CPU делает один шаг в секунду, ровно, без пауз. С такой скоростью происходят вся арифметика, присваивания переменных и переходы программы.
Медленное устройство — в миллионах циклов от тебя. Устройство — это часть оборудования вне CPU: диск, хранящий файлы, или сетевая карта, отправляющая и принимающая данные по кабелю. Когда программе нужен файл или ответ от другого компьютера, CPU отправляет запрос одному из этих устройств, и устройство тратит время на ответ.
Сколько времени? Жёсткий диск тратит примерно десять миллисекунд, чтобы найти и вернуть данные. Сетевой запрос к другому компьютеру может занять десятки или сотни миллисекунд. Миллисекунда — это тысячная доля секунды, и CPU выполняет примерно один миллион циклов за одну миллисекунду.
Значит, чтение с диска за десять миллисекунд — это примерно десять миллионов циклов CPU. В масштабе из Шага 1 — один цикл растянут до одной секунды — десять миллионов циклов это примерно четыре месяца. CPU задал диску вопрос, и ответ занимает, в терминах CPU, треть года.
Простой: расточительство. Вот наивная вещь, которую программа могла бы сделать после отправки запроса медленному устройству: ничего. Она велит CPU остановиться и ждать ответа устройства, прежде чем выполнять следующую инструкцию. Это называется простоем — CPU включён, но не делает полезной работы; он просто ждёт.
Подумай, во что это обходится. За десять миллисекунд чтения с диска CPU мог бы выполнить десять миллионов инструкций. Если он вместо этого простаивает, эти десять миллионов мест под инструкции потеряны — потрачены ни на что. Оборудование способно на огромную работу и не используется ни для чего.
Для одного чтения с диска это расточительно. Для программы, делающей тысячи запросов к диску и сети — скажем, веб-сервер, отвечающий многим пользователям, — простой при каждом запросе сделал бы машину почти полностью бесполезной. CPU тратил бы почти всё время на ожидание и почти нисколько — на вычисления.
Почему это работает
Почему устройство такое медленное — оно сломано? Нет. CPU быстрый, потому что работает с электрическими сигналами внутри крошечного чипа, на расстояниях, измеряемых нанометрами. Диску приходится физически двигать читающую головку и ждать вращающуюся пластину; сетевому запросу приходится пройти по кабелю, возможно через всю страну, и вернуться. Это связано с реальным физическим движением или реальным расстоянием. Предел задаёт физика, а не дефект. Устройство нельзя сделать таким же быстрым, как CPU, поэтому программу нужно устроить так, чтобы она справлялась с этим промежутком.
Async: делай другую полезную работу, пока ждёшь. Выход — не простаивать. Когда программа отправляет запрос медленному устройству, ей не обязательно останавливаться. Она может позволить CPU продолжить и выполнять другие инструкции — работу, не зависящую от ответа устройства, — и вернуться к обработке ответа, когда тот действительно придёт.
Эта идея называется async, сокращение от asynchronous (асинхронный). Слово означает «не в ногу»: запрос и его ответ не связаны жёстко во времени. Программа выпускает запрос, держит CPU занятым другой работой, а ответ устройства обрабатывается позже, когда бы он ни появился.
Противоположный подход — выпустить запрос, затем остановиться и ждать ответа, прежде чем делать что-либо ещё, — называется синхронным: программа и устройство движутся в ногу. Синхронный подход проще для понимания, но это ровно то поведение «простаивать» из Шага 3. Async существует именно для того, чтобы избежать этого расточительства: он держит быстрый CPU занятым на протяжении огромного промежутка времени, который открывает медленное устройство.
Подсчёт потраченных впустую циклов.
Программа делает одно чтение с диска, занимающее 10 миллисекунд. CPU работает со скоростью один цикл за наносекунду — значит, он может выполнить 1 000 000 циклов за миллисекунду.
Синхронная версия. Программа спрашивает диск, затем простаивает, пока не придёт ответ.
- Время простоя: 10 миллисекунд.
- Циклов доступно за это время: 10 мс × 1 000 000 = 10 000 000 циклов.
- Полезных инструкций выполнено за время ожидания: 0.
- Потрачено впустую мест под инструкции: 10 000 000.
Async-версия. Программа спрашивает диск, затем держит CPU выполняющим другую работу, не нуждающуюся в файле — обновление счётчика, подготовку следующего запроса, обслуживание другого пользователя.
- Время простоя: всё те же 10 миллисекунд устройства.
- Но CPU не простаивает: он тратит эти 10 миллионов циклов на другие инструкции.
- Потрачено впустую мест под инструкции: 0.
Диск занимает 10 миллисекунд в любом случае — async не ускоряет диск. Что async меняет — это то, тратится ли CPU впустую за эти 10 миллисекунд. Синхронный подход выбрасывает десять миллионов циклов; async держит каждый из них в работе.
Частая ошибка
Частое заблуждение — что async делает медленное устройство быстрее. Это не так. Диск всё равно тратит свои 10 миллисекунд; сетевой запрос всё равно тратит свои сотни миллисекунд. Async не меняет ничего в устройстве. Он меняет только то, что CPU делает во время ожидания — простаивает (впустую) или занят другой работой (async). Общее время на собственный ответ устройства не меняется.
CPU выполняет примерно одну инструкцию за наносекунду. Сколько инструкций он может выполнить за одну миллисекунду (одну тысячную долю секунды)?
Чтение с диска занимает 10 миллисекунд. CPU выполняет 1 000 000 циклов за миллисекунду. Если CPU простаивает всё чтение, сколько циклов потрачено впустую?
То же чтение с диска за 10 миллисекунд, но программа использует async и держит CPU выполняющим другую работу. Сколько циклов потрачено впустую теперь?
Программа отправляет запрос медленному устройству, затем не выполняет других инструкций, пока не придёт ответ. Введи 1, если это синхронный подход, 2, если это async.
За сколько миллисекунд времени устройства async заставит чтение с диска длиной 10 миллисекунд завершиться?
Зачем существует async?
CPU выполняет примерно одну инструкцию за наносекунду, но медленное устройство — диск, сетевая карта — тратит миллисекунды на ответ, а это миллионы циклов CPU. Если программа велит CPU остановиться и ждать устройство, CPU простаивает: включён, но не делает полезной работы, выбрасывая каждый цикл ожидания. Этот подход с ожиданием называется синхронным. Async (асинхронный) — это альтернатива: программа отправляет запрос, позволяет CPU выполнять другую полезную работу, не зависящую от ответа, и обрабатывает ответ позже, когда тот придёт. Async не делает устройство быстрее — диск всё равно тратит своё время — он гарантирует, что быстрый CPU никогда не тратится впустую, пока работает медленное устройство. Остальная часть этого блока показывает, как это устроено на самом деле.