Емпіричні докази: чому однопотокові корутини працюють

Твердження, що однопотокова кооперативна конкурентність є ефективною для IO-bound навантажень, підтверджується вимірами, академічними дослідженнями та операційним досвідом великомасштабних систем.


1. Вартість перемикання: корутина vs потік ОС

Головна перевага корутин полягає в тому, що кооперативне перемикання відбувається в просторі користувача, без виклику ядра ОС.

Виміри на Linux

МетрикаПотік ОС (Linux NPTL)Корутина / async-завдання
Перемикання контексту1.2–1.5 мкс (pinned), ~2.2 мкс (unpinned)~170 нс (Go), ~200 нс (Rust async)
Створення завдання~17 мкс~0.3 мкс
Пам'ять на завдання~9.5 KiB (мін.), 8 MiB (стек за замовч.)~0.4 KiB (Rust), 2–4 KiB (Go)
Масштабованість~80 000 потоків (тест)250 000+ async-завдань (тест)

Джерела:

Що це означає на практиці

Перемикання корутини коштує ~200 наносекунд — на порядок дешевше, ніж перемикання потоку ОС (~1.5 мкс). Але ще важливіше те, що перемикання корутин не має непрямих витрат: скидання кешу TLB, інвалідація предиктора розгалужень, міграція між ядрами — все це характерно для потоків, але не для корутин у межах одного потоку.

Для event loop, що обслуговує 80 корутин на ядро, загальні накладні витрати на перемикання становлять:

80 × 200 нс = 16 мкс на повний цикл по всіх корутинах

Це нехтовно мало порівняно з 80 мс часу очікування I/O.


2. Пам'ять: масштаб відмінностей

Потоки ОС виділяють стек фіксованого розміру (8 MiB за замовчуванням на Linux). Корутини зберігають лише свій стан — локальні змінні та точку відновлення.

РеалізаціяПам'ять на одиницю конкурентності
Потік Linux (стек за замовч.)8 MiB віртуальної, ~10 KiB RSS мінімум
Горутина Go2–4 KiB (динамічний стек, росте за потребою)
Корутина Kotlinдесятки байтів у heap; співвідношення потік:корутина ≈ 6:1
Async-завдання Rust~0.4 KiB
Фрейм корутини C++ (Pigweed)88–408 байтів
Корутина Python asyncio~2 KiB (проти ~5 KiB + 32 KiB стек для потоку)

Джерела:

Наслідки для веб-серверів

Для 640 конкурентних завдань (8 ядер × 80 корутин):

  • Потоки ОС: 640 × 8 MiB = 5 GiB віртуальної пам'яті (насправді менше завдяки лінивому виділенню, але тиск на планувальник ОС є значним)
  • Корутини: 640 × 4 KiB = 2.5 MiB (різниця у три порядки)

3. Проблема C10K та реальні сервери

Проблема

У 1999 році Dan Kegel сформулював проблему C10K: сервери, що використовують модель «один потік на з'єднання», не могли обслужити 10 000 одночасних з'єднань. Причиною були не обмеження апаратного забезпечення, а накладні витрати потоків ОС.

Рішення

Проблема була вирішена переходом до подієво-орієнтованої архітектури: замість створення потоку для кожного з'єднання один event loop обслуговує тисячі з'єднань в одному потоці.

Саме такий підхід реалізований у nginx, Node.js, libuv та — в контексті PHP — True Async.

Бенчмарки: nginx (подієво-орієнтований) vs Apache (потік на запит)

Метрика (1000 одночасних з'єднань)nginxApache
Запитів на секунду (статика)2 500–3 000800–1 200
Пропускна здатність HTTP/2>6 000 req/s~826 req/s
Стабільність під навантаженнямСтабільнийДеградація при >150 з'єднаннях

nginx обслуговує у 2–4 рази більше запитів, ніж Apache, споживаючи при цьому значно менше пам'яті. Apache з архітектурою потік-на-запит приймає не більше 150 одночасних з'єднань (за замовчуванням), після чого нові клієнти чекають у черзі.

Джерела:


4. Академічні дослідження

SEDA: Staged Event-Driven Architecture (Welsh et al., 2001)

Matt Welsh, David Culler та Eric Brewer з UC Berkeley запропонували SEDA — серверну архітектуру на основі подій та черг між етапами обробки.

Ключовий результат: Сервер SEDA на Java перевершив Apache (C, потік-на-з'єднання) за пропускною здатністю при 10 000+ одночасних з'єднаннях. Apache не міг прийняти більше 150 одночасних з'єднань.

Welsh M., Culler D., Brewer E. SEDA: An Architecture for Well-Conditioned, Scalable Internet Services. SOSP '01 (2001). PDF

Порівняння архітектур веб-серверів (Pariag et al., 2007)

Найбільш ретельне порівняння архітектур провели Pariag et al. з Університету Ватерлоо. Вони порівняли три сервери на одній кодовій базі:

  • µserver — подієво-орієнтований (SYMPED, один процес)
  • Knot — потік-на-з'єднання (бібліотека Capriccio)
  • WatPipe — гібридний (конвеєр, подібний до SEDA)

Ключовий результат: Подієво-орієнтований µserver та конвеєрний WatPipe забезпечили на ~18% вищу пропускну здатність, ніж потоковий Knot. WatPipe потребував 25 потоків запису для досягнення тієї ж продуктивності, що µserver з 10 процесами.

Pariag D. et al. Comparing the Performance of Web Server Architectures. EuroSys '07 (2007). PDF

AEStream: прискорення обробки подій за допомогою корутин (2022)

Дослідження, опубліковане на arXiv, провело пряме порівняння корутин та потоків для потокової обробки даних (event-based processing).

Ключовий результат: Корутини забезпечили щонайменше 2x пропускну здатність порівняно зі звичайними потоками для обробки потоку подій.

Pedersen J.E. et al. AEStream: Accelerated Event-Based Processing with Coroutines. (2022). arXiv:2212.10719


5. Масштабованість: 100 000 завдань

Kotlin: 100 000 корутин за 100 мс

У бенчмарку TechYourChance створення та запуск 100 000 корутин зайняли ~100 мс накладних витрат. Еквівалентна кількість потоків потребувала б ~1.7 секунди лише на створення (100 000 × 17 мкс) та ~950 MiB пам'яті для стеків.

Rust: 250 000 async-завдань

У бенчмарку context-switch 250 000 async-завдань було запущено в одному процесі, тоді як потоки ОС досягли ліміту на ~80 000.

Go: мільйони горутин

Go регулярно запускає сотні тисяч і мільйони горутин у production-системах. Саме це дозволяє серверам на кшталт Caddy, Traefik та CockroachDB обробляти десятки тисяч одночасних з'єднань.


6. Підсумок доказів

ТвердженняПідтвердження
Перемикання корутин дешевше за потоки~200 нс проти ~1500 нс — 7–8x (Bendersky 2018, Blandy)
Корутини споживають менше пам'яті0.4–4 KiB проти 9.5 KiB–8 MiB — 24x+ (Blandy, Go FAQ)
Подієво-орієнтований сервер масштабується кращеnginx 2–4x пропускна здатність проти Apache (бенчмарки)
Подієво-орієнтований > потік-на-з'єднання (академічно)+18% пропускна здатність (Pariag 2007), C10K вирішено (Kegel 1999)
Корутини > потоки для обробки подій2x пропускна здатність (AEStream 2022)
Сотні тисяч корутин в одному процесі250K async-завдань (Rust), 100K корутин за 100мс (Kotlin)
Формула N ≈ 1 + T_io/T_cpu є правильноюGoetz 2006, Zalando, закон Літтла

Джерела

Виміри та бенчмарки

Академічні роботи

  • Welsh M. et al. SEDA: An Architecture for Well-Conditioned, Scalable Internet Services. SOSP '01. PDF
  • Pariag D. et al. Comparing the Performance of Web Server Architectures. EuroSys '07. PDF
  • Pedersen J.E. et al. AEStream: Accelerated Event-Based Processing with Coroutines. arXiv:2212.10719

Індустріальний досвід

Дивіться також