七个 Python 多线程数据同步的坑,尤其最后一个,死锁了都查不出来.
我跟你说,Python多线程这玩意儿,看着简单,用起来全是坑。我踩过无数次,每次都想把电脑砸了。
第一个坑:共享变量说改就改
两个人同时往一个账户存钱,结果只改了一次。你以为是先后执行,实际是同时读到旧值,一起加了一百。代码跑完了,钱少了一半。这种问题,打印日志都看不出错误在哪。
第二个坑:list append也不安全
觉得list是线程安全的?错得离谱。两个线程同时往列表里塞数据,有时候会丢一个。尤其是元素稍微多的时候,丢数据是常态。我排查过整整一天,最后发现是两个线程争同一个列表。
第三个坑:用锁反而更慢
加了Lock,数据是安全了。本来三个线程并行跑,加锁后变成串行,比单线程还慢。有人在循环里面放锁,那就更惨了。每次加锁解锁都有开销,循环多了,性能直接崩掉。
第四个坑:锁的上下文管理也会出错
用with lock看起来很安全,可要是有人在锁里面调了别的函数,那个函数又去拿同一个锁。那就死锁了。程序卡死,什么日志都不打印,像睡着了一样。
第五个坑:全局解释器锁是个假救星
很多人以为GIL能保证数据安全,实际它只保证一行字节码不被多个线程同时执行。一行Python代码翻译成好多条字节码,读变量和写变量之间随时可能切换线程。这个误解坑了太多新手。
第六个坑:先进先出队列也靠不住
queue.Queue是线程安全的,可有人不用它。自己搞了个列表当队列,加锁又加得不对。数据到了队列里,读取的时候线程没退出,等着等着就卡死了。队列满了没人拿,生产者一直等,消费者还在睡大觉。
第七个坑:死锁了都查不出来
这个最要命。两个锁互相等着对方释放,线程停在那里。CPU占用率是正常的,内存正常,什么异常都不报。你用pdb调试,一进去就卡住。杀进程重启,问题复现不了。生产环境偶尔卡一次,三天后用户投诉才注意到。没有日志,没有堆栈,只能靠猜。后来我用threading模块的threading.enumerate()打印所有线程状态,发现一个线程卡在lock.acquire上。这才找到问题出在哪。
说句实在话,多线程数据同步没有完美的方案。能不用锁就不用锁,能用队列就用队列。实在要用锁,锁的粒度要小,拿到锁尽快释放。别忘了给每个线程打标记,方便排查。
写这段的时候,我正盯着一个卡住的进程。这次是哪个坑?我还在查。