Я хочу подождать как файлового дескриптора, так и мьютекса, какой рекомендуемый способ сделать это?

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

Какой рекомендуемый способ выполнить это? Должен ли я создать межпоточный канал и написать ему, когда очередь переходит от каких-либо элементов к некоторым элементам? Разве нет лучшего способа?

И если мне нужно создать межпоточный канал, почему не больше библиотек, реализующих общие очереди, вы можете создать общую очередь и межпоточный канал как единый объект?

Является ли тот факт, что я хочу сделать это вообще, подразумевает фундаментальный недостаток дизайна?

Я спрашиваю об этом как для С++, так и для Python. И я немного заинтересован в кросс-платформенном решении, но в первую очередь заинтересован в Linux.

Для более конкретного примера...

У меня есть код, который будет искать материал в дереве файловой системы. У меня есть несколько каналов связи, открытых для внешнего мира через сокеты. Запросы, которые могут (или не могут) привести к необходимости поиска материала в дереве файловой системы, будут поступать.

Я собираюсь изолировать код, который ищет материал в дереве файловой системы в одном или нескольких потоках. Я хотел бы получить запросы, которые приведут к необходимости поиска в дереве и поместить их в потокобезопасную очередь вещей, которые должны выполняться поисковыми потоками. Результаты будут помещены в очередь завершенных запросов.

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

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

Стандартный способ ожидания в пустой очереди - использовать переменную условия. Но это не сработает, если мне нужно будет обслуживать другие запросы, пока я жду. Либо я заканчиваю опрос очереди результатов все время (и в среднем задержку результатов на половину интервала опроса), блокирование и отсутствие запросов на обслуживание.

Ответ 1

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

Изменить: Linux имеет eventfd и timerfd. Они могут быть добавлены в ваш список epoll и использованы для выхода из epoll_wait при запуске из другого потока или события таймера соответственно.

Есть еще один вариант, и это сигналы. Можно использовать fcntl изменить файловый дескриптор таким образом, чтобы сигнал выдавался, когда файловый дескриптор становится активным. Обработчик сигнала может затем отправить сообщение, готовое к файлу, в любую выбранную вами очередь. Это может быть простая семафорная или мьютексовая/конвеерная очередь. Поскольку один из них больше не использует select/poll, больше не нужно использовать трубку для отправки сообщений на основе файлов без файлов.

Предупреждение о работоспособности: я не пробовал это, и хотя я не понимаю, почему это не сработает, я действительно не знаю последствий для производительности подхода signal.

Ответ 2

Я решил эту точную проблему, используя то, что вы упоминаете, pipe() и libevent (который обертывает epoll). Рабочий поток записывает байт в его трубу FD, когда его очередь вывода идет от пустого до непустого. Это пробуждает основной поток ввода-вывода, который затем может захватывать выходной поток рабочего потока. Это отлично работает на самом деле очень просто для кодирования.

Ответ 3

У вас есть тег Linux, поэтому я собираюсь выбросить его: "Очереди сообщений POSIX" делают все это, что должно выполнять ваши "встроенные", в "запросе, если не на вашем менее желательном кросс-платформенном желании.

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

Ответ 4

Duck и twk на самом деле лучшие ответы, чем дорон (тот, который выбран OP), на мой взгляд. Дорон предлагает писать в очередь сообщений из контекста обработчика сигналов и утверждает, что очередь сообщений может быть "любым типом очереди". Я бы настоятельно предостерег вас от этого, так как многие вызовы библиотеки C/system нельзя безопасно вызывать из обработчика сигнала (см. async-signal-safe).

В частности, если вы выбираете очередь, защищенную мьютексом, вы не должны обращаться к нему из обработчика сигнала. Рассмотрим этот сценарий: ваш потребительский поток блокирует очередь для чтения. Сразу же после этого ядро ​​доставляет сигнал, чтобы уведомить вас о том, что в файловом дескрипторе теперь есть данные. Вы сигнализируете прогон обработчика в потребительском потоке, обязательно), и пытается помещать что-то в вашу очередь. Для этого сначала нужно запереть замок. Но он уже держит замок, поэтому вы теперь зашли в тупик.

select/poll, по моему опыту, является единственным жизнеспособным решением для управляемой событиями программы в UNIX/Linux. Хотелось бы, чтобы в программе mutlithreaded был лучший способ, но вам нужен какой-то механизм для "пробуждения" вашего потребительского потока. Мне еще предстоит найти метод, который не включает системный вызов (поскольку потребительский поток находится в ожидании внутри ядра во время любого блокирующего вызова, такого как select).

EDIT: Я забыл упомянуть один способ, специфичный для Linux, для обработки сигналов при использовании select/poll: signalfd (2). Вы получаете файловый дескриптор, который вы можете выбрать/опросить, и обработка кода выполняется нормально, а не в контексте обработчика сигнала.

Ответ 5

Кажется, никто еще не упомянул этот вариант:

Не запускайте select/poll/и т.д. в вашей "основной теме". Начните выделенный вторичный поток, который выполняет ввод-вывод, и выдает уведомления в вашу поточно-безопасную очередь (ту же очередь, с которой ваши другие потоки используют для связи с основным потоком), когда операции ввода-вывода завершены.

Тогда ваш основной поток просто должен ждать очереди уведомлений.

Ответ 6

Это очень часто встречающаяся проблема, особенно когда вы разрабатываете сетевую серверную программу. Основной вид основной программы Linux на стороне сервера будет выглядеть следующим образом:

epoll_add(serv_sock);
while(1){
    ret = epoll_wait();
    foreach(ret as fd){
        req = fd.read();
        resp = proc(req);
        fd.send(resp);
    }
}

Это однопоточный (основной поток), основанный на epoll сервер. Проблема в том, что она однопоточная, а не многопоточная. Это требует, чтобы proc() никогда не блокировал или запускался в течение значительного времени (например, 10 мс для обычных случаев).

Если proc() будет работать в течение длительного времени, МЫ НУЖДАЕМ МНОГО НИТИ и выполняем proc() в отдельном потоке (рабочий поток).

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

epoll_add(serv_sock);
while(1){
    ret = epoll_wait();
    foreach(ret as fd){
        req = fd.read();
        queue.add_job(req); // fast, non blockable
    }
}

Затем нам нужен способ получить результат задачи из рабочего потока. Как? Если мы просто проверяем очередь сообщений напрямую, до или после epoll_wait().

epoll_add(serv_sock);
while(1){
    ret = epoll_wait(); // may blocks for 10ms
    resp = queue.check_result(); // fast, non blockable
    foreach(ret as fd){
        req = fd.read();
        queue.add_job(req); // fast, non blockable
    }
}

Однако действие проверки будет выполняться после завершения epoll_wait(), и epoll_wait() обычно блокирует 10 микросекунд (обычные случаи), если все ожидающие его файловые дескрипторы неактивны.

Для сервера 10 мс достаточно долго! Можем ли мы сигнализировать epoll_wait() для немедленного завершения после получения результата задачи?

Да! Я опишу, как это делается в одном из моих проектов с открытым исходным кодом:

Создайте трубку для всех рабочих потоков, а epoll ждет и на этом канале. Как только результат задачи сгенерирован, рабочий поток записывает один байт в трубку, тогда epoll_wait() завершится почти в одно и то же время! - Линия Linux имеет задержку от 5 до 20 секунд.


В моем проекте SSDB (совместимая с протоколом Redis совместимая база данных NoSQL на диске), я создаю SelectableQueue для передачи сообщений между основной поток и рабочие потоки. Так же как и его имя, SelectableQueue имеет файловый дескриптор, который может ждать epoll.

SelectableQueue: https://github.com/ideawu/ssdb/blob/master/src/util/thread.h#L94

Использование в основном потоке:

epoll_add(serv_sock);
epoll_add(queue->fd());
while(1){
    ret = epoll_wait();
    foreach(ret as fd){
        if(fd is queue){
            sock, resp = queue->pop_result();
            sock.send(resp);
        }
        if(fd is client_socket){
            req = fd.read();
            queue->add_task(fd, req);
        }
    }
}

Использование в рабочем потоке:

fd, req = queue->pop_task();
resp = proc(req);
queue->add_result(fd, resp);

Ответ 7

Один из способов добиться того, что вы хотите сделать, - это выполнить шаблон наблюдателя

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

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

Ответ 8

С++ 11 имеет std:: mutex и std:: condition_variable. Эти два могут использоваться, чтобы иметь один сигнал потока другой, когда выполняется определенное условие. Мне кажется, что вам нужно будет выстроить свое решение из этих примитивов. Если среда не поддерживает эти возможности библиотеки С++ 11, вы можете найти очень похожие в boost. Извините, не могу сказать много о питоне.