终止正在运行的子线程(一、几种方式的介绍)

时间:2022-01-13 05:00:59

最近开发的东西有涉及到线程的创建和释放,由于对这一块不是很熟悉,查阅很多资料,现记录如下:


如何正确的终止正在运行的子线程

转自http://www.cnblogs.com/Creator/archive/2012/03/21/2408413.html(略有改动)

      最近开发一些东西,线程数非常之多,当用户输入Ctrl+C的情形下,默认的信号处理会把程序退出,这时有可能会有很多线程的资源没有得到很好的释放,造成了内存泄露等等诸如此类的问题,所以,就如上述使用场景一般,如何从待外部安全的终止正在运行的线程,就是本文要讨论的内容。

    首先我们来看一下,让当前正在运行的子线程停止的所有方法

1.任何一个线程调用exit

2.pthread_exit

3.pthread_kill

4.pthread_cancel 

 

PS:下面的代码都会用到一个函数checkResults:

static void checkResults(char *string, int rc) {
if (rc) {
printf("Error on : %s, rc=%d",
string, rc);
exit(EXIT_FAILURE);
}
return;
}

 下面进行一一分析:

1)任何一个线程调用exit

      任何一个线程只要调用了exit都会导致进程结束,各种子线程当然也能很好的结束了,可是这种退出会有一个资源释放的问题.我们知道当一个进程终止时,内核对该进程所有尚未关闭的文件描述符调用close关闭,所以即使用户程序不调用close,在终止时内核也会自动关闭它打开的所有文件。标准C++ IO流也会很好的在exit退出时得到flush并且释放资源,这些东西并不会造成资源的浪费(系统调用main函数入口类似于exit(main(argc,argv))).实际上,无论进程怎样结束,系统都将会释放掉所有代码所申请的资源,无论是堆上的还是栈上的。这种结束所有线程(包括主线程)的方式实际上在很多时候是非常可取的,但是对于针对关闭时进行一些别的逻辑的处理(指非资源释放逻辑)就不会很好,例如我想在程序被kill掉之前统计一下完成了多少的工作,这个统计类似于MapReduce,需要去每个线程获取,并且最后归并成一个统一的结果等等场景。


2) pthread_exit

    当前运行的线程运行到pthread_exit后直接退出线程。对于各个子线程能够清楚地知道自己在什么时候结束的情景下,这个函数非常好用。可是实际上很多时候一个线程不知道自己在什么时候会结束,例如遭遇Ctrl+C时,kill进程时。当然如果排除所有的外界干扰的话,那就让每个线程干完自己的事情后,然后自觉地乖乖的调用pthread_exit就可以了。

    这里还有一种方法,既然子线程可以通过pthread_exit来正确退出,那么我们可以在遭遇Ctrl+C时,kill进程时处理signal信号,然后分别给在某一个线程可以访问的公共区域存上一个flag变量,线程内部每运行一段时间(很短)来检查一下flag,若发现需要终止自己时,自己调用pthread_exit,此法有一个弱点就是当子线程需要进行阻塞的操作时,可能无暇顾及检查flag,例如socket阻塞操作。如果你的子线程的任务基本没有非阻塞的函数,那么这么做也不失为一种很好的方案。

 

3) pthread_kill

   不要被这个可怕的邪恶的名字所吓倒,其实pthread_kill并不像他的名字那样威力大,使用之后,你会感觉,他徒有虚名而已。

    pthread_kill的职责其实只是向指定的线程发送signal信号而已,并没有真正的kill掉一个线程,当然这里需要说明一下,有些信号的默认行为就是exit,那此时你使用pthread_kill发送信号给目标线程,目标线程会根据这个信号的默认行为进行操作,有可能是exit。当然我们同时也可以更改获取某个信号的行为,以此来达到我们终止子线程的目的。

#define _MULTI_THREADED
#include <pthread.h>
#include <stdio.h>
#include <signal.h>
#include "check.h"

/*
struct sigaction {
         __sighandler_t sa_handler;
        unsigned long sa_flags;
        void (*sa_restorer)(void);
        sigset_t sa_mask;   /* mask last for extensibility */
};
*/

void sighand(int signo);

void *threadfunc(void *parm)
{
  pthread_t             self = pthread_self();
  pthread_id_np_t       tid;
  int                   rc;

  pthread_getunique_np(&self, &tid);
  printf("Thread 0x%.8x %.8x entered\n", tid);
  errno = 0;
  rc = sleep(30);
  if (rc != 0 && errno == EINTR) {
    printf("Thread 0x%.8x %.8x got a signal delivered to it\n",
           tid);
    return NULL;
  }
  printf("Thread 0x%.8x %.8x did not get expected results! rc=%d, errno=%d\n",
         tid, rc, errno);
  return NULL;
}

int main(int argc, char **argv)
{
  int                     rc;
  int                     i;
  struct sigaction        actions;
  pthread_t               threads;

  printf("Enter Testcase - %s\n", argv[0]);
 
  printf("Set up the alarm handler for the process\n");
  memset(&actions, 0, sizeof(actions));
  sigemptyset(&actions.sa_mask);
  actions.sa_flags = 0;
  actions.sa_handler = sighand;

  rc = sigaction(SIGALRM,&actions,NULL);
  checkResults("sigaction\n", rc);

  rc = pthread_create(&threads, NULL, threadfunc, NULL);
  checkResults("pthread_create()\n", rc);

  sleep(3);

  rc = pthread_kill(threads, SIGALRM);
  checkResults("pthread_kill()\n", rc);

  rc = pthread_join(threads, NULL);
  checkResults("pthread_join()\n", rc);

  printf("Main completed\n");
  return 0;
}

void sighand(int signo)
{
  pthread_t             self = pthread_self();
  pthread_id_np_t       tid;
 
  pthread_getunique_np(&self, &tid);
  printf("Thread 0x%.8x %.8x in signal handler\n",
         tid);
  return;
}

运行结果:

Output:

Enter Testcase - QP0WTEST/TPKILL0
Set up the alarm handler for the process
Thread 0x00000000 0000000c entered
Thread 0x00000000 0000000c in signal handler
Thread 0x00000000 0000000c got a signal delivered to it
Main completed
  可见,我们可以通过截获的 signal 信号,来释放掉线程申请的资源,可是遗憾的是我们不能在 signal 处理函数内调用 pthread_exit 来终结掉我们想要终结的线程,因为 pthread_exit 只能终结当前线程,而 signal 被调用的方式可以理解为内核的回调,不是在同一个线程运行的,所以这里只能做处理释放资源的事情,而线程内部只能通过判断有没有被中断(一般是 EINTR )来决定是否要求自己结束,判定后可以调用 pthread_exit 退出。

   此法对于一般的操作也是非常可行的,可是在有的情况下就不是一个比较好的方法了,比如我们有一些线程在处理网络IO事件,假设它是一种一个客户端对应一个服务器线程,阻塞从Socket中读消息的情况。我们一般在网络IO的库里面会加上对EINTR信号的处理,例如recv时发现返回值小于0,检查error后,会进行他对应的操作。有可能它会再recv一次,那就相当于我的线程根本就不会终止,因为网络IO的类有可能不知道在获取EINTR时要终止线程。也就是说这不是一个特别好的可移植方案,如果你线程里的操作使用了很多外来的不太熟悉的类,而且你并不知道它对EINTR的处理手段是什么,这时你在使用这样的方法来终止就有可能出问题了。而且如果你不是特别熟悉这方面的话你会很苦恼,为什么我的测试代码全是ok的,一加入你们部门开发的框架进来就不ok了,肯定是你们框架出问题了。好了,为了不必要的麻烦,我最后没有使用这个方案。

 

3)pthread_cancel

    这个方案是我最终采用的方案,我认为是解决这个问题,相比较而言,通用的最好的解决方案。

   pthread_cancel可以单独使用,因为在很多系统函数里面本身就有很多的断点,当调用这些系统函数时就会命中其内部的断点来结束线程如下面的代码中,即便注释掉我们自己设置的断点pthread_testcancel()程序还是一样的会被成功的cancel掉,因为printf函数内部有取消点(如果想了解更多的函数的取消点情况,可以阅读《Unix高级环境编程》的线程部分

#include <pthread.h>
#include <stdio.h>
#include<stdlib.h>
#include <unistd.h>
void *threadfunc(void *parm)
{
printf("Entered secondary thread\n");
while (1) {
printf("Secondary thread is looping\n");
pthread_testcancel();
sleep(1);
}
return NULL;
}

int main(int argc, char **argv)
{
pthread_t thread;
int rc=0;

printf("Entering testcase\n");

/* Create a thread using default attributes */
printf("Create thread using the NULL attributes\n");
rc = pthread_create(&thread, NULL, threadfunc, NULL);
checkResults("pthread_create(NULL)\n", rc);

/* sleep() is not a very robust way to wait for the thread */
sleep(1);

printf("Cancel the thread\n");
rc = pthread_cancel(thread);
checkResults("pthread_cancel()\n", rc);

/* sleep() is not a very robust way to wait for the thread */
sleep(10);
printf("Main completed\n");
return 0;
}

输出:

Entering testcase
Create thread using the NULL attributes
Entered secondary thread
Secondary thread is looping
Cancel the thread
Main completed

  POSIX保证了绝大部分的系统调用函数内部有取消点,我们看到很多在cancel调用的情景下,recvsend函数最后都会设置pthread_testcancel()取消点,其实这不是那么有必要的。那么究竟什么时候该pthread_testcancel()出场呢?在《Unix高级环境编程》中有说,当遇到大量的基础计算时(如科学计算),需要自己来设置取消点

   ok,得益于pthread_cancel,我们很轻松的把线程可以cancel掉,可是我们的资源呢?何时释放...

       下面来看两个pthread函数

void pthread_cleanup_push(void (*routine)(void *), void *arg);  
void pthread_cleanup_pop(int execute);
  这两个函数能够保证在 函数pthread_cleanup_push 调用之后, 函数pthread_cleanup_pop调用之前,任何形式的线程结束后都会调用之前向 pthread_cleanup_push 注册的回调函数

        另外我们还可通过下面这个函数来设置一些状态

int pthread_setcanceltype(int type, int *oldtype); 

Cancelability

Cancelability State

Cancelability Type

disabled

PTHREAD_CANCEL_DISABLE

PTHREAD_CANCEL_DEFERRED

disabled

PTHREAD_CANCEL_DISABLE

PTHREAD_CANCEL_ASYNCHRONOUS

deferred

PTHREAD_CANCEL_DISABLE

PTHREAD_CANCEL_DEFERRED

asynchronous

PTHREAD_CANCEL_ENABLE

PTHREAD_CANCEL_ASYNCHRONOUS

    当设置typePTHREAD_CANCEL_ASYNCHRONOUS时,线程并不会等待命中取消点才结束,而是立马结束。

#include <pthread.h>                                                            
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
int footprint=0;
char *storage;

void freerc(void *s)
{
free(s);
puts("the free called");
}

void *thread(void *arg) {
int rc=0, oldState=0;
rc = pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &oldState); //close the cancel switch
checkResults("pthread_setcancelstate()\n", rc);
if ((storage = (char*) malloc(80)) == NULL) {
perror("malloc() failed");
exit(6);
}
rc = pthread_setcancelstate(PTHREAD_CANCEL_ENABLE,&oldState); //open the cancel switch
checkResults("pthread_setcancelstate(2)\n", rc);
/* Plan to release storage even if thread doesn't exit normally */

pthread_cleanup_push(freerc, storage); /*the freerc is method here, you can use your own method*/

puts("thread has obtained storage and is waiting to be cancelled");
footprint++;
while (1)
{
pthread_testcancel(); //make a break point here
//pthread_exit(NULL); //test exit to exam whether the freerc method called
sleep(1);
}

pthread_cleanup_pop(1);
}

main() {
pthread_t thid;
void *status=NULL;

if (pthread_create(&thid, NULL, thread, NULL) != 0) {
perror("pthread_create() error");
exit(1);
}

while (footprint == 0)
sleep(1);

puts("IPT is cancelling thread");

if (pthread_cancel(thid) != 0) {
perror("pthread_cancel() error");
sleep(2);
exit(3);
}

if (pthread_join(thid, &status) != 0) {
if(status != PTHREAD_CANCELED){
perror("pthread_join() error");
exit(4);
}
}
if(status == PTHREAD_CANCELED)
puts("PTHREAD_CANCELED");

puts("main exit");
}