Ожидание по условию (pthread_cond_wait) и одновременное изменение сокета (select)

Я пишу POSIX-совместимый многопоточный сервер на c / c ++, который должен иметь возможность асинхронно принимать, читать и записывать большое количество подключений. На сервере есть несколько рабочих потоков, которые выполняют задачи и иногда (и непредсказуемо) помещают данные в очередь для записи в сокеты. Данные также иногда (и непредсказуемо) записываются в сокеты клиентами, поэтому сервер также должен читать асинхронно. Один из очевидных способов сделать это - дать каждому соединению поток, который читает и записывает из / в свой сокет; однако это уродливо, поскольку каждое соединение может сохраняться в течение долгого времени, и серверу, таким образом, может потребоваться удерживать сотни или тысячи потоков только для того, чтобы отслеживать соединения.

Лучшим подходом было бы иметь один поток, который обрабатывал бы все коммуникации с помощью функций select () / pselect (). То есть один поток ожидает, что любой сокет станет доступным для чтения, а затем порождает задание для обработки ввода, которое будет обрабатываться пулом других потоков всякий раз, когда ввод доступен. Всякий раз, когда другие рабочие потоки производят вывод для соединения, он ставится в очередь, а коммуникационный поток ожидает, пока этот сокет станет доступным для записи, прежде чем записывать его.

Проблема заключается в том, что поток связи может ожидать в функции select () или pselect (), когда выходные данные помещаются в очередь рабочими потоками сервера. Возможно, что, если в течение нескольких секунд или минут вход не поступает, блок вывода в очереди будет просто ждать завершения коммуникационного потока select () ing. Однако этого не должно происходить - данные должны быть записаны как можно скорее.

Прямо сейчас я вижу пару решений, ориентированных на многопоточность. Один из них состоит в том, чтобы поток связи был занят ожиданием ввода и обновлял список сокетов, которые он ожидает для записи, каждые десятые доли секунды или около того. Это не оптимально, так как предполагает ожидание при занятости, но это сработает. Другой вариант - использовать pselect () и отправлять сигнал USR1 (или что-то подобное) всякий раз, когда новый вывод помещен в очередь, позволяя потоку связи немедленно обновлять список сокетов, которые он ожидает для статуса записи. Я предпочитаю последнее здесь, но все же не люблю использовать сигнал для чего-то, что должно быть условием (pthread_cond_t). Еще один вариант - включить в список файловых дескрипторов, которых ожидает select (), фиктивный файл, в который мы записываем один байт всякий раз, когда нужно добавить сокет в доступный для записи fd_set для select (); это разбудит коммуникационный сервер, потому что этот конкретный фиктивный файл будет доступен для чтения, что позволит коммуникационному потоку немедленно обновить его доступный для записи fd_set.

Я интуитивно чувствую, что второй подход (с сигналом) - это «самый правильный» способ программирования сервера, но мне любопытно, знает ли кто-нибудь, какой из вышеперечисленных является наиболее эффективным, вообще говоря, будь то один из выше вызовет состояния гонки, о которых я не знаю, или если кто-то знает более общее решение этой проблемы. Что мне действительно нужно, так это функция pthread_cond_wait_and_select (), которая позволяет потоку связи ждать как изменения в сокетах, так и сигнала от условия.

Заранее спасибо.


person user1110198    schedule 21.12.2011    source источник


Ответы (3)


Это довольно частая проблема.

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

Обратите внимание, что, поскольку чтение и запись канала меньше или равно PIPE_BUF являются атомарными, указатели записываются и читаются за один раз. Можно даже иметь несколько рабочих потоков, записывающих указатели в один и тот же канал из-за гарантии атомарности.

person Maxim Egorushkin    schedule 21.12.2011

К сожалению, лучший способ сделать это для каждой платформы разный. Канонический переносимый способ сделать это - поместить блок потока ввода-вывода в poll. Если вам нужно заставить поток ввода-вывода покинуть poll, вы отправляете один байт в pipe, который этот поток опрашивает. Это вызовет немедленный выход потока из poll.

В Linux лучше всего использовать epoll. В операционных системах, основанных на BSD (в том числе, я думаю, OSX) kqueue. В Solaris это было /dev/poll, а теперь есть еще кое-что, имя которого я забыл.

Вы можете просто рассмотреть возможность использования такой библиотеки, как libevent или Boost.Asio. Они предоставляют вам лучшую модель ввода-вывода для каждой поддерживаемой платформы.

person David Schwartz    schedule 22.12.2011

Ваш второй подход - более чистый. Совершенно нормально иметь такие вещи, как select или epoll, включать в свой список настраиваемые события. Это то, что мы делаем в моем текущем проекте для обработки таких событий. Мы также используем таймеры (в Linux timerfd_create) для периодических событий.

В Linux eventfd позволяет создавать для этой цели такие произвольные пользовательские события - поэтому я бы сказал, что это вполне приемлемая практика. Для функций только POSIX, ну, хм, возможно, одна из команд конвейера или socketpair, что я тоже видел.

Опрос занятости - не лучший вариант. Сначала вы будете сканировать память, которая будет использоваться другими потоками, что приведет к конфликту памяти ЦП. Во-вторых, вам всегда придется возвращаться к вашему select вызову, который создаст огромное количество системных вызовов и переключений контекста, что снизит общую производительность системы.

person edA-qa mort-ora-y    schedule 21.12.2011