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() 的动作。

所以,大致的流程是这样的:

  1. 主线程试图停止 nodes 的处理线程,调用了 pthread_cancel();
  2. nodes 的处理线程在运行至取消点时才会退出,而刚好运行到 DestoryNetNode(),删除TcpServerTcpServer 的析构函数内部刚好有取消点 pthread_join()
  3. pthread_join() 内部触发了 Unwind_ForceUnwind() ,抛出了异常;
  4. 异常发生在 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

posted @ 2022-11-21 16:47  流翎  阅读(1085)  评论(0编辑  收藏  举报