这个Python生产者-消费者无锁方法线程安全么?

6 投票
6 回答
4013 浏览
提问于 2025-04-15 11:33

我最近写了一个程序,使用了一个简单的生产者/消费者模式。最开始的时候,它有一个关于不当使用线程锁(threading.Lock)的错误,我最终修复了这个问题。但这让我思考,是否可以在不使用锁的情况下实现生产者/消费者模式。

在我的情况下,需求很简单:

  • 一个生产者线程。
  • 一个消费者线程。
  • 队列只能放一个项目。
  • 生产者可以在当前项目被消费之前生产下一个项目。因此,当前项目会被丢失,但这对我来说没问题。
  • 消费者可以在下一个项目被生产之前消费当前项目。因此,当前项目可能会被消费两次(或更多次),但这对我来说也没问题。

所以我写了这个:

QUEUE_ITEM = None

# this is executed in one threading.Thread object
def producer():
    global QUEUE_ITEM
    while True:
        i = produce_item()
        QUEUE_ITEM = i

# this is executed in another threading.Thread object
def consumer():
    global QUEUE_ITEM
    while True:
        i = QUEUE_ITEM
        consume_item(i)

我的问题是:这段代码是线程安全的吗?

立即的评论是:这段代码其实并不是完全无锁的——我使用的是CPython,它有全局解释器锁(GIL)。

我测试了一下这段代码,似乎它能正常工作。它会转化为一些加载和存储操作,这些操作因为有GIL的存在是原子的(即不会被打断)。但我也知道,当x实现了__del__方法时,del x操作并不是原子的。所以如果我的项目有一个__del__方法,并且发生了一些不好的调度,事情可能会出错。或者不会?

另一个问题是:为了让上面的代码正常工作,我需要对生产的项目类型施加什么样的限制?

我的问题只是关于理论上是否可以利用CPython和GIL的一些特性,来实现一个无锁的解决方案(也就是说,代码中不显式使用像threading.Lock这样的锁)。

6 个回答

1

这其实并不是真正安全的线程操作,因为生产者可能会在消费者还没处理完QUEUE_ITEM的时候就把它覆盖掉,而消费者可能会把QUEUE_ITEM消费两次。正如你提到的,你对此没问题,但大多数人可不这样认为。

对于更深入的理论问题,还是需要对cpython内部结构了解更多的人来回答你。

6

耍花招是会让你受伤的。直接用队列来在线程之间进行沟通就好了。

2

是的,这样做会按照你描述的方式工作:

  1. 生产者可以生成一个可以跳过的元素。
  2. 消费者可以消费同样的元素。

但我也知道,当某个对象有del方法时,执行删除操作并不是原子性的。这意味着如果我的项目有一个del方法,而在某些不好的调度情况下,事情可能会出错。

我在这里没有看到“del”。如果在consume_item中发生了删除操作,那么del可能会在生产者线程中发生。我认为这不会是一个“问题”。

不过,别费心去使用这个。你最终会浪费CPU在无意义的轮询循环上,而且这并不比使用带锁的队列快,因为Python本身就有一个全局锁。

撰写回答