记一次Python线程安全问题的排查
应用场景
在某ERP的自动化流程对接业务中,多线程是通过Python Celery框架实现的。Celery框架是分布式任务队列,内置MQ和生产者消费者操作,可实现异步功能,也可实现定时任务,非常便捷。
Celery底层采用Redis记录MQ,基于此原因,可以很方便地实现多机的分布式队列。架构如下图所示(出处:腾讯云)。本业务在两台服务器上,开启了30余worker。
问题出现
然而,某次客户发现订单ID有标注错乱的情况。定位到订单结果ret的获取位置如下(左边是修改之前,又边是修改之后)。
问题排查
经过搜索,发现原来Python中多个线程之间是共享全局变量的。
全局变量在Python中存储在特殊的命名空间中,称为全局命名空间或全局作用域。每个模块都有自己的全局命名空间,可以在整个模块中访问和使用全局变量。
总结
总结一下多线程共享/不共享全局变量的语言。
多线程共享全局变量
- C/C++:在C/C++中,多个线程可以访问和修改全局变量。但是,需要特别注意并发访问全局变量可能导致数据竞争和不确定的行为,因此在多线程编程中必须使用同步机制,如互斥锁(mutex)或原子操作(atomic operations)来确保线程之间的正确同步。
- Java:Java中的多线程可以访问和修改共享的全局变量。然而,与C/C++不同,Java提供了内置的线程同步机制,如synchronized关键字和Lock对象,以确保线程安全性和正确的同步。
- Python:Python的全局变量是默认共享的,多个线程可以访问和修改它们。然而,由于全局解释器锁(Global Interpreter Lock,GIL)的存在,Python的多线程并不能实现真正的并行执行,而是通过在解释器级别进行线程切换来模拟并发。这导致在Python中,多线程并不能充分利用多核处理器的优势。如果涉及到需要真正并行执行的任务,可以考虑使用多进程或其他并发编程库。
多线程不共享全局变量
- Go:Go语言中,每个goroutine(类似于线程)都有自己的栈空间,但它们共享相同的堆空间。在Go中,多个goroutine之间默认不共享全局变量,每个goroutine都有自己的局部变量副本。如果需要在线程之间共享数据,需要使用通信机制,如通道(Channel)。
- Rust:Rust语言通过所有权(ownership)和借用(borrowing)的概念来保证内存安全和线程安全。Rust默认情况下不允许多个线程直接共享全局变量,而是通过所有权和借用规则来确保线程安全。如果需要在线程之间共享数据,可以使用线程安全的数据结构和同步原语。
记一次Python线程安全问题的排查
https://www.catop.top/2023/06/02/a-inspection-of-python-thread-security/