Код для асинхронного ввода-вывода
В то время как асинхронные IO (неблокирующие дескрипторы с select/poll/epoll/kqueue и т.д.) не являются самой документированной вещью в Интернете, есть несколько хороших примеров.
Однако, все эти примеры, определяя дескрипторы, возвращаемые вызовом, имеют только "do_some_io(fd)
" заглушку. Они действительно не объясняют, как наилучшим образом подойти к фактическому асинхронному IO в таком методе.
Блокировка ввода-вывода очень аккуратная и простая для чтения кода. С другой стороны, неблокирование, асинхронное IO, волосатое и грязное.
Какие подходы существуют? Что является надежным и читаемым?
void do_some_io(int fd) {
switch(state) {
case STEP1:
... async calls
if(io_would_block)
return;
state = STEP2;
case STEP2:
... more async calls
if(io_would_block)
return;
state = STEP3;
case STEP3:
...
}
}
или, возможно, (ab) с использованием вычислений GCC:
#define concatentate(x,y) x##y
#define async_read_xx(var,bytes,line) \
concatentate(jmp,line): \
if(!do_async_read(bytes,&var)) { \
schedule(EPOLLIN); \
jmp_read = &&concatentate(jmp,line); \
return; \
}
// macros for making async code read like sync code
#define async_read(var,bytes) \
async_read_xx(var,bytes,__LINE__)
#define async_resume() \
if(jmp_read) { \
void* target = jmp_read; \
jmp_read = NULL; \
goto *target; \
}
void do_some_io() {
async_resume();
async_read(something,sizeof(something));
async_read(something_else,sizeof(something_else));
}
Или, возможно, исключения С++ и конечный автомат, поэтому рабочие функции могут запускать бит abort/resume или, возможно, управляемый таблицей государственный компьютер?
Это не то, как заставить его работать, и как сделать его поддерживаемым, чтобы я гонялся!
Ответы
Ответ 1
Я предлагаю взглянуть на: http://www.kegel.com/c10k.html, затем взгляните на существующие библиотеки, такие как libevent, Boost.Asio, которые уже выполняют работу и посмотреть, как они работают.
Дело в том, что для каждого типа системного вызова подход может быть различным:
- выберите простой реактор
- epoll имеет как интерфейс, связанный с краем, так и уровнем, требующий разного подхода.
- iocp - это проактор, требующий другого подхода.
Предложение: используйте хорошую существующую библиотеку, например Boost.Asio для С++ или libevent для C.
EDIT: так ASIO обрабатывает этот
class connection {
boost::asio:ip::tcp::socket socket_;
public:
void run()
{
// for variable length chunks
async_read_until(socket_,resizable_buffer,'\n',
boost::bind(&run::on_line_recieved,this,errorplacehplder);
// or constant length chunks
async_read(socket_,buffer(some_buf,buf_size),
boost::bind(&run::on_line_recieved,this,errorplacehplder);
}
void on_line_recieved(error e)
{
// handle it
run();
}
};
Поскольку ASIO работает как проактор, он уведомляет вас о завершении работы и
обрабатывает EWOULDBLOCK внутренне.
Если вы говорите как реактор, вы можете имитировать это поведение:
class conn {
// Application logic
void run() {
read_chunk(&conn::on_chunk_read,size);
}
void on_chunk_read() {
/* do something;*/
}
// Proactor wrappers
void read_chunk(void (conn::*callback),int size, int start_point=0) {
read(socket,buffer+start,size)
if( complete )
(this->*callback()
else {
this -> tmp_size-=size-read;
this -> tmp_start=start+read;
this -> tmp_callback=callback
your_event_library_register_op_on_readable(callback,socket,this);
}
}
void callback()
{
read_chunk(tmp_callback,tmp_size,tmp_start);
}
}
Что-то вроде этого.
Ответ 2
Государственные машины - один хороший подход. Это немного сложная задача, которая спасет вас от головных болей в будущем, когда будущее начнется действительно, очень скоро.; -)
Другой метод - использовать потоки и блокировать ввод-вывод на одном fd в каждом потоке. Компромисс заключается в том, что вы делаете ввод-вывод простым, но можете вводить сложность в синхронизации.
Ответ 3
Для решения этой проблемы существует отличный шаблон дизайна "coroutine".
Это лучшее из обоих миров: аккуратный код, точно такой же, как синхронный поток io и отличная производительность без переключения контекста, например async io. Coroutine выглядит как одиночный синхронный поток, с одним указателем инструкции. Но многие сопрограммы могут работать в одном потоке ОС (так называемая "совместная многозадачность" ).
Пример кода сопрограммы:
void do_some_io() {
blocking_read(something,sizeof(something));
blocking_read(something_else,sizeof(something_else));
blocking_write(something,sizeof(something));
}
Похож на синхронный код, но на самом деле поток управления использует другой способ, например:
void do_some_io() {
// return control to network io scheduler, to handle another coroutine
blocking_read(something,sizeof(something));
// when "something" is read, scheduler fill given buffer and resume this coroutine
// return control to network io scheduler, to handle another coroutine
CoroSleep( 1000 );
// scheduler create async timer and when it fires, scheduler pass control to this coroutine
...
// and so on
Таким образом, однопоточный планировщик управляет многими сопрограммами с пользовательским кодом и аккуратными синхронными вызовами в io.
Пример реализации С++ coroutines - это "boost.coroutine" (на самом деле это не часть boost:)
http://www.crystalclearsoftware.com/soc/coroutine/
Эта библиотека полностью реализует механику coroutine и может использовать boost.asio в качестве планировщика и асинхронного слоя.
Ответ 4
У вас должен быть основной цикл, который предоставляет async_schedule(), async_foreach(), async_tick() и т.д. Эти функции, в свою очередь, помещают записи в глобальный список методов, которые будут выполняться при следующем вызове async_tick(). Затем вы можете написать код, который будет намного более аккуратным и не включает никаких операторов switch.
Вы можете просто написать:
async_schedule(callback, arg, timeout);
Или:
async_wait(condition, callback, arg, timeout);
Тогда ваше состояние можно даже установить в другом потоке (при условии, что вы заботитесь о безопасности потоков при доступе к этой переменной).
Я реализовал асинхронную структуру в C для моего внедренного проекта, потому что я хотел иметь неперехватную многозадачность, а асинхронный режим идеально подходит для выполнения многих задач, выполняя небольшую работу на каждой итерации основного цикла.
Код здесь: https://github.com/mkschreder/fortmax-blocks/blob/master/common/kernel/async.c
Ответ 5
Вы хотите отделить "io" от обработки, после чего прочитанный вами код станет очень читаемым. В основном у вас есть:
int read_io_event(...) { /* triggers when we get a read event from epoll/poll/whatever */
/* read data from "fd" into a vstr/buffer/whatever */
if (/* read failed */) /* return failure code to event callback */ ;
if (/* "message" received */) return process_io_event();
if (/* we've read "too much" */) /* return failure code to event callback */ ;
return /* keep going code for event callback */ ;
}
int process_io_event(...) {
/* this is where you process the HTTP request/whatever */
}
... тогда реальный код находится в событии процесса, и даже если у вас есть несколько запросов ответов, он довольно читабельен, вы просто делаете "return read_io_event()" после установки состояния или чего-то еще.