Ожидание условия (pthread_cond_wait) и изменение сокета (выбор) одновременно - PullRequest
9 голосов
/ 21 декабря 2011

Я пишу 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 (), которая позволяет потоку связи ожидать как изменения сокетов, так и сигнала от условия.

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

Ответы [ 3 ]

6 голосов
/ 21 декабря 2011

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

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

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

4 голосов
/ 22 декабря 2011

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

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

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

3 голосов
/ 21 декабря 2011

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

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

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

...