请移步到这里:
http://note.youdao.com/noteshare?id=797569d9f0daf39b84a5eed3f5649622&sub=05396B98DAF44650ACC76BE7F90549F0
线程可以通过调用pthread_cancel函数来请求取消同一进程中的其他线程。
从编程的角度来讲, 不建议使用这个接口。 笔者对该接口的评价不高, 该接口实现了一个似是而非的功能, 却引入了一堆问题。 陈硕在《Linux多线程服务器编程》 一书中也提到过, 不建议使用取消接口来使线程退出, 个人表示十分赞同。
1 函数取消接口
Linux提供了如下函数来控制线程的取消:
int pthread_cancel(pthread_t thread);
一个线程可以通过调用该函数向另一个线程发送取消请求。 这不是个阻塞型接口, 发出请求后,函数就立刻返回了, 而不会等待目标线程退出之后才返回。
如果成功, 该函数返回0, 否则将错误码返回。
对于glibc实现而言, 调用pthread_cancel时, 会向目标线程发送一个SIGCANCEL的信号。
线程收到取消请求后, 会采取什么行动呢? 这取决于该线程的设定。 NPTL提供了函数来设置线程是否允许取消, 以及在允许取消的情况下, 如何取消。
pthread_setcancelstate函数用来设置线程是否允许取消, 函数定义如下:
int pthread_setcancelstate(int state, int *oldstate);
state参数有两种可能的值:
如果取消状态是PTHREAD_CANCEL_DISABLE, 则表示线程不理会取消请求, 取消请求会被暂时挂起, 不予处理。
线程的默认取消状态是PTHREAD_CANCEL_ENABLE。 如果state是PTHREAD_CANCEL_ENABLE, 那么收到取消请求后, 会发生什么? 这取决于线程的取消类型。
pthread_setcanceltype函数用来设置线程的取消类型, 其定义如下:
int pthread_setcanceltype(int type, int *oldtype);
取消类型有两种值:
PTHREAD_CANCEL_ASYNCHRONOUS为异步取消, 即线程可能在任何时间点(可能是立即取消, 但也不一定) 取消线程。 这种取消方式的最大问题在于, 你不知道取消时线程执行到了哪一步。所以, 这种取消方式太粗暴, 很容易造成后续的混乱。 因此不建议使用该取消方式。
PTHREAD_CANCEL_DEFERRED是延迟取消, 线程会一直执行, 直到遇到一个取消点, 这种方式也是新建线程的默认取消类型。
什么是取消点? 就是对于某些函数, 如果线程允许取消且取消类型是延迟取消, 并且线程也收到了取消请求, 那么当执行到这些函数的时候, 线程就可以退出了。
标准规定了很多函数必须是取消点, 由于太多(有好几十个之多) , 就不一一罗列了, 通过man pthreads可以查询到这些取消点函数。
线程执行到取消点, 会自动处理取消请求, 但是如果线程没有用到任何取消点函数, 那该怎么办, 如何响应取消请求?
为了应对这种场景, 系统引入了pthread_testcancel函数, 该函数一定是取消点。 所以编程者可以周期性地调用该函数, 只要有取消请求, 线程就能响应。 该函数定义如下:
void pthread_testcancel(void);
如果线程被取消, 并且其分离状态是可连接的, 那么需要由其他线程对其进行连接。 连接之后, pthread_join函数的第二个参数会被置成PTHREAD_CANCELED, 通过该值可以知道线程并不是“寿终正寝”, 而是被其他线程取消而导致的退出。
线程取消是一种在线程的外部强行终止线程的执行做法, 由于无法预知目标线程内部的情况, 尤其是第一种异步取消类型, 因此可能会带来毁灭性的结果。
目标线程可能会持有互斥量、 信号量或其他类型的锁, 这时候如果收到取消请求, 并且取消类型是异步取消, 那么可能目标线程掌握的资源还没有来得及释放就被迫退出了, 这可能会给其他线程带来不可恢复的后果, 比如死锁(其他线程再也无法获得资源) 。
即使执行异步取消也安然无恙的函数称为异步取消安全函数(async-cancel-safe function) , 手册里说只有下述三个函数是异步取消安全函数, 所以对于其他函数, 一律都不是异步取消安全函数。
pthread_cancel()
pthread_setcancelstate()
pthread_setcanceltype()
所以对编程人员而言, 应该遵循以下原则:
实例代码:
#include
#include
#include
#include
#include
#define _GNU_SOURCE /* See feature_test_macros(7) */
#include
#include
void* th_reader1(void *p)
{
printf("线程1正在写\n");
pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, NULL);//设置为不能取消
sleep(10);
printf("等待结束\n");
pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL);//设置为可以取消
printf("线程1写完了\n");
pthread_exit( (void *)0 );
}
int main()
{
pthread_t tid1;
void *ret1;
printf("start thread1\n");
pthread_create(&tid1, NULL, th_reader1, NULL); //创建线程1
sleep(3);
pthread_cancel(tid1);//取消线程
if(pthread_join(tid1, &ret1) < 0){
perror("join\n");
exit(-1);
}
return 0;
}
运行结果:
# ./a.out
start thread1
线程1正在写
等待结束 //没有输出 线程1写完了
假设遇到取消请求, 线程执行到了取消点, 却没有来得及做清理动作(如动态申请的内存没有释放, 申请的互斥量没有解锁等) , 可能会导致错误的产生, 比如死锁, 甚至是进程崩溃。
下面来看一个简单的例子:
void* cancel_unsafe(void*)
{
static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_lock(&mutex); // 此处不是撤消点
struct timespec ts = {3, 0};
nanosleep(&ts, 0); // 是撤消点
pthread_mutex_unlock(&mutex); // 此处不是撤消点
return 0;
}
int main(void)
{
pthread_t t;
pthread_create(&t, 0, cancel_unsafe, 0);
pthread_cancel(t);
pthread_join(t, 0);
cancel_unsafe(0); // 发生死锁!
return 0;
}
在上面的例子中, nanosleep是取消点, 如果线程执行到此处时被其他线程取消, 就会出现以下情况: 互斥量还没有解锁, 但持有锁的线程已不复存在。 这种情况下其他线程再也无法申请到互斥量,很有可能在某处就会陷入死锁的境地。
为了避免这种情况, 线程可以设置一个或多个清理函数, 线程取消或退出时, 会自动执行这些清理函数, 以确保资源处于一致的状态。 其相关接口定义如下:
void pthread_cleanup_push(void (*routine)(void *),void *arg);
void pthread_cleanup_pop(int execute);
标准允许用宏(macro) 来实现这两个接口, Linux就是用宏来实现的。 这意味着这两个函数必须同时出现, 并且属于同一个语法块。
何为同一个语法块? 比较难解释, 我尝试来解释一下它的反面。 如果两个函数在不同的函数中出现, 它们就不是处于同一个语法块。 示例代码如下:
void foo()
{
.....
pthread_cleanup_pop(0)
.....
}
void *thread_work(void *arg)
{
......
pthread_cleanup_push(clean,clean_arg);
......
foo()
......
}
这个例子比较简单, 因为pthread_cleanup_push在线程的主函数里面, 而pthread_cleanup_pop在另外一个函数里面, 这一对函数明显不在一个语法块里面。
上面这种错误是很好防范的, 比较难防范的是下面这种:
pthread_cleanup_push(clean_func,clean_arg);
......
if(cond)
{
pthread_cleanup_pop(0);
}
在日常编码中很容易犯上面这种错误。 因为pthread_cleanup_push和phtread_cleanup_pop的实现中包含了{和}, 所以将pop放入if{}的代码块中, 会导致括号匹配错乱, 最终会引发编译错误。
第二个需要注意的是, 可以注册多个清理函数, 如下所示:
pthread_cleanup_push(clean_func_1,clean_arg_1)
pthread_cleanup_push(clean_func_2,clean_arg_2)
//...
pthread_cleanup_pop(execute_2);
pthread_cleanup_pop(execute_1);
从push和pop的名字可以看出, 这是栈的风格, 后入先出, 就是后注册的清理函数会先执行。
其中pthread_cleanup_pop的用处是, 删除注册的清理函数。 如果参数是非0值, 那么执行一次, 再删除清理函数。 否则的话, 就直接删除清理函数。
下面看下示例代码:
#include
#include
#include
#include
void clean(void* arg)
{
printf("CLEAN_UP:%s\n",(char*)arg);
}
void *thread(void *param)
{
int input = (int)param;
printf("thread start\n");
pthread_cleanup_push(clean,"first cleanup handler");
pthread_cleanup_push(clean,"second cleanup handler");
/*work logic here*/
if(input != 0){
/*pthread_exit退出, 清理函数总会被执行
*/
pthread_exit((void*)1);
}
pthread_cleanup_pop(0);
pthread_cleanup_pop(0);
/*return 返回, 如果上面
pop函数的参数是
0, 则不会执行清理函数
*/
return ((void *)0);
}
int main()
{
pthread_t tid ;
void *res ;
int ret ;
ret = pthread_create(&tid,NULL,thread,(void*)0);
if(ret != 0)
{
/*error handle here*/
return -1;
}
pthread_join(tid,&res);
printf("first thread exit,return code is %d\n",(int)res);
ret = pthread_create(&tid,NULL,thread,(void*)1);
if(ret != 0)
{
/*error handle here*/
return -1
}
pthread_join(tid,&res);
printf("second thread exit,return code is %d\n",(int)res);
return 0;
}
当线程用return退出, 并且pthread_cleanup_pop的参数是0时, 那么注册的清理函数不被执行:
thread start
first thread exit,return code is 0
thread start
CLEAN_UP:second cleanup handler
CLEAN_UP:first cleanup handler
second thread exit,return code is 1
如果将上面示例代码中的pthread_cleanup_pop的参数改成1, 就会发现, 无论是调用pthread_exit函数返回, 还是在线程的主函数中调用return返回, 都会调用清理函数:
thread start
CLEAN_UP:second cleanup handler
CLEAN_UP:first cleanup handler
first thread exit,return code is 0
thread start
CLEAN_UP:second cleanup handler
CLEAN_UP:first cleanup handler
second thread exit,return code is 1
有了清理函数, 本节开头处提到的例子就可以改进为如下形式了:
void cleanup(void* mutex) {
pthread_mutex_unlock((pthread_mutex_t*)mutex);
}
void* cancel_unsafe(void*) {
static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cleanup_push(cleanup, &mutex);
pthread_mutex_lock(&mutex);
struct timespec ts = {3, 0};
nanosleep(&ts, 0);
pthread_mutex_unlock(&mutex);
pthread_cleanup_pop(0);
return 0;
}
在这种情况下, 如果线程被取消, 清理函数则会负责解锁操作。
注意:
pthread_cleanup_pop(1)函数不要和pthread_cleanup_push函数放到一起,最好放到可以清除数据的地方。