Как сигнализировать select() о немедленном возврате?

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

Теперь мне также нужно иметь возможность сигнализировать рабочему потоку, чтобы он немедленно сделал что-то другое; из основного потока, мне нужно, чтобы рабочий поток select() сразу же вернулся. На данный момент я решил это следующим образом (подход в основном взят из здесь и здесь):

При запуске программы рабочий поток создает для этого дополнительный сокет типа дейтаграмма (UDP) и привязывает его к какому-то случайному порту (назовем этот сокет B). Аналогичным образом основной поток создает сокет для отправки дейтаграммы. При вызове select() рабочий поток теперь перечисляет как A, так и B в fd_set. Когда основной поток должен подать сигнал, он sendto() отправляет пару байтов на соответствующий порт на localhost. В рабочем потоке, если B остается в fd_set после возврата select(), вызывается recvfrom(), а полученные байты просто игнорируются.

Кажется, это работает очень хорошо, но я не могу сказать, что мне нравится это решение, главным образом потому, что оно требует привязки дополнительного порта для B, а также потому, что оно добавляет несколько дополнительных вызовов API сокетов, которые могут дать сбой. догадываюсь – и мне не очень хочется выяснять подходящее действие для каждого из случаев.

Я думаю, что в идеале я хотел бы вызвать некоторую функцию, которая принимает A в качестве входных данных и ничего не делает, кроме немедленного возврата select(). Однако я не знаю такой функции. (Думаю, я мог бы, например, shutdown() сокет, но побочные эффекты не очень приемлемы :)

Если это невозможно, вторым лучшим вариантом будет создание B, который намного проще, чем настоящий сокет UDP, и на самом деле не требует выделения каких-либо ограниченных ресурсов (кроме разумного объема памяти). . Я предполагаю, что сокеты домена Unix сделают именно это, но: платформу, чем та, что у меня есть в настоящее время, хотя некоторое умеренное количество #ifdef материала в порядке. (Я ориентируюсь в основном на Windows и Linux — и, кстати, пишу на C++.)

Пожалуйста, не предлагайте рефакторинг, чтобы избавиться от двух отдельных потоков. Такой дизайн необходим, потому что основной поток может быть заблокирован на длительное время (например, при выполнении каких-то интенсивных вычислений — и я не могу начать периодически вызывать receive() из самого внутреннего цикла вычислений), а тем временем кому-то нужно буферизовать входящие данные (и по причинам, которые я не могу контролировать, это не может быть отправителем).

Теперь, когда я писал это, я понял, что кто-то обязательно ответит просто "Boost.Asio", так что я только что впервые посмотрел на это... Однако не смог найти очевидного решения. Обратите внимание, что я также не могу (легко) повлиять на то, как создается сокет A, но при необходимости я должен позволить другим объектам обернуть его.


person Reunanen    schedule 21.12.2008    source источник


Ответы (3)


Вы почти там. Используйте прием с "самостоятельной трубкой". Откройте канал, добавьте его в свой select() для чтения и записи fd_set, напишите в него из основного потока, чтобы разблокировать рабочий поток. Он переносим между системами POSIX.

Я видел вариант аналогичной техники для Windows в одной системе (на самом деле он использовался вместе с описанным выше методом, разделенным #ifdef WIN32). Разблокировать можно, добавив фиктивный (несвязанный) сокет дейтаграммы в fd_set и затем закрыв его. Минус в том, что, конечно, каждый раз приходится заново открывать.

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

person Alex B    schedule 21.12.2008
comment
Спасибо, но, как указано в вопросе, окончательное решение должно работать и в Windows. - person Reunanen; 21.12.2008
comment
Хорошо, спасибо за предложение Windows. Есть идеи, насколько дорого повторное открытие на практике? - person Reunanen; 21.12.2008
comment
Спасибо Checkers, создание и закрытие (несвязанного) сокета, похоже, действительно работает очень хорошо. - person Reunanen; 21.12.2008
comment
У меня нет точных цифр, но поскольку вы пишете для Windows, это почти всегда означает рабочий стол, а это означает, что стоимость должна быть незначительной. - person Alex B; 22.12.2008
comment
В Windows вы также можете просто держать сокет UDP открытым и отправлять в него пакет, чтобы select() возвращал готовый к чтению в этом сокете, или вы можете создать пару сокетов TCP, которые подключены друг к другу и отправить пакет. байт на одном из них, в то время как select() отслеживает другой. - person Jeremy Friesner; 29.12.2018

Использование канала, а не сокета, немного чище, поскольку у другого процесса нет возможности завладеть им и все испортить.

Использование сокета UDP определенно создает вероятность того, что случайные пакеты будут входить и мешать.

Анонимный канал никогда не будет доступен никакому другому процессу (если вы не предоставите его ему).

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

person MarkR    schedule 22.12.2008

В unix это будет просто с использованием канала. Если вы работаете в Windows и хотите продолжать использовать оператор select, чтобы ваш код был совместим с unix, трюк с созданием несвязанного сокета UDP и его закрытием работает хорошо и легко. Но вы должны сделать его многопоточным.

Единственный способ, который я нашел для обеспечения многопоточности, — это закрыть и воссоздать сокет в том же потоке, в котором выполняется оператор select. Конечно, это сложно, если поток блокируется при выборе. И тут в винде приходит вызов QueueUserAPC. Когда окна блокируются в операторе select, поток может обрабатывать асинхронные вызовы процедур. Вы можете запланировать это из другого потока, используя QueueUserAPC. Windows прерывает выбор, выполняет вашу функцию в том же потоке и продолжает оператор выбора. Теперь вы можете в своем методе APC закрыть сокет и воссоздать его. Гарантированная безопасность потоков, и вы никогда не потеряете сигнал.

person guido4096    schedule 24.12.2012