1. 问题描述
在一次代码调试的过程中,遇到过一个问题,线程在调用pthread_cancel时,提示未找到目标线程,然后程序阻塞在了与目标线程相关的条件变量的释放上,造成了死锁的现象。
2. 问题复现
#include <pthread.h>
#include <iostream>
#include <unistd.h>
#include <cstring>#define BUF_SIZE 8192unsigned char g_buf[BUF_SIZE] = {0xff};class Worker {
public:Worker() {m_run = false;pthread_mutex_init(&m_mtx, NULL);pthread_cond_init(&m_cond, NULL);pthread_create(&m_tid, 0, writeThread, (void*)this);std::cout << "create thread " << std::hex << std::showbase << m_tid << std::endl;}~Worker() {pthread_t id = pthread_self();std::cout << "cur thread " << id << std::endl;m_run = false;int ret = pthread_cancel(m_tid);std::cout << "ret:" << ret << std::endl;// pthread_join(m_tid, NULL);// Destroy the signalsstd::cout << "finished 1" << std::endl;pthread_cond_destroy(&m_cond);std::cout << "finished 2" << std::endl;pthread_mutex_destroy(&m_mtx);std::cout << "finished 3" << std::endl;}static void* writeThread(void* obj) {((Worker*)obj)->writeWork();return 0;}void writeWork() {char buf[4096];while(true) {sched_yield();if(!m_run) {memcpy(buf, g_buf, BUF_SIZE);pthread_mutex_lock(&m_mtx);while (!m_run){pthread_cond_wait(&m_cond, &m_mtx);}pthread_mutex_unlock(&m_mtx);// write buf}}
private:bool m_run{false};pthread_cond_t m_cond;pthread_mutex_t m_mtx;pthread_t m_tid;
};int main() {Worker work;// avoid main thread exit before work threadsleep(3);return 0;
}
程序卡死,执行结果如下:
打印堆栈信息如下:
可以看到主线程阻塞在析构函数中销毁条件变量m_cond的位置,而工作线程则阻塞在pthread_cond_wait处等待条件变量的满足。因此看到的现象就如第一张图那样,主线程卡死,无法完成资源释放。
从pthread_cancel的返回值可以看出,工作线程并没有按照预期那样在取消点(pthread_cond_wait)处被成功释放。返回值3意味着没有找到要取消的线程。
表面原因我们找到了,但是根本原因还没有找出来,也就是为什么会造成这种结果,正常情况下pthread_cancel是不会失败的。猜测一种可能的原因:线程的栈空间被其他数据覆盖,线程相关信息找不到了。
3. 问题分析与定位
从上图就可以看出,0x7ffff6e83700是工作线程,该值应该是线程控制信息的地址,打印地址内容发现结果为0,这就有问题了,正常情况下应该是非0值,参考当前线程地址的打印信息就可以对比出。所以对该线程的任何操作(pthread_cancel、pthread_join)都不会正常执行。
知道了原因,那就来找找是什么地方导致了这种结果呢?猜测是不是赋值操作时,数组越界了,并且超出了足够大的范围。那么什么操作会有这种结果呢:
- 循环赋值,且循环次数足够大;
- 通过memcpy、memset、memmove进行赋值,且size足够大。
根据以上猜测,我们再来看看代码,发现在调用memcpy时,局部变量buf的大小只有4096,而传入的size值确是8192,远远超出了buf的大小。将size的大小改为5000后,再次执行程序。
可以看到程序正常结束,但是5000依然超出了实际内存大小,这说明要复现上述问题,需要的size值得足够大才行。
实际上通过检测工具可以更方便的check数组越界的情况,此处使用gcc自带的工具sanitizers,只需要在编译选项中增加-g -fsanitize=address
选项即可,增加调试信息可以更容易定位代码位置。
可以看到该工具定位到可能越界位置在45行代码,也就是调用memcpy的位置。
4. 总结
- pthread_cancel并不一定保证线程被释放,它只是给目标线程发送了一个信号,而只有当目标线程到达一个取消点(系统调用)时,目标线程才会退出。
- 如果需要等待线程退出,应该调用pthread_join来保证这一点。上述代码中,如果在cancel之后调用该函数,程序会出现段错误,因为在使用线程相关的信息时,拿到的是一个空值。
- 在对字符数组进行赋值时,c语言提供的一些函数并不安全,很多时候越界却不自知。所以我们需要有一些检测工具来帮助我们避免这些情况的发生,常用的工具如sanitizers、valgrind等。