POSIX线程编程

11
我需要编写一个多线程(例如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
这里有一个不错的教程,可以帮助你掌握基础知识:https://computing.llnl.gov/tutorials/pthreads/ - Karoly Horvath
你的函数看起来不错。将 for(;;){ } 替换为 scanf 函数以在控制台中读取字符,您可以检查字符是否等于 'q',然后广播一个信号,以便这两个线程可以正常停止。在此处检查停止线程的方法 https://dev59.com/kHI95IYBdhLWcg3w8iv1 - longbkit
大家好,可以有人看一下修改后的代码并告诉我是否正确吗?非常感谢。 - user489152
7个回答

14

线程终止主要有两种方法。

  • 使用取消点。 当请求取消并且线程到达取消点时,线程将以受控的方式终止执行;
  • 使用信号。 让线程安装一个信号处理程序,提供一种终止机制(设置标志并对 EINTR 做出反应)。

这两种方法都有注意事项。欲了解更多细节,请参考Kill Thread in Pthread Library

在您的情况下,似乎使用取消点是一个好机会。以下是一个有注释的示例。为了清晰起见,省略了错误检查。

#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(),这可能会有些麻烦。但是,如果正确使用,该方法是可行且相当优雅的。


谢谢Alek。我已经阅读了有关取消点的内容。好吧,我正在生成另一个线程,它是一个套接字服务器。我想我必须使用accept()、select()或recv()作为取消点。 - user489152
@user489152:它们三个都是取消点(你不需要“选择”一个函数来执行这个角色;相反,如果你调用它们中的某些函数,它们中的一些已经被“定义”为执行这个任务)。值得注意的一点是,在accept()返回后,您可能需要使用pthread_cleanup_push()close()操作接受的套接字设置一个处理程序,以确保在read()select()被取消时文件描述符正确关闭。 - alecov
假设一个线程(不是主线程)在使用sigwait时,使用信号SIGALRM,同时被其他线程(包括主线程)阻塞:即使它被阻塞在其掩码上,SIGALRM的发生是否会唤醒主线程从暂停中恢复? - roschach

7
答案很大程度上取决于用户按下CtrlC时要执行什么操作。
如果您的工作线程不修改需要在退出时保存的数据,则无需采取任何措施。 SIGINT的默认操作是终止整个进程,包括组成该进程的所有线程。
但是,如果您的线程确实需要执行清理操作,则需要进行一些工作。您需要考虑两个单独的问题:
1. 如何处理信号并将消息传递给线程以通知它们需要终止。 2. 您的线程如何接收和处理请求以终止。
首先,信号处理程序很麻烦。除非您非常小心,否则必须假设大多数库函数不能从信号处理程序中调用。幸运的是,sem_post被指定为异步信号安全,并且完全可以满足您的要求:
1. 在程序开始时,使用sem_init(&exit_sem, 0, 0);初始化一个信号量。 2. 为SIGINT(以及您想要处理的任何其他终止信号,如SIGTERM)安装信号处理程序,该处理程序执行sem_post(&exit_sem);并返回。 3. 将主线程中的for(;;);替换为while (sem_wait(&exit_sem)!=0)。 4. 在sem_wait成功后,主线程应通知所有其他线程它们应该退出,然后等待它们全部退出。
上述操作也可以使用信号掩码和sigwaitinfo完成,但我更喜欢使用信号量方法,因为它不需要您学习大量复杂的信号语义。
现在,有几种方法可以处理工作线程需要退出的通知。我看到的一些选项是:
  1. 让它们定期检查sem_getvalue(&exit_sem),如果返回非零值,则清理并退出。但请注意,如果线程被无限期地阻塞,例如在调用readwrite时,这种方法将不起作用。
  2. 使用pthread_cancel,并仔细在各个位置放置取消处理程序(pthread_cleanup_push)。
  3. 使用pthread_cancel,但也使用pthread_setcancelstate来在大部分代码中禁用取消功能,并且只在您要执行阻塞IO操作时重新启用它。这样,您只需要在启用取消的位置放置清理处理程序。
  4. 学习高级信号语义,并设置额外的信号和中断信号处理程序,通过pthread_kill向所有线程发送该信号,这将导致阻塞系统调用返回一个EINTR错误。然后,您的线程可以对此进行操作,并通过一系列失败返回方式正常退出C。

我不建议初学者使用第四种方法,因为很难正确使用,但对于高级C程序员来说,它可能是最好的方法,因为它允许您使用现有的C惯用语,通过返回值报告异常条件而不是“异常”。

还要注意,使用pthread_cancel时,如果您没有调用任何其他函数作为取消点,则需要定期调用pthread_testcancel。否则,取消请求将永远不会被执行。


@R:感谢您提供的所有意见。我认为我需要做一些作业——进行一些代码调整/测试,然后会与大家分享输出结果。顺便说一句,我的两个线程会清除数据(释放内存等)。但是,由于它们是套接字服务器,在我停止执行并重新启动后,套接字会报告已被绑定。因此,我认为当我停止程序执行时,必须真正“杀死/取消”所有线程。 - user489152
不,你只需要使用 SO_REUSEADDR 选项。顺便说一下,你一开始应该问关于“已绑定”问题的问题... - R.. GitHub STOP HELPING ICE
@R.. 你真的在这里探索了一些可能性。 - cnicutar
我不理解第三点,请解释一下。我改了我的代码,如果有问题请随意编辑。 - user489152
我的观点是这个有点过于复杂了。 - alecov
显示剩余8条评论

3

这是个不好的想法:

for(;;){
}

因为你的主线程将执行不必要的CPU指令。
如果你需要在主线程中等待,请使用pthread_join,如此问题中所回答的:C程序中的多线程

3
你所做的工作是有效的,我没有看出明显的问题(除了你忽略了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和取消点的信息。你还可以安装清理处理程序。当然,检查每个返回值也很重要。


信号是停止线程的最佳方式吗?我的意思是,如果我不使用“信号”方式会怎样? - user489152
如果你仔细看我的答案,你会发现信号只用于通知主线程。次要线程通过 pthread_cancel 停止。我只是注意到信号是一个复杂的问题,它们引入了许多(有时微妙的)问题。 - cnicutar
+1 这是我迄今为止看到的唯一正确的答案,即使它不完整。 - R.. GitHub STOP HELPING ICE
1
事实上,在创建任何线程之前,您希望阻止信号。否则,在线程启动并阻止信号之间存在竞争条件窗口。 - Maxim Egorushkin

1

如果在线程函数中调用pthread_cancel并使用pthread_cleanup_push来清理线程动态分配的数据或执行任何终止任务,那不是更容易吗?

所以,思路如下:

  1. 编写处理信号的代码
  2. 当您按下ctrl+c时...将调用处理函数
  3. 此函数取消线程
  4. 创建的每个线程都使用pthread_cleanup_push设置了线程清理函数
  5. 当线程被取消时,将调用pthread_cleanup_push的函数
  6. 在退出之前加入所有线程

这似乎是一个简单而自然的解决方案。

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;
}


1

我查看了你更新的代码,但仍然不正确。

信号处理必须只在一个线程中完成。针对进程的信号(如SIGINT)会被传递到未阻塞该信号的任何线程中。换句话说,不能保证给定的三个线程中的哪一个将接收SIGINT。在多线程程序中,最好的做法是在创建任何线程之前先阻塞所有信号,一旦所有线程都已创建,在主线程中解除信号阻塞(通常是主线程最适合处理信号)。有关更多信息,请参见Signal ConceptsSignalling in a Multi-Threaded Process

pthread_cancel最好避免使用,没有理由使用它。要停止线程,您应该以某种方式与它们通信,告诉它们应该终止并等待它们自愿终止。通常,线程将具有某种事件循环,因此向其他线程发送事件应该相对简单。


谢谢Maxim。我会研究一下的。问题是我的线程在一个无限循环中运行,只有当我终止它们(通常我不会这样做,因为它们被认为是守护线程)并使用Ctrl+C时,它们才会全部退出。 - user489152

0

在主函数中不需要使用for循环。使用th1->join(); th2->join();作为等待条件就足够了,因为线程永远不会结束。

要停止线程,您可以使用全局共享变量,例如bool stop = false;,然后在捕获信号(Ctrl+Z是UNIX中的信号)时设置stop = true以终止线程,由于您正在使用join()等待,因此主程序也将退出。

示例

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

这是无效的。在检查“stop”的值时,您必须持有互斥锁。 - R.. GitHub STOP HELPING ICE
严格来说,你是正确的。但由于你只在一个线程(主线程)中编写,并且只从其他两个线程中读取,所以这可以工作。你可以在变量前加上volatile,向编译器发出信号,表明该变量是从范围外修改的,不应被优化掉,并且每次访问时应从每个内存重新读取。 - RedX
否,编译器在不包含编译器无法证明不会修改stop的代码的while循环中将 while(!stop)替换为if(!stop)while(1)是完全合理的。 - R.. GitHub STOP HELPING ICE
我认为volatile就是为这些情况而设计的。在循环中看起来变量不会被修改,但异步地从编译器(可能)无法推断的另一个地方进行修改。 - RedX

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接