pthread_cancel在C++中使用的坑
问题现象
在项目中,某些情况下需要动态地创建和销毁线程。Linux系统下,一般用到的是posix线程库pthread提供的一系列API。此篇讲述的便是在C++11中使用posix线程库pthread_cancel销毁线程,而引起的进程终止(Abort coredump)情况。
出问题的代码大致如下(提炼了核心部分,非完全代码):
class TcpServer{
public:
TcpServer(){
pthread_create(&tid, NULL, threadFunc, NULL);
}
~TcpServer(){
pthread_cancel(tid);
pthread_join(tid, NULL);
}
void* threadFunc(void*){
while(1){
sleep(1);
}
}
...
private:
ind socket_fd;
};
class NetNodes{
public:
NetNodes(){}
~NetNodes(){}
void StopThread(){
pthread_cancel(tid);
pthread_join(tid, NULL);
}
void* threadFunc(void*){
while(1){
...
DestoryNetNode(fd);
...
}
}
void DestoryNetNode(int fd){
nodes.erase(fd);
}
private:
std::map<int, TcpServer*> nodes;
};
int main(){
NetNodes nodes;
...
nodes.StopThread();
...
}
程序挂死的堆栈信息如下:
问题分析
由堆栈信息分析,程序终止是触发了std::terminate(),并且是发生在TcpServer的析构函数中,在析构函数中,执行了pthread_join()函数,触发了内部的Unwind_ForceUnwind()。值得注意的是 pthread_join() 函数是可以作为线程取消点的,并且结合了项目日志上下文,触发过 nodes.StopThread() 的动作。
所以,大致的流程是这样的:
- 主线程试图停止 nodes 的处理线程,调用了 pthread_cancel();
- nodes 的处理线程在运行至取消点时才会退出,而刚好运行到 DestoryNetNode(),删除TcpServer,TcpServer 的析构函数内部刚好有取消点 pthread_join();
- pthread_join() 内部触发了 Unwind_ForceUnwind() ,抛出了异常;
- 异常发生在 TcpServer 的析构函数中,从而触发了 std::terminate() ,引发了程序终止退出;
这里要先了解几个知识点:线程的取消和取消点,C++的异常处理
线程的取消和取消点
pthread_cancel 向指定的线程发生取消信号,而如何处理取消信号由目标线程决定。目标线程可能忽略,或立即终止,或继续运行至取消点时才终止。
线程的取消点
- 通过 pthread_testcancel() 调用设置的取消点;
- 通过 pthread_cond_wait()、pthread_cond_timewait()、pthread_join() posix线程库函数;
- 通过 sem_wait()、sigwait()、send()、read()、sleep()等引起阻塞的系统调用;
C和C++在实现线程取消点上的差异
- C++11的 pthread_cancel() 利用了C++的异常机制来触发,通过引发一种无法捕获和抛出的“特殊异常”来实现,并触发堆栈展开、调用 C++ 析构函数并运行使用 pthread_cleanup_push() 注册的代码。其行为是一个forced_unwind,类似于一个异常。这个异常由被取消的线程抛出,注意这个异常catch到后必须重新抛出,否则无法正常完成栈清理。
- C版本中,__pthread_unwind是通过setjmp/longjmp实现的,不同于C++;
C++的异常处理
noexcept声明
在C++11中,可以使用noexcept来表明某个函数不会抛出异常
void function1(int) noexcept; //不会抛出异常
void function2(int); //可能抛出异常
void function3(int) noexcept(false); //可能抛出异常
void function4(int) noexcept(表达式); //根据表达式真假,决定是否不会抛出异常
- C++11 中,析构函数默认是noexcept(true)的,即承诺不会抛出异常;
- 在C++11中如果noexcept修饰的函数抛出了异常,编译器可以选择直接调用std::terminate()函数来终止程序的运行;
- 编译器不会在编译期间检查noexcept声明,实际上,如果一个函数在声明了noexcept 的同时又含有throw语句或调用了可能抛出异常的其他函数,编译器也将顺利编译通过,并不会因为这种违反异常的情况而报错(不排除个别编译器会对这种用法提出警告);
所以,应当禁止在析构函数中抛出异常或者调用可能抛出异常的其他函数。
问题总结
通过对C++11 posix线程取消点和取消方式,以及C++11 异常处理机制的了解,可以得出上述问题的具体原因了。
- 程序终止的直接原因是在析构函数中抛出了异常,而C++11 的析构函数默认是noexcept(true)的,所以触发了std::terminate()终止了程序;
- 程序终止的根本原因是C++11 posix线程取消方式区别于C,采用特殊异常来实现取消,析构函数内部存在线程取消点;
扩展
还有一种比较隐蔽的情况,结合了局部对象栈展开的场景。
栈展开
在运行时期间从函数调用栈中删除函数实体,称为栈展开。栈展开通常用于异常处理。
在C++中,如果一个异常发生了,会线性的搜索函数调用栈,来寻找异常处理者,并且带有异常处理的函数之前的所有实体,都会从函数调用栈中删除。
所以,如果异常没有在抛出它的函数中被处理,则会激活栈展开。
考虑在上述示例的代码中额外封装一层,如下:
class NetMgr{
public:
NetMgr();
virtual ~NetMgr();
static void* threadFunc(void* args);
void StopThread();
private:
pthread_t tid;
};
NetMgr::NetMgr(){
pthread_create(&tid, NULL, threadFunc, this);
}
NetMgr::~NetMgr(){
StopThread();
}
void* NetMgr::threadFunc(void* args){
NetNodes nodes;
while(1){
sleep(5);
}
}
void NetMgr::StopThread(){
pthread_cancel(tid);
pthread_join(tid, NULL);
}
同时main函数修改如下:
int main(){
NetMgr netMgr;
netMgr.StopThread();
sleep(5);
}
现在的线程取消点变了,变为sleep函数,当执行到sleep函数的时候,触发线程取消。但在此之前,并没有显示地销毁对象,但程序依旧被终止,依旧是之前的堆栈。这正是因为栈展开的原因。线程在执行取消动作时,会线性地搜索堆栈,对已经初始化的局部对象实体进行析构回收资源,NetNodes对象会被析构,会调用StopThread(),然后又会回到最初的问题。
异常未重新抛出
再来看一个例子:
class Thread{
public:
Thread();
virtual ~Thread();
static void* threadFunc(void* args);
void stop();
private:
pthread_t tid;
static pthread_mutex_t m;
static pthread_cond_t c;
};
pthread_mutex_t Thread::m = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t Thread::c = PTHREAD_COND_INITIALIZER;
Thread::Thread(){
pthread_create(&tid, NULL, threadFunc, this);
}
Thread::~Thread(){
stop();
}
void Thread::stop(){
pthread_cancel(tid);
pthread_join(tid, NULL);
}
void* Thread::threadFunc(void* args){
try{
pthread_mutex_lock(&m);
pthread_cond_wait(&c, &m);
} catch(...){
// do something
}
}
int main(){
Thread t;
t.stop();
sleep(5);
}
程序运行后的结果依旧是Aborted (core dumped)
这是因为catch(...)捕获了所有异常,而pthread_cancel引起的特殊异常是无法捕获的,解决办法是重新抛出异常:
void* Thread::threadFunc(void* args){
try{
pthread_mutex_lock(&m);
pthread_cond_wait(&c, &m);
} catch(...){
// do something
throw;
}
}
但是上面的代码可能违背了最初的语义,更好的写法如下:
#include <cxxabi.h>
void* Thread::threadFunc(void* args){
try{
pthread_mutex_lock(&m);
pthread_cond_wait(&c, &m);
} catch (abi::__forced_unwind&){
throw;
} catch(...){
// do something
}
}
总结
我们可以得出,posix的线程取消与C++ 结合得并不好(异常未重新抛出导致Abort,在C++03中亦存在),存在很多问题。当存在以下场景时,会导致进程Abort:
- 析构函数中存在线程取消点,取消发生在析构函数内;
- 取消发生引起栈展开,释放了临时对象,触发临时对象的析构,临时对象的析构内又有另外的线程取消动作,结合了第一点的场景;
- 取消发生在noexcept(true)的函数内;
- 线程处理函数内捕获了异常而未抛出;
同时,可以尝试,除了pthread_cancel, pthread_exit也存在上述的问题。
针对上面几种,可以用以下几种方式规避问题:
- 避免在析构函数中调用带取消点性质的函数,避免在析构函数中调用可能抛出异常的函数。取消线程可以另外封装noexcept(false)的函数,负责资源的清理和调用pthread_cancel()、pthread_join(),由开发人员显示调用stop类函数而不依赖析构行为。线程处理函数内避免使用带取消点的noexcept(true)函数;——此种方式不推荐,解决不彻底,难管控避免;
- 避免在线程处理函数内调用可能捕获所有异常的函数,必须保证abi::__forced_unwind异常能重新被抛出;——此种方式也是难管控避免;
- 禁用pthread_cancel()等取消函数,改用互斥量+条件变量的方式,通过条件变量信号通知代取消点。可以设置线程退出标志位,在要退出线程时,设置标志位并使用pthread_cond_signal()或pthread_cond_broadcast()通知;——推荐此种方式
class Thread{
public:
Thread();
virtual ~Thread();
static void* threadFunc(void* args);
void stop();
private:
pthread_t tid;
static bool bExit;
static pthread_mutex_t m;
static pthread_cond_t c;
};
pthread_mutex_t Thread::m = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t Thread::c = PTHREAD_COND_INITIALIZER;
bool Thread::bExit = true;
Thread::Thread(){
bExit = false;
pthread_create(&tid, NULL, threadFunc, this);
}
Thread::~Thread(){
stop();
}
void Thread::stop(){
// 这里m并不是专门用来锁bExit标志的,
// 而是进行业务的同步控制,这里只是借用
pthread_mutex_lock(&m);
bExit = true;
pthread_cond_signal(&c);
pthread_mutex_unlock(&m);
pthread_join(tid, NULL);
}
void* Thread::threadFunc(void* args){
while(!bExit){
// 这里的阻塞返回,有可能是业务的条件到达,也可能是通知线程退出
pthread_mutex_lock(&m);
pthread_cond_wait(&c, &m);
if(bExit) break;
pthread_mutex_unlock(&m);
// do something...
}
pthread_mutex_unlock(&m);
}
参考资料
1、https://udrepper.livejournal.com/21541.html
2、https://gcc.gnu.org/legacy-ml/gcc-help/2015-08/msg00040.html
本文来自博客园,作者:流翎,转载请注明原文链接:https://www.cnblogs.com/hjx168/p/16911895.html