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

请移步到这里:

http://note.youdao.com/noteshare?id=797569d9f0daf39b84a5eed3f5649622&sub=05396B98DAF44650ACC76BE7F90549F0

 

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

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
#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函数放到一起,最好放到可以清除数据的地方。

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

你可能感兴趣的:(Linux应用编程)