在Linux中何时使用pthread_exit(),何时使用pthread_join()?

60
我是新手,正在尝试了解pthreads。我看到了一些像下面这样的例子。
我可以看到main()被APIpthread_exit()阻塞,我也见过一些例子,其中主函数被APIpthread_join()阻塞。我不明白什么时候使用什么?
我参考了以下网站-https://computing.llnl.gov/tutorials/pthreads/。我无法理解何时使用pthread_join()和何时使用pthread_exit()的概念。
有人能解释一下吗?同时,欢迎提供一个好的pthread教程链接。
#include <pthread.h>
#include <stdio.h>
#define NUM_THREADS     5

void *PrintHello(void *threadid)
{
   long tid;
   tid = (long)threadid;
   printf("Hello World! It's me, thread #%ld!\n", tid);
   pthread_exit(NULL);
}

int main (int argc, char *argv[])
{
   pthread_t threads[NUM_THREADS];
   int rc;
   long t;
   for(t=0; t<NUM_THREADS; t++){
      printf("In main: creating thread %ld\n", t);
      rc = pthread_create(&threads[t], NULL, PrintHello, (void *)t);
      if (rc){
         printf("ERROR; return code from pthread_create() is %d\n", rc);
         exit(-1);
      }
   }

   /* Last thing that main() should do */
   pthread_exit(NULL);

意识到了另一件事,即

pthread_cancel(thread);
pthread_join(thread, NULL);

有时候,在线程执行时,你可能想要取消它。 你可以使用 pthread_cancel(thread); 来实现。 但是,请记住你需要启用 pthread 取消支持。 同时,还需要进行取消时的清理工作。
thread_cleanup_push(my_thread_cleanup_handler, resources);
pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, 0);

static void my_thread_cleanup_handler(void *arg)
{
  // free
  // close, fclose
}
10个回答

61

如openpub文档所述,pthread_exit()将退出调用它的线程。

在您的情况下,由于主函数调用它,主线程将终止,而您创建的线程将继续执行。这通常用于主线程仅需要创建线程并让线程完成其工作的情况。

pthread_join将挂起调用它的线程,除非目标线程终止。

这在您想要等待线程终止后再在主线程中继续处理时很有用。


2
也许自你回答这个问题以来,实现方式有所改变。根据 dexterous 所提到的网站,通过在 main() 显式地调用 pthread_exit() 作为它要执行的最后一件事情,main() 将被阻塞并保持活动状态,以支持它创建的线程直到它们完成为止。也就是说,一个线程不能在 main 线程 (即 main())之外存在。 - oklm

17

pthread_exit 函数终止调用线程,而 pthread_join 函数挂起调用线程直到目标线程执行完成。

它们在开放组文档中有详细的解释:


3
你是否注意到在main()函数中,我调用了pthread_exit()。这会阻塞main()的终止,使线程运行并完成。这种方式与pthread_join()非常相似。此外,pthread_join()阻塞main()的终止,直到线程被执行为止。 - dexterous
你知道有哪些好的链接可以开始了解pthread吗? - dexterous
1
@BasileStarynkevitch,为什么不应该呢?这是一个明确定义的用例。 - Jens Gustedt

10

这两种方法都确保您的进程在所有线程结束之前不会终止。

join方法让您的main函数的线程显式等待所有需要“加入”的线程。

pthread_exit方法以受控方式终止您的main函数和线程。 main具有特殊性质,即如果以其他方式结束main,则将终止包括全部其他线程在内的整个进程。

为了使这个方法起作用,您必须确保没有任何一个线程正在使用在main函数内部声明的本地变量。该方法的优点是您的main函数不必知道在您的进程中启动了哪些线程,例如因为其他线程已经创建了main不知道任何信息的新线程。


不是很清楚,您是说pthread_exit会阻塞main()函数以终止,以便其他线程得到机会并正常工作。您能否给我指出一个关于pthread的好链接?看起来,我缺少基础知识。 - dexterous
5
不,它没有阻止 main 函数。 main 函数的线程将被终止。你似乎混淆了main函数、它的执行线程和重新组合所有线程的进程。main函数有两种特殊情况:它是进程中开启的第一个线程,并且如果通过returnexit结束,它会终止整个进程。但是,如果你使用pthread_exit结束它,只有该线程结束而其他线程仍然存在。 - Jens Gustedt

7

pthread_exit() API

pthread_exit() API用于结束调用线程。 调用此函数后,将启动一个复杂的清理机制。 当清理完成后,线程将被终止。 当在线程中使用pthread_create()创建线程时,如果调用return()例程,则会隐式地调用pthread_exit() API。 实际上,从由pthread_create()创建的线程中调用return()和pthread_exit()具有相同的影响。

非常重要的是区分在main()函数启动时隐式创建的初始线程和由pthread_create()创建的线程。 从main()函数中调用return()例程将隐式调用exit()系统调用,并且整个进程将终止。 不会启动任何线程清理机制。 从main()函数中调用pthread_exit()将启动清理机制,并在完成其工作后终止初始线程。

当从main()函数中调用pthread_exit()时,整个进程(以及其他线程)的情况取决于PTHREAD实现。 例如,在IBM OS/400实现中,当从main()函数中调用pthread_exit()时,整个进程,包括其他线程,都将终止。 其他系统可能会有不同的行为。 在大多数现代Linux机器上,从初始线程中调用pthread_exit()不会终止整个进程,直到所有线程都终止。 如果想编写可移植的应用程序,请小心使用main()中的pthread_exit()。

pthread_join() API

是一种方便的等待线程终止的方法。 您可以编写自己的函数来等待线程终止,可能更适合您的应用程序,而不是使用pthread_join()。 例如,它可以是基于条件变量等待的函数。

我建议阅读David R. Butenhof的书籍“Programming with POSIX Threads”。 它很好地解释了讨论的主题(以及更复杂的事物)(尽管一些实现细节,如在主函数中使用pthread_exit,不总是反映在书中)。


3
您的代码中不需要调用pthread_exit(3)
一般来说,main线程不应该调用pthread_exit,但通常应该调用pthread_join(3)等待其他线程完成。
在您的PrintHello函数中,不需要调用pthread_exit,因为从该函数返回时会自动执行退出操作。
因此,您的代码应该是:
void *PrintHello(void *threadid)  {
  long tid = (long)threadid;
  printf("Hello World! It's me, thread #%ld!\n", tid);
  return threadid;
}

int main (int argc, char *argv[]) {
   pthread_t threads[NUM_THREADS];
   int rc;
   intptr_t t;
   // create all the threads
   for(t=0; t<NUM_THREADS; t++){
     printf("In main: creating thread %ld\n", (long) t);
     rc = pthread_create(&threads[t], NULL, PrintHello, (void *)t);
     if (rc) { fprintf(stderr, "failed to create thread #%ld - %s\n",
                                (long)t, strerror(rc));
               exit(EXIT_FAILURE);
             };
   }
   pthread_yield(); // useful to give other threads more chance to run
   // join all the threads
   for(t=0; t<NUM_THREADS; t++){
      printf("In main: joining thread #%ld\n", (long) t);
      rc = pthread_join(&threads[t], NULL);
      if (rc) { fprintf(stderr, "failed to join thread #%ld - %s\n",
                                (long)t, strerror(rc));
               exit(EXIT_FAILURE);
      }
   }
}

2

pthread_exit()将终止调用线程并从中退出(但如果它没有与主线程分离,则调用线程使用的资源不会释放到操作系统中)。

pthread_join()将等待或阻塞调用线程,直到目标线程终止。简单地说,它会等待目标线程退出。

在您的代码中,如果您在PrintHello函数中的pthread_exit()之前放置sleep(或delay),则主线程可能会退出并终止整个进程,尽管您的PrintHello函数尚未完成,它也将终止。 如果您在主函数中调用pthread_exit()之前在主函数中使用pthread_join()函数,它将阻塞主线程并等待完成您的调用线程(PrintHello)。


1
在主线程中使用pthread_exit(而不是pthread_join)会让主线程处于僵尸状态。由于没有使用pthread_join,其他已终止的可连接线程也将保持僵尸状态并导致资源泄漏

未能与可连接线程(即未分离线程)进行连接会产生“僵尸线程”。请避免这样做,因为每个僵尸线程都会消耗一些系统资源,当积累了足够多的僵尸线程时,将无法再创建新的线程(或进程)。

另一个问题是,让主线程处于僵尸状态,而其他线程正在运行,可能会在各种条件下引起实现相关的问题,例如如果在主线程中分配资源或在其他线程中使用局部于主线程的变量。
此外,只有在进程退出时才会释放所有共享资源,它并没有节省任何资源。因此,我认为应该避免使用pthread_exit来替换pthread_join

1

嗯。

POSIX pthread_exit 描述来自 http://pubs.opengroup.org/onlinepubs/009604599/functions/pthread_exit.html

After a thread has terminated, the result of access to local (auto) variables of the thread is 
undefined. Thus, references to local variables of the exiting thread should not be used for 
the pthread_exit() value_ptr parameter value.

这似乎与本地 main() 线程变量仍然可访问的想法相矛盾。


0
当调用pthread_exit()时,调用线程的堆栈对于任何其他线程来说都不再是“活动”内存。 “静态”内存分配的.data、.text和.bss部分仍然可供所有其他线程使用。 因此,如果您需要将某些内存值传递到pthread_join()调用者中以供其查看,则需要为调用pthread_join()的线程“可用”。 它应该使用malloc()/new进行分配,在pthread_join线程的堆栈上进行分配,1)一个堆栈值,该值由pthread_create调用者传递给pthread_join调用者或以其他方式提供给调用pthread_exit()的线程,或2)一个静态的.bss分配值。
了解如何在线程堆栈和.data/.bss内存部分之间管理内存非常重要,这些内存部分用于存储进程范围的值。

0
  #include<stdio.h>
  #include<pthread.h>
  #include<semaphore.h>
 
   sem_t st;
   void *fun_t(void *arg);
   void *fun_t(void *arg)
   {
       printf("Linux\n");
       sem_post(&st);
       //pthread_exit("Bye"); 
       while(1);
       pthread_exit("Bye");
   }
   int main()
   {
       pthread_t pt;
       void *res_t;
       if(pthread_create(&pt,NULL,fun_t,NULL) == -1)
           perror("pthread_create");
       if(sem_init(&st,0,0) != 0)
           perror("sem_init");
       if(sem_wait(&st) != 0)
           perror("sem_wait");
       printf("Sanoundry\n");
       //Try commenting out join here.
       if(pthread_join(pt,&res_t) == -1)
           perror("pthread_join");
       if(sem_destroy(&st) != 0)
           perror("sem_destroy");
       return 0;
   }

将此代码复制并粘贴到gdb上。Onlinegdb可以使用,您可以自行查看。

确保您理解一旦创建了线程,进程会与主线程同时运行。

  1. 没有join,主线程继续运行并返回0
  2. 使用join,主线程将被卡在while循环中,因为它等待线程执行完毕。
  3. 使用join并删除注释掉的pthread_exit,线程将在运行while循环之前终止,主线程将继续运行
  4. pthread_exit的实际用途可以用作if条件或case语句,以确保某些代码的1个版本在退出之前运行。
void *fun_t(void *arg)
   {
       printf("Linux\n");
       sem_post(&st); 
       if(2-1 == 1)  
           pthread_exit("Bye");
       else
       { 
           printf("We have a problem. Computer is bugged");
           pthread_exit("Bye"); //This is redundant since the thread will exit at the end
                                //of scope. But there are instances where you have a bunch
                                //of else if here.
       }
   }


我想展示在本例中有时需要使用信号量来先运行一段代码的情况。
#include<stdio.h>
#include<pthread.h>
#include<semaphore.h>

sem_t st;

void* fun_t (void* arg)
{
    printf("I'm thread\n");
    sem_post(&st);
}

int main()
{
    pthread_t pt;
    pthread_create(&pt,NULL,fun_t,NULL);
    sem_init(&st,0,0);
    sem_wait(&st);
    printf("before_thread\n");
    pthread_join(pt,NULL);
    printf("After_thread\n");
    
}

注意到 fun_t 函数是在 "before thread" 之后运行的。 如果它是从上到下线性执行的,则预期输出将为 before thread,I'm thread,after thread。 但在这种情况下,我们阻止主线程继续运行,直到 func_t 释放信号量。 结果可以通过 https://www.onlinegdb.com/ 进行验证。

在你的例子中,线程可能会在主线程调用sem_init()之前调用sem_post()。 - Rachid K.
这也是演示信号量的意图。 sem_init(&st,0,0)之后将紧随其后的sem_wait(&st)阻止主程序进一步执行,直到调用sem_post。但如果它仍在等待中,则不会调用sem_post因为它落后于主程序。pt同时与主线程运行。当init和wait在主线程上发生时,pt正在运行sem_post。现在,信号量已被释放。您可以在调试器上尝试它。注释掉sem_post行,在wait后面移动sem_post行,并在wait之前移动它以查看区别。 - Michael Kwan
无论如何,这不是一个正确的程序:我们不会在未初始化资源的情况下使用它。即使你很幸运全局变量被初始化为0,这也可能是sem_init()初始化的内容,但在初始化之前使用资源绝对是不正确的,而且你的程序不能保证线程不会在主线程调用sem_init()之前调用sem_post()。 - Rachid K.

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