Created
August 3, 2016 13:12
-
-
Save benhoyt/8c8a8d62debe8e5aa5340373f9c509c7 to your computer and use it in GitHub Desktop.
An atomic, thread-safe incrementing counter for Python
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
"""An atomic, thread-safe incrementing counter.""" | |
import threading | |
class AtomicCounter: | |
"""An atomic, thread-safe incrementing counter. | |
>>> counter = AtomicCounter() | |
>>> counter.increment() | |
1 | |
>>> counter.increment(4) | |
5 | |
>>> counter = AtomicCounter(42.5) | |
>>> counter.value | |
42.5 | |
>>> counter.increment(0.5) | |
43.0 | |
>>> counter = AtomicCounter() | |
>>> def incrementor(): | |
... for i in range(100000): | |
... counter.increment() | |
>>> threads = [] | |
>>> for i in range(4): | |
... thread = threading.Thread(target=incrementor) | |
... thread.start() | |
... threads.append(thread) | |
>>> for thread in threads: | |
... thread.join() | |
>>> counter.value | |
400000 | |
""" | |
def __init__(self, initial=0): | |
"""Initialize a new atomic counter to given initial value (default 0).""" | |
self.value = initial | |
self._lock = threading.Lock() | |
def increment(self, num=1): | |
"""Atomically increment the counter by num (default 1) and return the | |
new value. | |
""" | |
with self._lock: | |
self.value += num | |
return self.value | |
if __name__ == '__main__': | |
import doctest | |
doctest.testmod() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
The lockless approach does assume a
count()
implementation that is thread-safe. In CPython, the entireitertools
module is written in C for maximum performance. So a call toitertools.count()
is a single python expression, and thus atomic under the GIL. This isn't guaranteed by any standard, but it is a known implementation detail.In other flavors of python, YMMV.
If you're writing a portable library, stick with the explicit locking in benhoyt's version.
But if you're writing code only for yourself or your company, and you control the runtime, then leaning on the GIL to avoid lock contention can be useful.
Also note that the lockless
Counter.count()
is not atomic. An extrainc()
ordec()
can sneak in between the twonext()
calls. SoCounter.count()
isn't guaranteed to be strictly monotonically increasing when there are many parallel writes happening. But, no information is ever lost, so the count will always settle on the correct value, when the system is "at rest" (ie: no new writes). And in a high-throughput multithreaded use-case, an occasional +/-1 shouldn't be a problem.11 The count will be out-of-date by the time it is read anyway, unless there is locking at a higher level to block writers while the count is being read and used to make some decision. And if there is such locking, then it would mean the system is "at rest" for the read, so the count has settled and is correct for that moment.