and
and

Reputation: 2092

Python threads and atomic operations

I want to implement a thread with a synchronous stop() method.

I've seen versions like this:

class Thread1:
    def __init__(self):
        self._stop_event = threading.Event()
        self._thread = None

    def start(self):
        self._thread = threading.Thread(target=self._run)
        self._thread.start()

    def stop(self):
        self._stop_event.set()
        self._thread.join()

    def _run(self):
        while not self._stop_event.is_set():
            self._work()

    def _work(self):
        print("working")

But I've read that atomic operations are thread safe and it seems to me that it can be done without Event. So I came up with this:

class Thread2:
    def __init__(self):
        self._working = False
        self._thread = None

    def start(self):
        self._working = True
        self._thread = threading.Thread(target=self._run)
        self._thread.start()

    def stop(self):
        self._working = False
        self._thread.join()

    def _run(self):
        while self._working:
            self._work()

    def _work(self):
        print("working")

It think that similar implementation would be considered incorrect in C, because compiler can put _working to a register (or even optimize out) and the working thread would never know that the variable has changed. Can something like that happen in Python? Is this implementation correct? I don't aim to avoid events or locks altogether, just want to understand this atomic operations thing.

Upvotes: 8

Views: 8917

Answers (2)

Dima Tisnek
Dima Tisnek

Reputation: 11781

Here's a more comprehensive solution, which can also be used if the worker thread needs to delay sometimes.

class Worker(threading.Thread):
    quit = False

    def __init__(self, ...):
        super().__init__()
        self.cond = threading.Condition()
        ...

    def delay(self, seconds):
        deadline = time.monotonic() + seconds
        with self.cond:
            if self.quit:
                raise SystemExit()
            if time.monotinic() >= deadline:
                return
            self.cond.wait(time.monotonic() - deadline)

    def run(self):
        while not self.quit:
            # work here
            ...

            # when delay is needed
            self.delay(123)

    def terminate(self):
        with self.cond:
            self.quit = True
            self.cond.notify_all()
        self.join()

And used like this:

worker = Worker()
worker.start()
...
# finally
worker.terminate()

Of course, if you know for a fact that worker never sleeps, you can remove creation and all uses of self.cond, keeping the rest of code.

Upvotes: 1

jb.
jb.

Reputation: 23955

As far as I can tell it is also incorrect in Python, as _working can still be put in register or optimized in some other way, or some other thing may happen that would change it's value. Reads and writes th this field could be arbitrarily reordered by the processor.

Well lets say that in multithreading world you shouln't really ask: Why this shouldn't work, but rather Why this is guarranteed to work.

Having said that in most cases multithreading is a little bit easier in CPython, because of GIL that guarantees that:

  • Only one interpreter command is executed at any given time.
  • Forces often memory synchronization between threads.

Bear in mind that GIL is a implementation detail, that might go away if someone rewrites CPython without it.

Also note that the fact that it should implement it this way in any real system.

Upvotes: 2

Related Questions