惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Stack Overflow Blog
Stack Overflow Blog
WordPress大学
WordPress大学
罗磊的独立博客
S
Secure Thoughts
Schneier on Security
Schneier on Security
博客园 - Franky
www.infosecurity-magazine.com
www.infosecurity-magazine.com
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
爱范儿
爱范儿
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
Hacker News: Ask HN
Hacker News: Ask HN
PCI Perspectives
PCI Perspectives
Google DeepMind News
Google DeepMind News
S
Security Affairs
SecWiki News
SecWiki News
博客园 - 聂微东
Security Archives - TechRepublic
Security Archives - TechRepublic
Google Online Security Blog
Google Online Security Blog
H
Heimdal Security Blog
S
Security @ Cisco Blogs
Engineering at Meta
Engineering at Meta
C
CXSECURITY Database RSS Feed - CXSecurity.com
Cloudbric
Cloudbric
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
V
Visual Studio Blog
P
Proofpoint News Feed
Project Zero
Project Zero
T
Threat Research - Cisco Blogs
Webroot Blog
Webroot Blog
Blog — PlanetScale
Blog — PlanetScale
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
W
WeLiveSecurity
Last Week in AI
Last Week in AI
月光博客
月光博客
Microsoft Azure Blog
Microsoft Azure Blog
M
MIT News - Artificial intelligence
有赞技术团队
有赞技术团队
S
Securelist
GbyAI
GbyAI
Application and Cybersecurity Blog
Application and Cybersecurity Blog
C
CERT Recently Published Vulnerability Notes
Recent Commits to openclaw:main
Recent Commits to openclaw:main
Cyberwarzone
Cyberwarzone
B
Blog RSS Feed
P
Palo Alto Networks Blog
H
Hacker News: Front Page
D
Docker
雷峰网
雷峰网
Latest news
Latest news
Microsoft Security Blog
Microsoft Security Blog

Все публикации подряд на Хабре

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет Midjourney в 2026? Мой немного грустный разбор этого шикарного инструмента Никто не любит писать тесты, но ИИ может исправить это IPv8 выглядит как мечта. Поэтому почти наверняка не взлетит Производители вернули в продажу материнки с DDR3. Что происходит? Управление агентом с телефона через Telegram теперь в KodaCode От координации к лидерству: как меняется роль руководителя разработки Я сделала родителям бизнес вместо пенсии: зарабатываем 70 тысяч, мама не даёт продать В три раза быстрее приемка товара и оптимизация трудозатрат на 73%: как «РСТ-Инвент» помог Gulliver Group ИИ-шечный мир победил? О влиянии искусственного интеллекта на игропром Кремль снижает давление на Телеграмм пока Европа строит интернет по паспорту Как CEO, CTO и CIO за 8 часов собрали ИИ-директора, который умеет держать позицию под давлением Как (не) потерять домен за выходные Вместо 8 разных VPS: как я организовал практику студентам на одном сервере Почему твой Open Source проект не замечают? R&D: искусство управления неопределенностью в разработке AI-дефляция: вакансий для разработчиков больше, а рост зарплат — худший за 15 лет Мы отдали управление роботами OpenClaw. Что из этого вышло Галактический ID: система идентификации для всех форм разумной жизни Шесть основ бизнес-анализа: начинаем с вопроса «Кто в игре?» Код-ревью, в котором дело не в коде Данные переехали. Команда — нет Системной подход к сдаче OSWE в 2025 Почему комната управления реактором покрашена в цвет морской пены 4 YAML-файла вместо PySpark: как аналитикам строить пайплайны без разработчиков LLM-агент для поиска свободных доменов: автоматизируем подбор Когда, зачем и как правильно начинать новую сессию в Claude Code? Как я заставил нейросеть писать макросы для FreeCAD Анатомия ИИ‑агента для подбора персонала. От тысячи резюме к топ‑10 за минуты Опыт разработчика как экономика внимания Автономность как точка невозврата: кто будет субъектом в цифровом будущем Обучение ИИ в «диких» условиях: как рутинные действия превращаются в датасеты Как измерить LLM для задач кибербеза: обзор открытых бенчмарков Где хранить код? Сравнение GitHub, GitLab и Bitbucket Математика объясняет, почему нормальное распределение встречается повсюду Почему ваш FinOps не работает: 12 тезисов от практиков Как подписать проектную документацию УКЭП с использованием бесплатных лицензий Pilot Адаптивное администрирование Sigla Vision Я грузил уран в бочки, а потом 20 лет строил ИТ в атомной отрасли Чем позвонить с Эвереста? История и обзор спутниковой связи. Часть 2 Как языковая модель помогает контролировать качество инструктажей по охране труда в металлургии Как не передать на desktop свой IP в РКН Анатомия SAP Privileges: как устроено управление правами в macOS MoneyDev: Сказка про три главных слова Обновлённый токенизатор видео K-VAE 2.0 от Сбера Как сделать диспетчеризацию дома на 1284 квартиры почти бесплатно Как мы разогнали железную дорогу Мы дали агентам рутину. Теперь надо решить — что делать с освободившимся временем Токсичный контент, промпт-хакинг и защита ИИ — всё о Guardrails для LLM Умный город начинается с точного взгляда: как «Фалькон Тех» меняет пространство к лучшему Навайбкодил приложение для анализа графов Почему Дюну так интересно читать? Упрощаем работу с рутиной или как стать Гендальфом Белым Деконструкция Go: CPU, RAM и что там происходит. Go Assembler база. Часть 1.1 Какие профессии исчезнут из-за ИИ, а какие появятся? И что с этим делать Как мы построили IT-отдел, где хочется расти: архитектурные встречи, прозрачные метрики и книжные подарки Rufler: Делаем из Claude Code автономный рой через один YAML-конфиг Sing-box и белый список приложений Как построить надёжный обмен сообщениями в микросервисах: лучшие практики для enterprise OpenAI строит MLM-пирамиду, а McKinsey и Accenture помогают ей в этом Дом, который не построил Фишер (Часть 2) «Сверхзвуковой математик» против «Вдумчивого логиста»: битва алгоритмов 3D-упаковки Мультимодальные модели – грубый и дорогой инструмент Разговоры ничего не стоят. Код тоже Проверки физических лиц: с кого начнет ФНС Топ-10 бесплатных нейросетей для создания видео в 2026 году Первые слои кода: как наши решения сегодня определяют архитектуру ИИ на десятилетия Разработка нового статического анализатора: PVS-Studio JavaScript Поиск уязвимостей ПО: базовый минимум или роскошный максимум Почему оценка персонала не работает как инструмент управления Как мы разработали ИИ-ассистента и сократили рутину продуктовой команды на 50% Как я ушел из найма, нажарил косточек и продал на маркетплейсах на 168 млн в год Когда 1С:ERP уже внедрена, а нормального производственного плана всё ещё нет Как я сделал Claude мультимодальным, подключив к нему Qwen Omni Как приглашение на вакансию мечты превращается в атаку Infrastructure as Code: философия и лучшие практики IaC Тестируем Yandex Code Assistant на задаче, в которой нужно хранить секреты nxs-universal-chart v3.0: новое поколение универсального Helm-чарта Callback Injection: Техника, которая отправила Microsoft Defender в глухой нокаут «Все идеи на стол»: митап как способ вывести проект из тупика Сегодня я узнал нечто новое о GPU благодаря багу в своей игре Как заставить LLM ̶ ̶г̶а̶л̶л̶ю̶ ̶ эволюционировать Карта событий как фундамент аналитики: практический кейс для E-commerce Что выбрать для AI: x86, ARM или RISC-V? Дайджест железа за март Роль соматических мутаций в развитии аутоиммунных заболеваний: путь к избирательной терапии Mythos от Anthropic — тревожный сигнал для всех, а не только для банков Guardrails для LLM на Java: как приручить промпт‑инъекции и токсичные ответы Green-VLA: как мы собрали VLA-модель для реального антропоморфного робота и не потеряли обобщение Финансовая гонка вооружений: почему умные люди добровольно в ней участвуют Эра ИИ-агентов наступила: выбираем лучшего цифрового сотрудника # Практический опыт внедрения WinCC Redundancy на производственном предприятии Сделал MVP за 3 дня, а потом неделю прикручивал оплату. Оно того стоило? Физика против Маска: почему Starship V3 может оказаться ещё одной катастрофой Нефть Венесуэлы: крупнейшие запасы в мире, но не крупнейшая нефтяная держава JPA 4. Переосмысление Hibernate Почему зеркальная фотокамера Nikon D5 десятилетней давности идеально подошла для миссии «Артемида-2» Проект «Уровень-Спутник» или как мы сделали платформу для гидрологов «Замедлиться, чтобы ускориться»: почему ИИ повышает цену ошибок в требованиях и архитектуре Как с нуля поднять трафик IT-компании на 1657% при бюджете 55 тыс. и выжить Pixel-perfect Downsampling — идеальная отрисовка 50 миллионов точек без потерь
Твой async fn на самом деле enum, а Pin нужен потому, что Rust наступил на грабли самоссылающихся структур
vibecodingai · 2026-05-12 · via Все публикации подряд на Хабре

Уровень сложностиСложный

Время на прочтение7 мин

Охват и читатели847

Мнение

TL;DR. Каждый async fn в Rust компилируется в enum-стейт-машину. Размер этой стейт-машины равен размеру самого толстого варианта, поэтому забытая через .await переменная на пару мегабайт превращается в утечку памяти, помноженную на число задач. Pin существует, чтобы запретить перемещать такие стейт-машины после первого poll, потому что внутри них живут указатели на собственные поля. select! молча теряет данные, если использовать в нём future без cancellation safety. И executor в Tokio, при всей его магии, концептуально умещается в сотню строк.

Содержание


Представь сервис на Tokio, который держит десять тысяч соединений и неожиданно начинает съедать восемь гигабайт памяти на ровном месте. В коде нет утечек, Arc всё считает корректно, valgrind молчит. Виновник — один безобидный async fn, в котором между двумя .await лежит массив на пару мегабайт. Компилятор честно положил его в стейт-машину, и теперь каждое из десяти тысяч соединений таскает за собой эту память. Чтобы такие истории перестали быть мистикой, надо перестать думать об async Rust как о чёрном ящике и разобрать три темы, которые в обычной жизни не встречаются: трансформацию async fn в стейт-машину, самоссылающиеся структуры и зачем Pin, и как на самом деле работает executor.

Под капотом async Rust устроен неожиданно просто и неожиданно жестоко: каждый твой async fn это сгенерированный компилятором enum, каждый .await это match по этому enum, а легендарный Pin, на который ругается половина туториалов, существует ровно потому, что без него вся эта конструкция разваливается на первом же mem::swap. В конце соберём свой собственный executor за двести строк, чтобы окончательно стало ясно, что Tokio это не магия.

async fn это синтаксический сахар над enum

Возьмём абсолютно ничего не делающую функцию:

async fn foo() -> u32 {
    let a = bar().await;
    let b = baz().await;
    a + b
}

Если поставить cargo install cargo-expand и запустить cargo expand, можно увидеть реальный enum, в который компилятор разворачивает эту функцию. В упрощённом виде он выглядит так:

enum FooState {
    Start,
    WaitingOnBar { bar_fut: BarFuture },
    WaitingOnBaz { a: u32, baz_fut: BazFuture },
    Done,
}

Каждый .await это точка приостановки, и для каждой такой точки в enum появляется свой вариант с локальными переменными, которые должны пережить приостановку. poll у этого Future это огромный match, который смотрит текущее состояние, вызывает вложенный future, и если тот вернул Pending, сохраняет состояние и возвращает Pending наверх. Если Ready — переходит в следующий вариант enum.

Размер future и почему твой сервис ест память

Размер future равен размеру самого толстого варианта enum. Если у тебя в async fn живёт [u8; 1_000_000] через одну .await-точку, твой future весит мегабайт, и Box::pin его аллоцирует на каждом вызове. Переменные, которые не пересекают .await, в enum не попадают, поэтому иногда переписать код так, чтобы временные значения дропались до await, реально уменьшает память.

Проверить размер собственного future можно прямо в рантайме:

println!("{}", std::mem::size_of_val(&my_async_fn()));

А на nightly есть полезный флаг, который покажет все типы вместе с их размерами:

cargo +nightly rustc -- -Zprint-type-sizes

В выводе ищи строчки про async-блоки. Реальный случай из практики: у меня была корутина, которая держала в памяти String с подробным сообщением об ошибке через границу .await, и весила 8 КБ на задачу. Замена String на Box<str> срезала размер до 16 байт. На десяти тысячах задач это разница в восемьдесят мегабайт.

Самоссылающиеся структуры и зачем Pin

Теперь самое интересное. Что если внутри async fn есть такой код:

async fn weird() {
    let s = String::from("hello");
    let r = &s;
    other().await;
    println!("{}", r);
}

Ссылка r указывает на s, и обе живут в одном и том же варианте enum-а. То есть в сгенерированной структуре есть поле, которое указывает на другое поле той же структуры. Это и называется самоссылающейся структурой. Пока структура лежит на месте, всё работает. Но если её переместить в памяти (а в Rust перемещение это просто memcpy байтов), указатель внутри останется указывать на старый адрес, и ты получишь dangling pointer без единого unsafe в коде пользователя.

Решение могло быть таким: запретить перемещать future после первого poll. Именно это и делает Pin<P>. Важно понимать, что Pin это не магия и не часть компилятора — это обычный библиотечный тип. Вся гарантия держится на одном unsafe fn Pin::new_unchecked плюс автотрейте Unpin. Если ты как-то получил Pin<&mut T> безопасным путём, значит кто-то на стороне (компилятор для async-блоков, Box::pin, pin! макрос) заплатил за это unsafe-обязательство. Все методы Future::poll принимают Pin<&mut Self> именно для того, чтобы вынудить вызывающую сторону пройти через эту дверь.

Тут есть популярное недоразумение. Pin<&mut T> не запрещает мутировать T. Он запрещает только перемещать его. Если T: Unpin (а это автотрейт, который реализован почти для всего, кроме самоссылающихся future и нескольких специальных типов), Pin вообще ничего не даёт и сквозит насквозь. Поэтому Pin<&mut i32> это просто странно написанный &mut i32. А вот Pin<&mut SomeAsyncBlock> это уже настоящее ограничение.

Waker, или как future вообще узнаёт, что пора просыпаться

Если future вернул Pending, executor должен как-то понять, когда его снова poll-ить. Тупой вариант — поллить в цикле — съест CPU. Поэтому в Context, который передаётся в poll, лежит Waker. Это типизированный Arc-подобный объект с одним методом wake(), который говорит «эй, тот future, которому я принадлежу, готов двигаться дальше».

Кто вызывает wake()? Тот, кто знает, что данные появились. Если ты ждёшь сетевого пакета, wake() вызовет реактор поверх epoll/kqueue/io_uring, когда сокет станет читаемым. Если ты ждёшь таймера, его вызовет таймер-тред. Если ты ждёшь канала, его вызовет тот, кто отправил сообщение. Executor получает сигнал и кладёт future обратно в очередь готовых к опросу.

Cancellation safety, или почему select! иногда теряет данные

Прежде чем писать executor, упомяну ещё одну штуку, на которую регулярно наступают даже опытные. tokio::select! опрашивает несколько future параллельно и при первой готовности отбрасывает остальные. Отбрасывает буквально — дропает. Если ты внутри отброшенной ветки уже считал часть данных из сокета, эти данные исчезли вместе с future.

Каноничный пример, на котором горят люди:

let mut buf = [0u8; 1024];
tokio::select! {
    result = socket.read_exact(&mut buf) => {
        // обработать buf
    }
    _ = timeout => {
        // таймаут
    }
}

Если таймаут сработал, когда read_exact уже прочитал из сокета 500 байт, эти 500 байт улетели в небытие вместе с дропнутым future. Следующая попытка чтения вычитает «обрезанный» поток, протокол сломается, и понять, что произошло, по логам почти невозможно. Это называется отсутствием cancellation safety, и в документации Tokio для каждого метода честно написано, безопасен ли он в select!. read_exact — нет, recv у канала — да. Эта мелочь стоит людям недель отладки, и из синтаксиса select! она вообще никак не видна.

Свой executor за двести строк

Соберём минимальный однопоточный executor. Без I/O, только чтобы стало ясно, как крутится механизм.

use std::collections::VecDeque;
use std::future::Future;
use std::pin::Pin;
use std::sync::{Arc, Mutex};
use std::task::{Context, Wake, Waker};

struct Task {
    future: Mutex<Pin<Box<dyn Future<Output = ()> + Send>>>,
    executor: Arc<Executor>,
}

impl Wake for Task {
    fn wake(self: Arc<Self>) {
        self.executor.queue.lock().unwrap().push_back(self.clone());
    }
}

struct Executor {
    queue: Mutex<VecDeque<Arc<Task>>>,
}

impl Executor {
    fn new() -> Arc<Self> {
        Arc::new(Self { queue: Mutex::new(VecDeque::new()) })
    }

    fn spawn(self: &Arc<Self>, fut: impl Future<Output = ()> + Send + 'static) {
        let task = Arc::new(Task {
            future: Mutex::new(Box::pin(fut)),
            executor: self.clone(),
        });
        self.queue.lock().unwrap().push_back(task);
    }

    fn run(&self) {
        loop {
            let task = match self.queue.lock().unwrap().pop_front() {
                Some(t) => t,
                None => break,
            };
            let waker = Waker::from(task.clone());
            let mut cx = Context::from_waker(&waker);
            let mut fut = task.future.lock().unwrap();
            let _ = fut.as_mut().poll(&mut cx);
        }
    }
}

Это умышленно упрощённая модель. В реальном Tokio очередь lock-free, future внутри задачи защищён хитрой схемой состояний, чтобы wake() изнутри poll не привёл к дедлоку, плюс отдельный механизм для I/O-реактора и пула потоков с work-stealing. Но концептуальное ядро именно такое: spawn кладёт future в очередь, run берёт по одному, делает poll, и если future вернул Pending, он просто выпадает из очереди до следующего wake(). wake() кладёт его обратно. Всё.

Если добавить сюда mio для epoll и таймер-колесо, получится примитивный аналог async-std. Если добавить ещё несколько потоков и стащить идею work-stealing у Go, получится почти Tokio. Если добавить io_uring, получится glommio или monoio.

Бонус: async fn в трейтах, и почему это было больно десять лет

С 2015 года в Rust ждали возможность писать async fn в трейтах. В стабильной версии это появилось только в конце 2023-го (AFIT, async fn in trait), и причина задержки прямо вытекает из всего написанного выше. Когда у тебя в трейте есть async fn foo(&self) -> u32, компилятор должен вернуть наружу какой-то конкретный тип future. Но этот тип у каждой реализации трейта свой, размер у него свой, и для dyn Trait это превращается в кошмар: vtable не может заранее знать размер возвращаемого future, потому что разные реализации генерируют разные стейт-машины.

Решение в стабильной версии умышленно неполное: async fn в трейтах работает для impl Trait, но не для dyn Trait. Для dyn приходится явно писать fn foo(&self) -> Pin<Box<dyn Future<Output = u32> + '_>> и платить за бокс. Это не косметическая придирка — на этом сейчас стоит весь дизайн трейт-объектов в async-экосистеме, и крейт async-trait десять лет существовал именно потому, что он автоматизировал это боксирование.

Что с этим делать сегодня

Если ты пишешь обычный сервис на Tokio, можно расслабиться. Но как только начинаются нестандартные вещи — свой Stream, ручной Future, прокидывание данных между задачами через select! — стоит держать в голове четыре правила:

  1. Размер future имеет значение. Прогоняй -Zprint-type-sizes хотя бы раз перед релизом, и Box::pin для большого future это не лень, а оптимизация.

  2. .await это граница для Send. Если планируешь tokio::spawn, всё, что живёт через .await, должно быть Send, и если компилятор ругается на !Send future, виноват, скорее всего, Rc или RefCell, который зацепился через границу await.

  3. select! без cancellation safety это мина. Перед использованием чего-либо в select! смотри в доке cancellation safety, и если её там нет — считай, что небезопасно.

  4. cargo expand твой друг. Если не понимаешь, что компилятор сделал с твоим async-блоком, не гадай, посмотри глазами.

Финальная мысль

Когда в очередной раз увидишь в стеке трейса Pin<&mut dyn Future> и Poll::Pending, не закрывай терминал. Это компилятор показывает, что между твоим представлением о «функции, которая просто ждёт» и его представлением — сгенерированной стейт-машиной с самоссылающимися полями — никакого зазора нет, всё сходится по битам. И ровно поэтому async Rust способен держать миллионы соединений в одном процессе без потери производительности, чего другие языки добиваются только ценой отдельного рантайма со своим сборщиком мусора и собственными зелёными потоками.


Люблю Rust, пишу на нём и разбираю код так, чтобы сложные вещи становились понятнее — подписывайтесь: t.me/rust_code. А здесь я собрал репо с роадмэпом по Rust, для входа в мой любимый язык, надеюсь, будет полезно.

Спасибо за внимание, пишите в комментах ваши замечания!