首页 > 编程语言 >pthread_cancel在C++中使用的坑

pthread_cancel在C++中使用的坑

时间:2022-11-21 16:55:58浏览次数:60  
标签:函数 Thread void C++ 线程 pthread cancel

问题现象

在项目中,某些情况下需要动态地创建和销毁线程。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

标签:函数,Thread,void,C++,线程,pthread,cancel
From: https://www.cnblogs.com/hjx168/p/16911895.html

相关文章

  • 使用cmake编译c++源代码
    构建项目的背景:现在的主流都是编写一个cmakelist.txt,通过cmake去构建一个makefile,再make这个makefile生成可执行文件或者动态库静态库。 法1:1.新建一个CMakeLists.tx......
  • C++中的Struct和Class异同
    C++中为了和语言兼容,保留了C语言中的struct关键字,并且进行了适当扩充.C语言=>struct只是包含成员变量,但不包括成员函数C++中=>struct和class非常类似,既可以包括成员......
  • VS 2022创建ATL组件 (C++)
    步骤如下: 1、新建ATL项目 打开VisualStudio2022新建ATL项目2、添加接口类、实现接口方法.  添加一个新的ATL对象。右键MyComTest项目→添加→新建项→ATL→......
  • C++多线程
    c++多线程多线程其实非常简单多线程是多任务处理的一种特殊形式,多任务处理允许让电脑同时运行两个或两个以上的程序。一般情况下,两种类型的多任务处理:基于进程和基于线程......
  • [排序算法] 基数排序 (C++)
    基数排序解释基数排序基数排序RadixSort是一种非基于比较的排序算法。在基数排序中,和计数排序、桶排序的思想类似,我们要再次用到桶这个东西。......
  • C++初阶(vector容器+模拟实现)
    迭代器四种迭代器容器类名::iterator迭代器名;//正向迭代器容器类名::const_iterator迭代器名;//常量正向迭代器,const修饰,只能用于读取容器内的元素,不能改变其值容......
  • 用C/C++开发工业软件适合吗?
    用C/C++开发工业软件最适合的了,这是因为C/C++是仅次于汇编语言的最底层程序开发语言;同时工业软件最大的特征就是专业性强、复杂度高,需要相当深的专业知识、经验、科研基础,并......
  • C++ using 编译指令与名称冲突
    using编译指令:它由名称空间名和它前面的关键字usingnamespace组成,它使名称空间中的所有名称都可用,而不需要使用作用域解析运算符。在全局声明区域中使用using编译指......
  • effectiveC++1、2
    条款01视C++为一个语言联邦​ 在学习c++高效编程时会出现“所有的适当用法都有例外”的情况,解决的方法是:不把c++当作一门语言,而是将其视为以下四门主要次语言组成的联邦......
  • [排序算法] 桶排序 (C++)
    桶排序解释桶排序思想桶排序是一种空间换取时间的排序方式,是非基于比较的。桶排序顾名思义,就是构建多个映射数据的桶,将数据放入桶内,对每个桶内元素进行单独排序。假设......