Reputation: 2092
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
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
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:
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