代码干货 | 一个Reentrant Error引发的对Python信号机制的探索和思考

探讨使用 Celery 作为异步任务队列时遇到的一个关于热关闭的问题,该问题表现为随机抛出 RuntimeError 异常。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

本文来源于阿里云-云栖社区,原文点击这里


前几天工作时遇到了一个匪夷所思的问题。经过几次尝试后问题得以解决,但问题产生的原因却仍令人费解。查找 SO 无果,我决定翻看 Python 的源码。断断续续地研究了几天,终于恍然大悟。撰此文以记。

本文环境:

  • Ubuntu 16.04 (64 bit)
  • Python 3.6.2

使用的 C 源码可以从 Python 官网 获取。

起因

工作时用到了 celery 作为异步任务队列,为方便调试,我写了一个脚本用以启动/关闭 celery 主进程。代码简化后如下:

 
  1. import sys 
  2.  
  3. import subprocess 
  4.  
  5. # ... 
  6.  
  7. celery_process = subprocess.Popen( 
  8.  
  9.     ['celery''-A''XXX''worker'], 
  10.  
  11.     stdout=subprocess.PIPE, 
  12.  
  13.     stderr=sys.stderr 
  14.  
  15.  
  16. try: 
  17.  
  18.     # Start and wait for server process 
  19.  
  20. except KeyboardInterrupt: 
  21.  
  22.     # Ctrl + C pressed 
  23.  
  24.     celery_process.terminate() 
  25.  
  26.     celery_process.wait()  


代码启动了 celery worker,并尝试在捕获到 KeyboardInterrupt 异常时将其热关闭。

初看上去没什么问题。然而实际测试时却发生了十分诡异的事情:按下 Ctrl+C 后,程序 偶尔 会抛出这样的异常:RuntimeError: reentrant call inside <_io.BufferedWriter name='<stdout>’>。诡异之处有两点:

异常发生的时机有随机性

异常的 traceback 指向 celery 包,也就是说这是在 celery 主进程内部发生的异常

这个结果大大出乎了我的意料。随机性异常是众多最难缠的问题之一,因为这常常意味着并发问题,涉及底层知识,病灶隐蔽,调试难度大,同时没有有效的手段判断问题是否彻底解决(可能只是降低了频率)。


 展开全文

在C语言中,`PFRING_RING_REENTRANT`通常是指一个线程安全的环形缓冲区结构,它允许在并发环境中安全地读写数据,特别是在网络通信或驱动程序编程中常见。`ringbuffer`或`ping-pong buffer`是一个经典的队列实现,其中数据在两个固定大小的内存区域之间来回移动。 下面是一个简单的`PFRING_RING_REENTRANT`的概念实现使用示例: ```c #include <stdio.h> #include <stdlib.h> #include <pthread.h> #include <unistd.h> typedef struct { void* data; // 存储数据的指针 size_t capacity; // 缓冲区总容量 size_t head; // 当前头部位置 size_t tail; // 当前尾部位置 } RingBuffer; RingBuffer* create_ringbuffer(size_t capacity) { RingBuffer* ring = malloc(sizeof(RingBuffer)); if (ring == NULL) { perror("Memory allocation failed"); return NULL; } ring->data = malloc(capacity * sizeof(void*)); ring->capacity = capacity; ring->head = ring->tail = 0; return ring; } void* write_to_ringbuffer(RingBuffer* rb, void* data) { pthread_mutex_lock(&rb->mutex); // 获取锁 while (rb->tail == rb->head) { // 环满,等待 if (rb->tail == rb->capacity - 1 && rb->head == 0) break; // 如果循环结束,说明只有一个元素,可以插入 else usleep(10000); // 等待一段时间 } rb->data[rb->tail] = data; // 插入数据 rb->tail = (rb->tail + 1) % rb->capacity; // 更新尾部 pthread_mutex_unlock(&rb->mutex); // 释放锁 return NULL; } void* read_from_ringbuffer(RingBuffer* rb) { pthread_mutex_lock(&rb->mutex); while (rb->head == rb->tail) { // 环为空,等待 usleep(10000); // 等待一段时间 } void* data = rb->data[rb->head]; // 读取数据 rb->head = (rb->head + 1) % rb->capacity; // 更新头部 pthread_mutex_unlock(&rb->mutex); free(data); // 在这里假设数据不会长时间持有,应及时释放 return NULL; } int main() { RingBuffer* ringbuf = create_ringbuffer(5); if (!ringbuf) { printf("Failed to create ringbuffer\n"); return 1; } pthread_t writer, reader; if (pthread_create(&writer, NULL, write_to_ringbuffer, &ringbuf) != 0 || pthread_create(&reader, NULL, read_from_ringbuffer, &ringbuf) != 0) { perror("Thread creation failed"); return 1; } // 主线程结束时关闭环形缓冲区 pthread_join(writer, NULL); pthread_join(reader, NULL); free(ringbuf->data); free(ringbuf); return 0; } ``` 在这个例子中,我们创建了一个环形缓冲区并使用了互斥锁(`pthread_mutex`)保证在多线程环境下的操作安全。`write_to_ringbuffer`函数用于写入数据,而`read_from_ringbuffer`函数用于读取数据。在`main`函数中,主线程创建了两个子线程分别负责写入读出数据。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值