Linux编程——终止线程的正常方式及取消点

来源:互联网 发布:windows nginx php 编辑:程序博客网 时间:2024/06/14 14:35

问题背景:
在使用pthread_cancel和pthread_join退出一个线程时,线程本身会立即停止运行代码并退出吗?如果在线程中有动态分配的内存或是线程恰好运行到被锁住的地方,这时退出应该怎么处理呢?

下面是转载的两篇文章告诉你如果选择取消点(cancellation points)以及如何优雅地清理线程资源,原文分别为:
https://www.ibm.com/developerworks/cn/linux/thread/posix_threadapi/part4/
http://blog.csdn.net/shinichr/article/details/24271675

不过,我的建议是,尽量创建常驻线程,让它一直存在,没事做就等着,省了在取消点上伤脑筋。
更多多线程的编程指南,可参考https://docs.oracle.com/cd/E19455-01/806-5257/index.html。

下文中没有明确说明那些库函数可作为取消点,大家可参考 https://linux.die.net/man/7/pthreads 中的Cancellation points一节。

——————————

线程终止方式

一般来说,Posix的线程终止有两种情况:正常终止和非正常终止。线程主动调用pthread_exit()或者从线程函数中return都将使线程正常退出,这是可预见的退出方式;非正常终止是线程在其他线程的干预下,或者由于自身运行出错(比如访问非法地址)而退出,这种退出方式是不可预见的。

线程终止时的清理

不论是可预见的线程终止还是异常终止,都会存在资源释放的问题,在不考虑因运行出错而退出的前提下,如何保证线程终止时能顺利的释放掉自己所占用的资源,特别是锁资源,就是一个必须考虑解决的问题。
最经常出现的情形是资源独占锁的使用:线程为了访问临界资源而为其加上锁,但在访问过程中被外界取消,如果线程处于响应取消状态,且采用异步方式响应,或者在打开独占锁以前的运行路径上存在取消点,则该临界资源将永远处于锁定状态得不到释放。外界取消操作是不可预见的,因此的确需要一个机制来简化用于资源释放的编程。
在POSIX线程API中提供了一个pthread_cleanup_push()/pthread_cleanup_pop()函数对用于自动释放资源–从pthread_cleanup_push()的调用点到pthread_cleanup_pop()之间的程序段中的终止动作(包括调用pthread_exit()和取消点终止)都将执行pthread_cleanup_push()所指定的清理函数。API定义如下:

void pthread_cleanup_push(void (*routine) (void  *),  void *arg)void pthread_cleanup_pop(int execute)

pthread_cleanup_push()/pthread_cleanup_pop()采用先入后出的栈结构管理,void routine(void *arg)函数在调用pthread_cleanup_push()时压入清理函数栈,多次对pthread_cleanup_push()的调用将在清理函数栈中形成一个函数链,在执行该函数链时按照压栈的相反顺序弹出。execute参数表示执行到pthread_cleanup_pop()时是否在弹出清理函数的同时执行该函数,为0表示不执行,非0为执行;这个参数并不影响异常终止时清理函数的执行。
pthread_cleanup_push()/pthread_cleanup_pop()是以宏方式实现的,这是pthread.h中的宏定义:

#define pthread_cleanup_push(routine,arg)                                     \  { struct _pthread_cleanup_buffer _buffer;                                   \    _pthread_cleanup_push (&_buffer, (routine), (arg));#define pthread_cleanup_pop(execute)                                          \    _pthread_cleanup_pop (&_buffer, (execute)); }

可见,pthread_cleanup_push()带有一个”{“,而pthread_cleanup_pop()带有一个”}”,因此这两个函数必须成对出现,且必须位于程序的同一级别的代码段中才能通过编译。在下面的例子里,当线程在”do some work”中终止时,将主动调用pthread_mutex_unlock(mut),以完成解锁动作。

pthread_cleanup_push(pthread_mutex_unlock, (void *) &mut);pthread_mutex_lock(&mut);/* do some work */pthread_mutex_unlock(&mut);pthread_cleanup_pop(0);

必须要注意的是,如果线程处于PTHREAD_CANCEL_ASYNCHRONOUS状态,上述代码段就有可能出错,因为CANCEL事件有可能在pthread_cleanup_push()和pthread_mutex_lock()之间发生,或者在pthread_mutex_unlock()和pthread_cleanup_pop()之间发生,从而导致清理函数unlock一个并没有加锁的mutex变量,造成错误。因此,在使用清理函数的时候,都应该暂时设置成PTHREAD_CANCEL_DEFERRED模式。为此,POSIX的Linux实现中还提供了一对不保证可移植的pthread_cleanup_push_defer_np()/pthread_cleanup_pop_defer_np()扩展函数,功能与以下代码段相当:

{ int oldtype; pthread_setcanceltype(PTHREAD_CANCEL_DEFERRED, &oldtype); pthread_cleanup_push(routine, arg); ... pthread_cleanup_pop(execute); pthread_setcanceltype(oldtype, NULL); }

线程终止的同步及其返回值

一般情况下,进程中各个线程的运行都是相互独立的,线程的终止并不会通知,也不会影响其他线程,终止的线程所占用的资源也并不会随着线程的终止而得到释放。正如进程之间可以用wait()系统调用来同步终止并释放资源一样,线程之间也有类似机制,那就是pthread_join()函数。

void pthread_exit(void *retval) int pthread_join(pthread_t th, void **thread_return)int pthread_detach(pthread_t th)

pthread_join()的调用者将挂起并等待th线程终止,retval是pthread_exit()调用者线程(线程ID为th)的返回值,如果thread_return不为NULL,则*thread_return=retval。需要注意的是一个线程仅允许唯一的一个线程使用pthread_join()等待它的终止,并且被等待的线程应该处于可join状态,即非DETACHED状态。
如果进程中的某个线程执行了pthread_detach(th),则th线程将处于DETACHED状态,这使得th线程在结束运行时自行释放所占用的内存资源,同时也无法由pthread_join()同步,pthread_detach()执行之后,对th请求pthread_join()将返回错误。
一个可join的线程所占用的内存仅当有线程对其执行了pthread_join()后才会释放,因此为了避免内存泄漏,所有线程的终止,要么已设为DETACHED,要么就需要使用pthread_join()来回收。

关于pthread_exit()和return

理论上说,pthread_exit()和线程宿体函数退出的功能是相同的,函数结束时会在内部自动调用pthread_exit()来清理线程相关的资源。但实际上二者由于编译器的处理有很大的不同。
在进程主函数(main())中调用pthread_exit(),只会使主函数所在的线程(可以说是进程的主线程)退出;而如果是return,编译器将使其调用进程退出的代码(如_exit()),从而导致进程及其所有线程结束运行。
其次,在线程宿主函数中主动调用return,如果return语句包含在pthread_cleanup_push()/pthread_cleanup_pop()对中,则不会引起清理函数的执行,反而会导致segment fault。

——————————
线程取消:
取消操作允许线程请求终止其所在进程总的任何其他线程。不需要线程执行进一步操作时,可以选择取消操作。
取消点:
如果线程模式设置的是异步模式的话,那只有到取消点才会取消线程。下面会讲到两种取消方式。
那取消点有哪些呢?
1:通过pthread_testcancel 调用已编程方式建立线程取消点。
2:线程等待pthread_cond_wait或pthread_cond_timewait中的特定条件。
3:被sigwait(2)阻塞的函数。
4:一些标准的库调用。通常这些调用包括线程可基于阻塞的函数。
default情况下,会启用取消功能。
POSIX标准中,pthread_join(), pthread_testcancel(), pthread_cond_wait(), pthread_cond_timewait(), sem_wait(), sigwait() 等函数以及read,write等会引起阻塞的系统调用。

取消线程函数
int pthread_cancel(pthread_t thread);
成功之后返回0,失败返回错误号,错误号说明如下:
ESRCH:没有找到线程ID相对应的线程。
int phread_setcancelstate(int state,int *oldstate);设置本线程对信号的反应:
状态:
PTHREAD_CANCEL_ENABLE 默认,收到cancel信号马上设置退出状态
PTHREAD_CANCEL_DISABLE 很明显不处理cancel
返回值:
成功之后返回0.失败返回错误号,错误号说明如下:
EINVAL:状态不是PTHREAD_CANCEL_ENABLE或者PTHREAD_CANCEL_DISABLE
那取消方式呢?取消方式有两种,当然只有在PTHREAD_CANCEL_ENABLE状态下有效
int pthread_setcanceltype(int type, int *oldtype);
PTHREAD_CANCEL_ASYNCHRONOUS 立即执行取消信号
PTHREAD_CANCEL_DEFERRED 运行到下一个取消点
有什么区别呢?
在延迟模式(PTHREAD_CANCEL_DEFERRED)下,只能在取消点取消线程,在异步模式下,可以在任意一点取消线程,这建立还是用延迟模式
注意:当取消线程后,线程里面的锁可能还没有unlock,那么就会出现死锁的情况,如何避免呢?
这里介绍下清理处理程序,它可以将状态恢复到与起点一致的状态,其中包括清理已分配的资源和恢复不变量。
使用pthread_cleanup_push和pthread_cleanup_pop,
push函数是将处理程序推送到清理栈,执行顺序自然是FIFO
void pthread_cleanup_push(void(routine)(void ) , void (args);
有了这个清理函数,我们只需要把线程要清理的函数push上去,把unlock放在里面,就不会死锁了,当然之前还要lock一下mutex。
void cleanup(void *arg)
{
pthread_mutex_unlock(&mutex);
}
这里举个取消点的例子:

#include <pthread.h>  #include <stdio.h>  #include <unistd.h>  void* thr(void* arg)  {           pthread_setcancelstate(PTHREAD_CANCEL_ENABLE,NULL);           pthread_setcanceltype(PTHREAD_CANCEL_DEFERRED,NULL);           while(1)           {               pthread_testcancel();           }           printf("thread is not running\n");           sleep(2);  }  int main()  {           pthread_t tid;           int err;           err = pthread_create(&tid,NULL,thr,NULL);           pthread_cancel(tid);           pthread_join(tid,NULL);           sleep(1);           printf("Main thread exited\n");           return 0;  }  
原创粉丝点击