Программирование POSIX pthread

Мне нужно закодировать многопоточную (скажем, 2 потока) программу, где каждый из этих потоков выполняет другую задачу. Кроме того, эти потоки должны продолжать работать бесконечно в фоновом режиме после запуска. Вот что я сделал. Может кто-нибудь, пожалуйста, дайте мне некоторую обратную связь, если метод хорош, и если вы видите некоторые проблемы. Кроме того, я хотел бы знать, как закрыть потоки систематически, как только я закончу выполнение, скажем с помощью Ctrl + C.

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

Вот скелет:

void    *func1();
void    *func2();

int main(int argc, char *argv[])
{   

    pthread_t th1,th2;
    pthread_create(&th1, NULL, func1, NULL);
    pthread_create(&th2, NULL, func2, NULL);

    fflush (stdout);
    for(;;){
    }
    exit(0); //never reached
}

void *func1()
{
    while(1){
    //do something
    }
}

void *func2()
{
    while(1){
    //do something
    }
} 

Спасибо.

Отредактированный код с использованием входов ответов: Правильно ли я вышел из нитей?

#include <stdlib.h>     /*  exit() */
#include <stdio.h>      /* standard in and output*/
#include <pthread.h>
#include <unistd.h>
#include <time.h>
#include <sys/time.h>
#include <sys/types.h>
#include <signal.h>
#include <semaphore.h>

sem_t end;

void    *func1();
void    *func2();

void ThreadTermHandler(int signo){
    if (signo == SIGINT) {
        printf("Ctrl+C detected !!! \n");
        sem_post(&end);
        }
}
void *func1()
{
    int value;
    for(;;){
        sem_getvalue(&end, &value);
        while(!value){
            printf("in thread 1 \n");
        }
    }
    return 0;
}

void *func2()
{
    int value;
    for(;;){
        sem_getvalue(&end, &value);
        while(!value){
            printf("value = %d\n", value);
        }
    }
    return 0;
}

int main(int argc, char *argv[])
{


    sem_init(&end, 0, 0);
    pthread_t th1,th2;
    int value  = -2;
    pthread_create(&th1, NULL, func1, NULL);
    pthread_create(&th2, NULL, func2, NULL);

    struct sigaction sa;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = SA_SIGINFO;
    sa.sa_sigaction = ThreadTermHandler;
    // Establish a handler to catch CTRL+c and use it for exiting.
    if (sigaction(SIGINT, &sa, NULL) == -1) {
        perror("sigaction for Thread Termination failed");
        exit( EXIT_FAILURE );
    }

    /* Wait for SIGINT. */
    while (sem_wait(&end)!=0){}
    //{
        printf("Terminating Threads.. \n");
        sem_post(&end);
                sem_getvalue(&end, &value);
        /* SIGINT received, cancel threads. */
        pthread_cancel(th1);
        pthread_cancel(th2);
        /* Join threads. */
        pthread_join(th1, NULL);
        pthread_join(th2, NULL);
    //}
    exit(0);
}

Ответ 1

Существуют два подхода к завершению потока.

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

Оба подхода имеют оговорки. Подробнее см. "Убить поток в библиотеке Pthread" .

В вашем случае это хорошая возможность использовать точки отмены. Я буду работать с комментарием. Для ясности проверка ошибок была опущена.

#define _POSIX_C_SOURCE 200809L
#include <pthread.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

void sigint(int signo) {
    (void)signo;
}

void *thread(void *argument) {
    (void)argument;
    for (;;) {
        // Do something useful.
        printf("Thread %u running.\n", *(unsigned int*)argument);

        // sleep() is a cancellation point in this example.
        sleep(1);
    }
    return NULL;
}

int main(void) {
    // Block the SIGINT signal. The threads will inherit the signal mask.
    // This will avoid them catching SIGINT instead of this thread.
    sigset_t sigset, oldset;
    sigemptyset(&sigset);
    sigaddset(&sigset, SIGINT);
    pthread_sigmask(SIG_BLOCK, &sigset, &oldset);

    // Spawn the two threads.
    pthread_t thread1, thread2;
    pthread_create(&thread1, NULL, thread, &(unsigned int){1});
    pthread_create(&thread2, NULL, thread, &(unsigned int){2});

    // Install the signal handler for SIGINT.
    struct sigaction s;
    s.sa_handler = sigint;
    sigemptyset(&s.sa_mask);
    s.sa_flags = 0;
    sigaction(SIGINT, &s, NULL);

    // Restore the old signal mask only for this thread.
    pthread_sigmask(SIG_SETMASK, &oldset, NULL);

    // Wait for SIGINT to arrive.
    pause();

    // Cancel both threads.
    pthread_cancel(thread1);
    pthread_cancel(thread2);

    // Join both threads.
    pthread_join(thread1, NULL);
    pthread_join(thread2, NULL);

    // Done.
    puts("Terminated.");
    return EXIT_SUCCESS;
}

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

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

Ответ 2

Ответ зависит от того, что вы хотите сделать, когда пользователь нажимает Ctrl C.

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

Однако, если ваши потоки должны выполнять очистку, вам нужно проделать определенную работу. Вам нужно рассмотреть две отдельные проблемы:

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

Прежде всего, обработчики сигналов - это боль. Если вы не очень осторожны, вы должны предположить, что большинство функций библиотеки не являются законными для вызова из обработчика сигналов. К счастью, sem_post определяется как безопасный для асинхронного сигнала и может отлично соответствовать вашим требованиям:

  • В начале вашей программы инициализируйте семафор с помощью sem_init(&exit_sem, 0, 0);
  • Установите обработчик сигнала для SIGINT (и любые другие сигналы завершения, которые вы хотите обработать, например SIGTERM), который выполняет sem_post(&exit_sem); и возвращает.
  • Замените for(;;); в основном потоке с помощью while (sem_wait(&exit_sem)!=0).
  • После завершения sem_wait основной поток должен сообщить всем остальным потокам, что они должны выйти, а затем ждать, пока все они выйдут.

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

Теперь есть несколько способов, с помощью которых можно было бы информировать рабочие потоки о том, чтобы время было прекращено. Некоторые варианты, которые я вижу:

  • Периодически проверяйте sem_getvalue(&exit_sem) и очищайте и завершайте, если он возвращает ненулевое значение. Обратите внимание, что это не будет работать, если поток заблокирован неограниченно, например, при вызове read или write.
  • Используйте pthread_cancel и аккуратно разместите обработчики отмены (pthread_cleanup_push) повсюду.
  • Используйте pthread_cancel, но также используйте pthread_setcancelstate, чтобы отключить отмену во время большей части вашего кода, и только повторно включите его, когда вы собираетесь выполнять блокировку операций ввода-вывода. Таким образом, вам нужно только поместить обработчики очистки только в те места, где отменена.
  • Изучите расширенную семантику сигнала и настройте дополнительный сигнал и обработчик сигнала прерывания, который вы отправляете во все потоки через pthread_kill, что приведет к возврату системных вызовов блокировки с ошибкой EINTR. Тогда ваши потоки могут воздействовать на это и выходить из нормального пути C через строку отказа, возвращая полностью назад функцию запуска.

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

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

Ответ 3

Это плохая идея:

for(;;){
}

потому что ваш основной поток выполнит ненужные инструкции CPU.

Если вам нужно подождать в основном потоке, используйте pthread_join, как ответили в этом вопросе: Несколько потоков в программе C

Ответ 4

Что вы сделали, я не вижу никаких очевидных проблем (кроме того, что вы игнорируете возвращаемое значение pthread_create). К сожалению, остановки потоков более активно, чем вы думаете. Еще одним осложнением является тот факт, что вы хотите использовать сигналы. Вот что вы могли бы сделать.

  • В потоках "детей" используйте pthread_sigmask для блокировки сигналов
  • В основном потоке используйте sigsuspend для ожидания сигнала
  • После получения сигнала отмените (pthread_cancel) дочерние потоки

Ваша основная тема может выглядеть примерно так:

/* Wait for SIGINT. */
sigsuspend(&mask);

/* SIGINT received, cancel threads. */
pthread_cancel(th1);
pthread_cancel(th2);

/* Join threads. */
pthread_join(th1, NULL);
pthread_join(th2, NULL);

Очевидно, вам следует больше узнать о pthread_cancel и точках отмены. Вы также можете установить обработчик очистки. И, конечно, проверьте каждое возвращаемое значение.

Ответ 5

Посмотрел на ваш обновленный код и все еще выглядит не так.

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

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

Ответ 6

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

Итак, идея была бы:

  • написать код для обработки сигналов
  • когда вы выполняете ctrl + c... функция обработки называется
  • эта функция отменяет поток
  • каждый созданный поток задает функцию очистки потока с помощью pthread_cleanup_push
  • при отмене протектора функция pthread_cleanup_push называется
  • присоединить все потоки до выхода

Это похоже на простое и естественное решение.

static void cleanup_handler(void *arg)
{
    printf("Called clean-up handler\n");
}

static void *threadFunc(void *data)
{
    ThreadData *td = (ThreadData*)(data);
    pthread_cleanup_push(cleanup_handler, (void*)something);
    while (1) {
    pthread_testcancel(); /* A cancellation point */
    ...
    }
    pthread_cleanup_pop(cleanup_pop_arg);
    return NULL;
}

код >

Ответ 7

Вам не нужна петля для швартовки в основном. A th1->join(); th2->join(); будет достаточным условием ожидания, так как потоки никогда не заканчиваются.

Чтобы остановить потоки, вы можете использовать глобальный общий var, например bool stop = false;, тогда, когда вы поймаете сигнал (Ctrl + Z - это сигнал в UNIX), установите stop = true прерывание потоков, так как вы ожидаете с помощью join() также выйдет основная программа.

Пример

void *func1(){
  while(!stop){
    //do something
  }
}