Асинхронный ввод-вывод. Проблемы

Проблема: допустим вы пишете простой -сервер, который получает какое-то сообщение от клиента, обрабатывает его и что-то ему отправляет. Протокол вы разрабатываете сами, на основе TCP/IP. Встает вопрос что выбрать: синхронные или асинхронные сокеты. Рассмотрим каждый вариант подробнее.

Типичная диаграмма последовательности для решаемой задачи

Синхронные сокеты.

При работе с синхронными сокетами, при вызове какой-либо операции текущий поток выполнения блокируется, до завершения операции. Ну ничего, решаете вы и выделяете по потоку на каждое соединение (или по два, если нужна независимая отправка). Пока ваше приложение маленькое, и пользуются им относительно небольшое количество одновременных пользователей, вы счастливы. Но как только количество пользователей переходит какой-то критический барьер - приложение начинает жутко тормозить. Давайте разберемся почему?

  1. На каждый поток операционной системой выделяется определенный минимальный объем оперативной памяти (для стека и пр. - около 1 мб), при 500 пользователях это будет 500 МБ, без учета памяти потребляемой клиентом на выполнение своих операций.
  2. Переключение между потоками выполнения занимает какое-то время.
  3. Большинство потоков “висит” в ожидании завершения синхронной операции, при этом время на переключение между потоками продолжает тратиться.
  4. Трудность отладки многопоточных приложений.
Для решения этих и других проблем синхронного ввода-вывода и придумали асинхронные вызовы.

Асинхронные сокеты

Для решения проблем выше были придуманы асинхронные сокеты и операции. При вызове асинхронной операции - поток выполнения не блокируется, а продолжает свое выполнение. В большинстве случаев асинхронные вызовы работают через так называемые порты завершения (IOCP). При вызове асинхронной операции сокет ассоциируется с каким-либо портом завершения, сокет будет оповещать этот поток о намерениях завершить операцию. При сигнале от сокета, порт берет из пула потоков (thread pool), свободный поток и завершает операцию (в зависимости от реализации может потребоваться вызвать метод завершения операции).
Что мы от этого выигрываем? Нет потоков ожидающих завершения операции, а следовательно потраченого впустую процессорного времени на переключение между ними; минимальное количество потоков; другие плюшки:)
Проблемы
При любом чтении или записи из сокета возвращается МЕНЬШЕЕ или равное ожидаемому количество байт. И если при работе с синхронными сокетами мы могли быть уверены, что получим сообщение полностью, написав цикл, который будет вычитывать строго необходимое количество данных в буфер (также для отправки, включая асинхронную). При асинхронном получении данных такой уверенности нет (при условии, что длина не фиксирована) - приложение просто выплюнет нам какой-то обрывок сообщения, или два, или полтора - как получиться. Таким образом необходимо будет потратить какое-то время на разбор приходящих сообщений.
Вторая проблема - т.к. асинхронные сокеты работают через нативный IOCP, то буферы для приема и отправки будут в состоянии pined. А множество пришпиленых объектов - зло, т.к. будет способствовать фрагментации кучи (подробнее тут), поэтому обычно принимается решение выделить один большой буфер, и использовать его постоянно. В .NET 2.0 появилась возможность вместе с асинхронными сокетами использовать ArraySegment{T}, который может “разбить” наш большой буфер на несколько небольших сегментов, но не все так просто, как кажеться - всем этим хозяйством необходимо управлять. Итого нам необходим менеджер сегментов (можно почитать тут), и парсер сообщений.
comments powered by Disqus