Какой инвариант должен соблюдать многопоточный async runtime при планировании одной future?
Многопоточный async runtime не должен одновременно вызывать poll для одной и той же future. В каждый момент времени у runtime должен быть единственный эксклюзивный доступ к её состоянию; между вызовами poll задача может быть перенесена на другой поток.
Это ограничение следует из сигнатуры poll: она получает Pin<&mut Self>. Такой доступ исключает параллельную модификацию состояния future. При этом Waker может быть вызван из другого потока: runtime должен поставить задачу в очередь, но не начать повторный poll, пока текущий вызов ещё выполняется.
Модель Future в Rust отделяет описание асинхронной операции от её выполнения. Future хранит внутреннее состояние, а executor периодически проверяет его через poll, что позволяет одному потоку обслуживать множество задач без отдельного системного потока для каждой из них.
Для такой модели необходим безопасный способ изменять состояние future. Поэтому poll использует эксклюзивную ссылку, а runtime отвечает за то, чтобы эта ссылка не существовала одновременно в нескольких потоках.
Внутри future обычно находятся состояние сетевого запроса, позиция разбора данных, флаги завершения и значения, живущие между точками ожидания. Параллельный вызов poll мог бы одновременно изменить эти поля и привести к гонке данных, двойной регистрации обработчика или преждевременному переходу в состояние Ready.
Проблема может возникнуть даже в корректно работающей системе пробуждений: один поток может выполнять poll, пока другой поток вызывает сохранённый Waker. Поэтому вызов wake нельзя трактовать как разрешение немедленно повторно войти в poll.
Типичная реализация future имеет такой интерфейс:
Pin гарантирует ограничения на перемещение объекта, но сам по себе не является блокировкой и не делает параллельный poll безопасным. Эксклюзивность обеспечивается тем, что runtime владеет задачей или синхронизированно получает доступ к ней.
Если poll возвращает Pending, future обычно сохраняет переданный Waker или регистрирует его в источнике события. Источник может вызвать wake из другого потока. Runtime помещает задачу в очередь готовых к проверке, объединяя повторные пробуждения при необходимости.
Runtime обязан исключить повторный вход: пока задача находится в poll, она помечена как выполняемая. После возврата Pending или Ready эта отметка снимается, и только затем задача может быть снова выбрана для выполнения. Между двумя вызовами её состояние может мигрировать между worker-потоками, если future и все значения, сохраняемые через точки ожидания, удовлетворяют требованиям Send.
Важно различать свойства:
Pin<&mut Self>.В runtime с work-stealing одна задача ожидает сетевой ответ. Поток A вызывает её poll, после чего future возвращает Pending. Пока поток A ещё завершает обработку очереди, сетевой драйвер на потоке B вызывает wake.
Рассматривались три варианта:
poll из потока B — это создаёт риск повторного входа и гонки;poll только после завершения текущего вызова — это сохраняет безопасность и позволяет миграцию.Выбирается третий вариант. Поток B только отмечает задачу готовой, а любой worker вызывает poll после того, как runtime снял признак выполняемости с предыдущего вызова. В результате задача безопасно мигрирует между потоками, а параллельного доступа к её состоянию нет.
Send для одновременного вызова poll из двух потоков?Нет. Send разрешает передать владение future другому потоку, но не разрешает иметь два одновременных эксклюзивных доступа к одному объекту. Runtime может перемещать future между потоками между вызовами poll, однако одновременный poll всё равно запрещён.
Даже наличие Sync не превращает два параллельных вызова метода, изменяющего состояние через &mut, в безопасную операцию. Для совместного доступа потребовалась бы синхронизация, но обычная архитектура executor вместо этого предоставляет одному worker исключительное владение задачей.
wake быть вызван во время выполнения poll?Да. Например, источник события может обнаружить готовность операции до того, как текущий poll полностью завершится. Вызов wake должен лишь обеспечить последующую постановку задачи на проверку.
Runtime не должен немедленно рекурсивно вызывать poll той же future. Обычно пробуждение фиксируется флагом или помещается в очередь, а повторный вызов выполняется после выхода из текущего poll. Иначе нарушается эксклюзивность Pin<&mut Self>.
Одна и та же задача может попасть в очередь несколько раз и быть одновременно выбрана разными worker-потоками. Это приведёт к параллельному poll, повреждению внутреннего состояния future и потенциально к неопределённому поведению в небезопасной реализации executor.
Поэтому runtime обычно хранит состояние задачи: например, выполняется ли она, ожидает ли пробуждения и была ли поставлена в очередь. Повторные вызовы wake допустимы, но механизм планирования должен гарантировать, что они не превращаются в одновременные вызовы poll.