Комментарии 22
Я понимаю, что статья посвящена использованию ASIO, но у меня по первому "наивному" примеру с использованием треда на каждый запрос, есть такой вопрос - почему не сделать тред не на запрос, а на диск, и какую то очередь с заданиями (может несколько, если нужны приоритеты)?
Кажется, что такое решение заняло бы несколько десятков строк, но было бы полностью кросс платформенным, и не тянуло бы за собой достаточно увесистую библиотеку?
Если поток запросил операцию ввода-вывода обычным линуховым API (не io_uring), он останавливается до завершения операции, так как обычные операции являются чисто синхронными. Соответственно, несколько операций ввода-вывода одновременно выполняться не будут, завершение их будет происходить в строго определённом порядке и т.п. -- а не как при асинхронном вводе-выводе.
Вопрос отличный!) Дело в том что если смотреть на внутренности asio то там в некоторых случаях так и происходит. Вы можете создать asio::thread_pool и использовать его только с одним диском. Да на самом деле создать поток, и запустить в нем asio::io_context сделает то же самое. И потом использовать thread_pool или io_context (библиотека называет их execution_contexts) вместе с нужным диском. В каждом контексте действительно есть своя очередь которая и будет разгребаться потоком/потоками.
Если говорить про размер asio, позволю себе отметить что standalone версия библиотеки представляет из себя header only библиотеку что, по моему скромному мнению, достаточно легковесно:)
на Windows используется winiocp
Странное какое-то название. Не гуглится совсем. Нет ли у вас про него ссылки, чтобы можно было понять, что это вообще?
И непонятно зачем оно нужно - в Windows, ещё со времен NT поддерка асинхронного ввода-вывода для любых устройств встроена прямо в планировщик операций ввода-вывода в ядре, и доступна через стандартные системные вызовы ReadFile/WriteFile Win32 API(т.е. NtReadFile/NtWriteFile Native API ядра). Возможно - это какая-то прослойка, которая обеспечивает независимость библиотеки от платформы?
Про I/Completion Ports знаю (ещё со времен Win2K - они там появились), а про winiocp - не знаю, так что хочу подтверждения от автора. А лучше - добавление в текст статьи, чтобы сразу было понятно, какой именно системный механизм используется версией Asio под Windows. Либо можно вообще про Windows не упоминать - она к теме статьи с боку припёку.
Я имел ввиду Windows IO Conpletion Ports. asio называет этот функционал winiocp в исходниках, поэтому так и написал :) К сожалению я не уделял много внимания тому как работает asio на винде, знаю только то что asio::random_access_file будет вам доступен если вы на windows благодаря io completion ports. Упомянул я об этом только для того чтобы дать понять что эта фича библиотеки работает не на всех платформах.
Подозреваю, здесь автор имел ввиду windows IO completion ports. Об этом много написано в MSDN, отмечу лишь, что есть база родом из 90х, о которой Вы справедливо упомянули. И есть новаторство из Vista or higher - см TP_IO и пр, где предоставили новые расширяемые и кастомизируемые интерфейсы и улучшения для многопоточного асинхронного ввода вывода, в т.ч. удобства для организации thread pools (TP), очередей, шедулеров и пр. Обвес удобный можно найти в WIL от MSFT, примеры использования в spdlog, IIRC.
Специально для решения этих проблем есть корутины, которые асио поддерживает
Думаю стоило добавить что мы ещё не используем C++20 :) Я точно помню что видел в asio поддержку boost::fiber, однако сейчас что-то не могу найти
https://think-async.com/Asio/asio-1.28.0/doc/asio/reference.html
В итоге на C++17 остаётся использовать только колбеки или std::future из коробки
Такие тоже поддерживает, но вообще я имел ввиду стекфул корутины, для них не нужен C++20)
https://think-async.com/Asio/asio-1.28.0/doc/asio/reference/spawn.html
Вы правы!
The
spawn()
function is a high-level wrapper over the Boost.Coroutine library.
К сожалению ещё не встречался с этим корутинами, обязательно обращу на них внимание :) Спасибо!
Поддержу boost::coroutine - вполне рабочее решение.
Но для борьбы с callback hell при работе с асинхронными API может быть стрельбой по воробъям. В состав boost::asio входят stackless coroutines: https://think-async.com/Asio/asio-1.22.0/doc/asio/overview/core/coroutine.html
По сути, это duff device на стероидах и имеет определенные (серьезные) ограничения. Но при правильном использовании позволяет очень эффективно реализовывать конечные автоматы, а оверхед при их использовании минимален - весь механизм запоминания состояния использует один int.
Автор хочет выжать производительность и вместо знаний системного апи полагается на asio. Замечу, последний обладает оверхедом, который контролируется сторонним производителем. Сделать тонкую и лаконичную обвязку системных функций для своих нужд ядро не может?
Большое спасибо за ваш комментарий! Сейчас опишу все как есть:
Я еще раз перечитал статью и не нашел, где я говорил, что хочу выжать максимум производительности.
Мы не используем Asio на самом нагруженном пути, лишь во вспомогательных задачах.
Хотите ещё быстрее, чем на io_uring? Можно всё прямо в user-space делать на Storage Performance Development Kit SPDK. В режиме poll, драйвер NVMe вам через неблокирующиеся ring buffer будет отдавать в user-space блоки, где можно sched_setaffinity() треды прибить гвоздями к CPU конкретным, и вообще всё будет хорошо с процессорными кэшами.
Крутые дядьки объединяют Data Plane Development Kit DPDK с SPDK для быстрых сетевых хранилищ данных.
В DPDK также сетевая карточка не видна ядру, а через ring buffer пробрасывается в user space, где у вас свой "быстрый" сетевой стек. Одна только структура sk_buff чего стоит, ни в какие кеши не лезет, не то что FreeBSD'шный mbuf простой.
Спасибо, у Вас получился интересный и полезный мини-обзор! А не можете пояснить что значит проброс через ring buffer? Имеется в виду какой-то конкретный API или просто некая реализация кольцевого буфера с его mmap-ом в userspace? Если последнее, то интересно, а можно ли программу заставить не спинлочно ждать новые данные в кольцевом буфере (на ум приходит только ioctl дёргать)? Буду очень признателен за пояснение!
Да, это конкретный API фреймворка DPDK, вот конкретно [ring buffer])https://doc.dpdk.org/guides/prog_guide/ring_lib.html)
Там вся соль в том, чтобы без ioctl(), чтобы всё в user-space отрабатывать и не ходить в ядро.
RTE ring — это таки очереди в памяти. С очередями сетевых карт напрямую работают poll mode drivers через компонент ethdev.
Спасибо за пояснение и за наводку на dpdk. Коварные ручонки иногда чешутся промапить переферию в свои объятия. Очень рад, что для этого нашёлся сборник рецептов!
Решаем задачу асинхронного ввода-вывода с библиотекой Asio