0
点赞
收藏
分享

微信扫一扫

UNIX高级编程:第12章 线程控制

1 线程取消

线程可以通过调用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_ENABLE
  • PTHREAD_CANCEL_DISABLE

如果取消状态是PTHREAD_CANCEL_DISABLE, 则表示线程不理会取消请求, 取消请求会被暂时挂起, 不予处理。
线程的默认取消状态是PTHREAD_CANCEL_ENABLE。 如果state是PTHREAD_CANCEL_ENABLE, 那么收到取消请求后, 会发生什么? 这取决于线程的取消类型。
pthread_setcanceltype函数用来设置线程的取消类型, 其定义如下:

int pthread_setcanceltype(int type, int *oldtype);

取消类型有两种值:

  • PTHREAD_CANCEL_DEFERRED
  • PTHREAD_CANCEL_ASYNCHRONOUS

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()

所以对编程人员而言, 应该遵循以下原则:

  1. 轻易不要调用pthread_cancel函数, 在外部杀死线程是很糟糕的做法, 毕竟如果想通知目标线程退出, 还可以采取其他方法。
  2. 如果不得不允许线程取消, 那么在某些非常关键不容有失的代码区域, 暂时将线程设置成不可取消状态, 退出关键区域之后, 再恢复成可以取消的状态。
  3. 在非关键的区域, 也要将线程设置成延迟取消,永远不要设置成异步取消。

实例代码:

#include <stdlib.h>
#include <pthread.h>

#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#define _GNU_SOURCE /* See feature_test_macros(7) */
#include <unistd.h>
#include <sys/syscall.h>

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写完了

2 线程清理函数

假设遇到取消请求, 线程执行到了取消点, 却没有来得及做清理动作(如动态申请的内存没有释放, 申请的互斥量没有解锁等) , 可能会导致错误的产生, 比如死锁, 甚至是进程崩溃。
下面来看一个简单的例子:
 

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值, 那么执行一次, 再删除清理函数。 否则的话, 就直接删除清理函数。

  • 第三个问题最关键, 何时会触发注册的清理函数:
  • 当线程的主函数是调用pthread_exit返回的, 清理函数总是会被执行。
  • 当线程是被其他线程调用pthread_cancel取消的, 清理函数总是会被执行。
  • 当线程的主函数是通过return返回的, 并且pthread_cleanup_pop的唯一参数execute是0时, 清理函数不会被执行。
  • 当线程的主函数是通过return返回的, 并且pthread_cleanup_pop的唯一参数execute是非零值时, 清理函数会执行一次。

下面看下示例代码:
 

#include<stdio.h>
#include<stdlib.h>
#include<pthread.h>
#include<string.h>
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函数放到一起,最好放到可以清除数据的地方。

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 


举报

相关推荐

0 条评论