Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is this Python producer-consumer lockless approach thread-safe?

I recently wrote a program that used a simple producer/consumer pattern. It initially had a bug related to improper use of threading.Lock that I eventually fixed. But it made me think whether it's possible to implement producer/consumer pattern in a lockless manner.

Requirements in my case were simple:

  • One producer thread.
  • One consumer thread.
  • Queue has place for only one item.
  • Producer can produce next item before the current one is consumed. The current item is therefore lost, but that's OK for me.
  • Consumer can consume current item before the next one is produced. The current item is therefore consumed twice (or more), but that's OK for me.

So I wrote this:

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)

My question is: Is this code thread-safe?

Immediate comment: this code isn't really lockless - I use CPython and it has GIL.

I tested the code a little and it seems to work. It translates to some LOAD and STORE ops which are atomic because of GIL. But I also know that del x operation isn't atomic when x implements __del__ method. So if my item has a __del__ method and some nasty scheduling happens, things may break. Or not?

Another question is: What kind of restrictions (for example on produced items' type) do I have to impose to make the above code work fine?

My questions are only about theoretical possibility to exploit CPython's and GIL's quirks in order to come up with lockless (i.e. no locks like threading.Lock explicitly in code) solution.

like image 775
Jasiu Avatar asked May 12 '09 21:05

Jasiu


3 Answers

Trickery will bite you. Just use Queue to communicate between threads.

like image 61
Dustin Avatar answered Sep 28 '22 01:09

Dustin


Yes this will work in the way that you described:

  1. That the producer may produce a skippable element.
  2. That the consumer may consume the same element.

But I also know that del x operation isn't atomic when x implements del method. So if my item has a del method and some nasty scheduling happens, things may break.

I don't see a "del" here. If a del happens in consume_item then the del may occur in the producer thread. I don't think this would be a "problem".

Don't bother using this though. You will end up using up CPU on pointless polling cycles, and it is not any faster than using a queue with locks since Python already has a global lock.

like image 39
Unknown Avatar answered Sep 28 '22 02:09

Unknown


This is not really thread safe because producer could overwrite QUEUE_ITEM before consumer has consumed it and consumer could consume QUEUE_ITEM twice. As you mentioned, you're OK with that but most people aren't.

Someone with more knowledge of cpython internals will have to answer you more theoretical questions.

like image 32
David Locke Avatar answered Sep 28 '22 01:09

David Locke