Каким способом многопоточный async runtime запускает future, которая не реализует Send, без передачи её между рабочими потоками?
Такую future запускают в локальном исполнителе текущего потока, например через механизм spawn_local и LocalSet в Tokio. Она может выполняться только на одном потоке и не мигрирует между worker-потоками, поэтому от неё не требуется Send.
Это не делает future потокобезопасной и не преобразует её в Send: runtime лишь предоставляет более строгое ограничение планирования.
Модель async/await создавалась для большого числа задач, которые большую часть времени ожидают ввод-вывод. Многопоточные runtime обычно распределяют такие задачи между worker-потоками, поэтому перемещаемая между ними future должна реализовывать Send.
Однако некоторые значения, например Rc, RefCell или локальные API конкретного потока, намеренно не являются потокобезопасными. Для них нужен локальный исполнитель, который сохраняет задачу на одном потоке вместо требования сделать все используемые типы Send.
Если передать !Send future в обычный многопоточный механизм запуска, runtime потенциально сможет приостановить её на одном потоке, а продолжить на другом. Состояние future может содержать Rc, RefCell или другие данные, которые нельзя безопасно перемещать между потоками.
Ошибочное решение приводит к отказу компиляции при обычном spawn, а попытка обойти ограничение через небезопасный код может создать гонку данных или нарушение инвариантов типа. При этом сам факт отсутствия Send не означает, что future нельзя выполнять вообще: ей требуется другой режим планирования.
Локальный исполнитель принимает future с ограничением !Send и гарантирует, что все её проверки состояния выполняются на одном потоке. В Tokio для этого обычно используют LocalSet и вызывают spawn_local внутри области, обслуживаемой этим LocalSet; конкретный API зависит от runtime.
Rc<RefCell<_>> в примере делает future !Send, но локальный исполнитель не пытается передать её другому worker-потоку. Многопоточность runtime при этом не исчезает: другие обычные Send-задачи могут работать на пуле, а локальная группа обслуживается на потоке, который исполняет LocalSet.
Главное ограничение — отсутствие параллельного выполнения локальных задач между потоками. Если локальная future долго выполняет CPU-bound работу или блокирующую операцию, она блокирует поток, на котором работает локальный исполнитель. Для масштабирования обычно заменяют Rc на Arc, RefCell на подходящий синхронизированный тип и используют обычный многопоточный spawn, если это допускает логика приложения.
Локальный режим также не устраняет необходимость кооперативного планирования: future должна регулярно возвращать управление через точки ожидания. !Send определяет возможность перемещения между потоками, но не гарантирует отсутствие блокировок, корректность алгоритма или отсутствие логических ошибок.
Сервис использует библиотеку графического контекста, который разрешено обращаться только из одного потока. Обработчик запроса создаёт async-задачу, но контекст содержит потоково-локальный объект и потому делает future !Send.
Рассматривались три варианта. Передача контекста через Arc<Mutex<_>> не подходит, если сама библиотека запрещает вызовы из других потоков; преобразование всех данных в Send возможно только после замены библиотеки или её обёртки; выделение отдельного потока с локальным runtime сохраняет требования библиотеки, но усложняет обмен сообщениями.
Выбран локальный исполнитель на выделенном потоке. Запросы передают ему команды через канал, а результат возвращается обратно через ответный канал. Это сохраняет потоковую привязку контекста, делает границу взаимодействия явной и предотвращает блокировку worker-потоков, хотя добавляет задержку обмена и ограничивает горизонтальное масштабирование самого контекста.
spawn_local future потокобезопасной?Нет. spawn_local не добавляет реализации Send и не меняет свойства содержащихся типов. Он лишь обещает не перемещать future между потоками; если тот же объект понадобится передать в другой поток, ограничение снова проявится.
!Send, даже если она создаётся внутри многопоточного runtime?Send проверяется по типу самой future, то есть по значениям, которые входят в её сохранённое состояние. Переменная Rc или активное заимствование через RefCell может попасть в это состояние между точками ожидания. Число потоков runtime не превращает такие значения в потокобезопасные.
Корректный локальный исполнитель этого не допускает: он должен продолжать poll той же future на закреплённом потоке. Если задача действительно нуждается в миграции или параллельном выполнении, её состояние и все значения, живущие между ожиданиями, должны удовлетворять требованиям многопоточного запуска, обычно включая Send и подходящие ограничения времени жизни.