Skip to content

Instantly share code, notes, and snippets.

@benhoyt
Created August 3, 2016 13:12
Show Gist options
  • Star 21 You must be signed in to star a gist
  • Fork 3 You must be signed in to fork a gist
  • Save benhoyt/8c8a8d62debe8e5aa5340373f9c509c7 to your computer and use it in GitHub Desktop.
Save benhoyt/8c8a8d62debe8e5aa5340373f9c509c7 to your computer and use it in GitHub Desktop.
An atomic, thread-safe incrementing counter for Python
"""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()
@PhilMarsh
Copy link

PhilMarsh commented Dec 8, 2022

@PhilMarsh You sure this is alright? The docs don't say anything about itertools.count() being thread-safe.

The lockless approach does assume a count() implementation that is thread-safe. In CPython, the entire itertools module is written in C for maximum performance. So a call to itertools.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 extra inc() or dec() can sneak in between the two next() calls. So Counter.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.1


1 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment