Как присоединиться к ветке, которая висит на блокировке ввода-вывода? - PullRequest
27 голосов
/ 15 октября 2008

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

Как мне правильно решить эту ситуацию? Должен ли я отправить pthread_kill (theard, SIGIO) или pthread_kill (theard, SIGALRM), чтобы разбить блок? Является ли это даже правильным сигналом? Или есть другой способ разрешить эту ситуацию и позволить этому дочернему потоку выйти из блокирующего чтения?

В настоящее время немного озадачен, так как никто из моих поисков не нашел решения.

Это в Linux и использует pthreads.

Edit: я немного поигрался с SIGIO и SIGALRM, когда я не устанавливаю обработчик сигнала, они ломают блокирующий ввод-вывод, но выдают сообщение на консоли («I / O возможен»), но когда я устанавливаю обработчик сигнала, чтобы избежать этого сообщения, они больше не нарушают блокировку ввода-вывода, поэтому поток не завершается. Так что я вернулся к первому шагу.

Ответы [ 13 ]

15 голосов
/ 27 сентября 2010

Канонический способ сделать это с pthread_cancel, где поток сделал pthread_cleanup_push / pop, чтобы обеспечить очистку для любых ресурсов, которые он использует.

К сожалению, это НЕ может быть использовано в коде C ++, никогда. Любой код C ++ std lib или ЛЮБОЙ try {} catch() в стеке вызовов во время pthread_cancel потенциально может segvi убить весь ваш процесс.

Единственный обходной путь - обработать SIGUSR1, установить флаг остановки, pthread_kill(SIGUSR1), а затем в любом месте, где поток заблокирован при вводе / выводе, если вы получаете EINTR, проверьте флаг остановки перед повторной попыткой ввода / вывода. На практике это не всегда удается в Linux, не знаю почему.

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

14 голосов
/ 15 октября 2008

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

Обычно используется pthread_kill () с SIGUSR1 или SIGUSR2 для отправки сигнала потоку. Другие предлагаемые сигналы - SIGTERM, SIGINT, SIGKILL - имеют семантику всего процесса, которая может вас не заинтересовать.

Что касается поведения при отправке сигнала, я думаю, что это связано с тем, как вы обрабатывали сигнал. Если у вас нет установленного обработчика, действие этого сигнала по умолчанию применяется, но в контексте потока, который получил сигнал. Так, например, SIGALRM будет «обрабатываться» вашим потоком, но обработка будет состоять из завершения процесса - вероятно, не желаемого поведения.

Получение сигнала потоком, как правило, прерывает его при чтении с помощью EINTR, если только он действительно не находится в таком непрерывном состоянии, как упомянуто в предыдущем ответе. Но я думаю, что это не так, иначе ваши эксперименты с SIGALRM и SIGIO не остановили бы процесс.

Возможно ли ваше чтение в каком-то цикле? Если чтение завершается с возвратом -1, то вырвитесь из цикла и выйдите из потока.

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

#include <stdlib.h>
#include <stdio.h>
#include <pthread.h>
#include <signal.h>

int global_gotsig = 0;

void *gotsig(int sig, siginfo_t *info, void *ucontext) 
{
        global_gotsig++;
        return NULL;
}

void *reader(void *arg)
{
        char buf[32];
        int i;
        int hdlsig = (int)arg;

        struct sigaction sa;
        sa.sa_handler = NULL;
        sa.sa_sigaction = gotsig;
        sa.sa_flags = SA_SIGINFO;
        sigemptyset(&sa.sa_mask);

        if (sigaction(hdlsig, &sa, NULL) < 0) {
                perror("sigaction");
                return (void *)-1;
        }
        i = read(fileno(stdin), buf, 32);
        if (i < 0) {
                perror("read");
        } else {
                printf("Read %d bytes\n", i);
        }
        return (void *)i;
}

main(int argc, char **argv)
{
        pthread_t tid1;
        void *ret;
        int i;
        int sig = SIGUSR1;

        if (argc == 2) sig = atoi(argv[1]);
        printf("Using sig %d\n", sig);

        if (pthread_create(&tid1, NULL, reader, (void *)sig)) {
                perror("pthread_create");
                exit(1);
        }
        sleep(5);
        printf("killing thread\n");
        pthread_kill(tid1, sig);
        i = pthread_join(tid1, &ret);
        if (i < 0)
                perror("pthread_join");
        else
                printf("thread returned %ld\n", (long)ret);
        printf("Got sig? %d\n", global_gotsig);

}
9 голосов
/ 15 октября 2008

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

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

6 голосов
/ 15 октября 2008

Зависит от того, как оно ожидает ввода-вывода.

Если поток находится в состоянии «Бесперебойный ввод-вывод» (обозначен как «D» вверху), то с этим действительно ничего нельзя поделать. Потоки обычно только кратко входят в это состояние, делая что-то, например, ожидание подкачки страницы (или загрузки по требованию, например, из файла mmap или из общей библиотеки и т. Д.), Однако сбой (особенно NFS-сервера) может вызвать чтобы оставаться в этом состоянии дольше.

Нет подлинного способа выбраться из этого состояния "D". Поток не будет отвечать на сигналы (вы можете отправлять их, но они будут поставлены в очередь).

Если это обычная функция ввода-вывода, такая как read (), write () или функция ожидания, такая как select () или poll (), сигналы будут доставляться нормально.

5 голосов
/ 26 апреля 2016

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

Начиная с ядра Linux 2.6.22, система предлагает новую функцию под названием signalfd(), которую можно использовать для открытия дескриптора файла для заданного набора сигналов Unix (за исключением тех, которые прямо убивают процесс.)

// defined a set of signals
sigset_t set;
sigemptyset(&set);
sigaddset(&set, SIGUSR1);
// ... you can add more than one ...

// prevent the default signal behavior (very important)
sigprocmask(SIG_BLOCK, &set, nullptr);

// open a file descriptor using that set of Unix signal
f_socket = signalfd(-1, &set, SFD_NONBLOCK | SFD_CLOEXEC);

Теперь вы можете использовать функции poll() или select() для прослушивания сигнала по более обычному дескриптору файла (сокет, файл на диске и т. Д.), Который вы слушали.

NONBLOCK важен, если вы хотите цикл, который может проверять сигналы и другие файловые дескрипторы снова и снова (т. Е. Это также важно для вашего другого файлового дескриптора).

У меня есть такая реализация, которая работает с (1) таймерами, (2) сокетами, (3) каналами, (4) сигналами Unix, (5) обычными файлами. На самом деле, действительно любой дескриптор файла плюс таймеры.

https://github.com/m2osw/snapcpp/blob/master/snapwebsites/libsnapwebsites/src/snapwebsites/snap_communicator.cpp
https://github.com/m2osw/snapcpp/blob/master/snapwebsites/libsnapwebsites/src/snapwebsites/snap_communicator.h

Вас также могут заинтересовать такие библиотеки, как libevent

3 голосов
/ 15 октября 2008

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

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

Код для обработки этого «события» из основного цикла необходимо будет добавить в каждый из потоков.

Если основной цикл должен был разбудить все потоки, он может либо записать в файл, либо закрыть его.


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

2 голосов
/ 15 октября 2008

Я думаю, как вы сказали, единственный способ - послать сигнал, а затем поймать и обработать его соответствующим образом. Альтернативами могут быть SIGTERM, SIGUSR1, SIGQUIT, SIGHUP, SIGINT и т. Д.

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

1 голос
/ 17 июня 2009

Если вы блокируете стороннюю библиотеку, которая зацикливается на EINTR, вы можете рассмотреть комбинацию использования pthread_kill с сигналом (USR1 и т. Д.), Вызывающего пустую функцию (не SIG_IGN) с фактическим закрытием / заменой дескриптор файла в вопросе. Используя dup2 для замены fd на / dev / null или аналогичный, вы заставите стороннюю библиотеку получить результат конца файла при повторном чтении.

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

1 голос
/ 26 октября 2008

Я удивлен, что никто не предложил pthread_cancel. Недавно я написал многопоточную программу ввода-вывода, после чего вызывал метод cancel (), а потом join () работал просто великолепно.

Первоначально я пробовал pthread_kill (), но в итоге просто завершил всю программу сигналами, которые я тестировал.

1 голос
/ 15 октября 2008

Я всегда добавляю функцию " kill ", связанную с функцией потока, которую я запускаю до объединения, которая гарантирует, что поток будет присоединен в течение разумного времени. Когда поток использует блокировку ввода-вывода, я пытаюсь использовать систему, чтобы сломать блокировку. Например, при использовании сокета у меня будет вызов kill shutdown (2) или close (2) , что приведет к чистому завершению сетевого стека.

Реализация сокетов в Linux является поточно-ориентированной.

...