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

StackOverflow https://stackoverflow.com/questions/203754

Вопрос

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

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

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

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

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

Это было полезно?

Решение

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

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

Другие советы

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

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

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

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

Я бы тоже рекомендовал использовать 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);

}

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

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

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

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

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

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

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

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

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

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


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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

что-то вроде:

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

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

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??.для других файлов я не уверен

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top