Как присоединиться к ветке, которая висит на блокировке ввода-вывода?

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

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

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

Это на Linux и с использованием pthreads.

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


person Grumbel    schedule 15.10.2008    source источник
comment
Аналогичная проблема и возможные решения обсуждаются там: Файловые дескрипторы и многопоточные программы   -  person dmityugov    schedule 10.11.2008
comment
Статья дала мне именно то, что я искал - shutdown (fd, SHUT_RDWR) ;. Спасибо.   -  person sivabudh    schedule 28.06.2010
comment
qqq, похоже, имеет правильный ответ, который, к сожалению, набрал очень мало голосов. pthread_cancel - это решение вашей проблемы.   -  person R.. GitHub STOP HELPING ICE    schedule 27.09.2010
comment
Пока нить остается заблокированной, она не причинит вреда. Проблема в том, что поток просыпается, когда вы закрываете его. Итак, исправление состоит в том, чтобы поместить некоторый код после строки, которая блокирует, что останавливает поток от каких-либо действий else, если завершение работы выполняется.   -  person David Schwartz    schedule 28.09.2011


Ответы (13)


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

Начиная с ядра 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 signals
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

person Alexis Wilke    schedule 26.04.2016

Канонический способ сделать это - использовать pthread_cancel, где поток выполнил _2 _ / _ 3_ для очистки всех используемых ресурсов.

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

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

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

person qqq    schedule 26.09.2010
comment
Что вы имеете в виду на практике, это не всегда удается в Linux, не знаю почему. (@qqq). Его можно подключить к этому lwn.net/Articles/683118. - person Juraj Michalak; 02.01.2019

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

Обычно используется 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);

}
person bog    schedule 15.10.2008
comment
Вы правы, read () на самом деле находится в цикле while, который проверяет EINTR, поскольку он находится в сторонней библиотеке, а не в моем собственном коде, я полностью пропустил этот факт, и поэтому простой сигнал не делаю то, что ожидал. - person Grumbel; 17.10.2008
comment
Можно ли с помощью этого метода освободить ресурсы, полученные с помощью flockfile? - person Ynv; 27.04.2012

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

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

person Community    schedule 15.10.2008
comment
или у вас может быть один канал для всех потоков, статус готовности возвращается из выбора / опроса нескольким потокам, ожидающим одного дескриптора файла (пока он запущен на уровне). Таким образом, все потоки, ожидающие одного канала-убийцы, получат уведомление о смерти. - person Greg Rogers; 17.06.2009

Зависит от того, как ждёт IO.

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

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

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

person MarkR    schedule 15.10.2008

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

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

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

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


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

person Andrew Edgecombe    schedule 15.10.2008

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

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

person Chris Young    schedule 15.10.2008

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

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

person David Holm    schedule 15.10.2008

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

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

person HUAGHAGUAH    schedule 25.10.2008

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

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

person bdonlan    schedule 16.06.2009

Сигналы и потоки - тонкая проблема в Linux, судя по различным страницам руководства. Вы используете LinuxThreads или NPTL (если у вас Linux)?

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

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

person shodanex    schedule 15.10.2008

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

Когда запускается событие ввода-вывода, должно быть указано условие.

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

что-то типа:

while (!_finished)
{
    pthread_cond_wait(&cond);
    handleio();
}
cleanup();

Помните, что условные переменные правильно обрабатывают сигналы. У них могут быть такие вещи, как «ложное пробуждение». Поэтому я бы обернул вашу собственную функцию вокруг функции cond_wait.

person Nicholas Mancuso    schedule 15.10.2008

struct pollfd pfd;
pfd.fd = socket;
pfd.events = POLLIN | POLLHUP | POLLERR;
pthread_lock(&lock);
while(thread_alive)
{
    int ret = poll(&pfd, 1, 100);
    if(ret == 1)
    {
        //handle IO
    }
    else
    {
         pthread_cond_timedwait(&lock, &cond, 100);
     }
}
pthread_unlock(&lock);

thread_alive - это переменная, зависящая от потока, которая может использоваться в сочетании с сигналом для завершения потока.

Что касается раздела ввода-вывода дескриптора, вам необходимо убедиться, что вы использовали open с параметром O_NOBLOCK, или, если это сокет, есть аналогичный флаг, вы можете установить MSG_NOWAIT ??. для других fds я не уверен

person luke    schedule 15.10.2008