Package: atomics
atomicsThis library implements a wrapper around the lower levelpatomicC library (which is provided as
part of this library through thebuild_patomiccommand insetup.py).It exposes hardware level lock-free (and address-free) atomic operations on a
memory buffer, either internally allocated or externally provided, via a set of
atomic classes.The operations in these classes are both thread-safe and process-safe,
meaning that they can be used on a shared memory buffer for interprocess
communication (including with other languages such as C/C++).Table of ContentsInstallingExamplesIncorrectMulti-ThreadingMulti-ProcessingDocsTypesConstructionLifetimeContractAlignmentPropertiesOperationsSpecial MethodsMemory OrderExceptionsBuildingFuture ThoughtsContributingInstallingLinux/MacOS:$python3-mpipinstallatomicsWindows:$py-mpipinstallatomicsThis library requires Python3.6+, and has a dependency on thecffilibrary.
While the code here has no dependency on any implementation specific features,
thecffilibrary functions used are likely to not work outside of CPython and
PyPy.Binaries are provided for the following platforms:Windows[x86, amd64]MacOSX[x86_64, universal2]Linux[i686, x86_64, aarch64, ppc64le, s390x][manylinux2014, musllinux_1_1]Linux[i686, x86_64][manylinux1]If you are on one of these platforms andpiptries to build from source or
fails to install, make sure that you have the latest version ofpipinstalled.
This can be done like so:Linux/MacOS:$python3-mpipinstall--upgradepipWindows:$py-mpipinstall--upgradepipIf you need to build from source, check out theBuildingsection
as there are additional requirements for that.ExamplesIncorrectThe following example has a data race (ais modified from multiple threads).
The program is not correct, anda's value will not equaltotalat the end.fromthreadingimportThreada=0deffn(n:int)->None:globalafor_inrange(n):a+=1if__name__=="__main__":# setuptotal=10_000_000# run threads to completiont1=Thread(target=fn,args=(total//2,))t2=Thread(target=fn,args=(total//2,))t1.start(),t2.start()t1.join(),t2.join()# print resultsprint(f"a[{a}] != total[{total}]")Multi-ThreadingThis example implements the previous example butais now anAtomicIntwhich
can be safely modified from multiple threads (as opposed tointwhich can't).
The program is correct, andawill equaltotalat the end.importatomicsfromthreadingimportThreaddeffn(ai:atomics.INTEGRAL,n:int)->None:for_inrange(n):ai.inc()if__name__=="__main__":# setupa=atomics.atomic(width=4,atype=atomics.INT)total=10_000# run threads to completiont1=Thread(target=fn,args=(a,total//2))t2=Thread(target=fn,args=(a,total//2))t1.start(),t2.start()t1.join(),t2.join()# print resultsprint(f"a[{a.load()}] == total[{total}]")Multi-ProcessingThis example is the counterpart to the above correct code, but using processes
to demonstrate that atomic operations are also safe across processes. This
program is also correct, andawill equaltotalat the end. It is also how
one might communicate with processes written in other languages such as C/C++.importatomicsfrommultiprocessingimportProcess,shared_memorydeffn(shmem_name:str,width:int,n:int)->None:shmem=shared_memory.SharedMemory(name=shmem_name)buf=shmem.buf[:width]withatomics.atomicview(buffer=buf,atype=atomics.INT)asa:for_inrange(n):a.inc()delbufshmem.close()if__name__=="__main__":# setupwidth=4shmem=shared_memory.SharedMemory(create=True,size=width)buf=shmem.buf[:width]total=10_000# run processes to completionp1=Process(target=fn,args=(shmem.name,width,total//2))p2=Process(target=fn,args=(shmem.name,width,total//2))p1.start(),p2.start()p1.join(),p2.join()# print results and cleanupwithatomics.atomicview(buffer=buf,atype=atomics.INT)asa:print(f"a[{a.load()}] == total[{total}]")delbufshmem.close()shmem.unlink()NOTE:Althoughshared_memoryis showcased here,atomicviewaccepts any
type that supports the buffer protocol as its buffer argument, so other sources
of shared memory such asmmapcould be used instead.DocsTypesThe following helper (abstract-ish base) types are available inatomics:[ANY,INTEGRAL,BYTES,INT,UINT]This library provides the followingAtomicclasses inatomics.base:Atomic --- ANYAtomicIntegral --- INTEGRALAtomicBytes --- BYTESAtomicInt --- INTAtomicUint --- UINTTheseAtomicclasses are constructable on their own, but it is strongly
suggested using theatomic()function to construct them. Each class
corresponds to one of the above helper types (as indicated).This library also providesAtomic*View(inatomics.view) andAtomic*ViewContext(inatomics.ctx) counterparts to theAtomic*classes,
corresponding to the same helper types.The latter of the two sets of classes can be constructed manually, although it
is strongly suggested using theatomicview()function to construct them. The
former set of classes cannot be constructed manually with the available types,
and should only be obtained by called.__enter__()on a correspondingAtomic*ViewContextobject.Even though you should never need to directly use these classes (apart from the
helper types), they are provided to be used in type hinting. The inheritance
hierarchies are detailed in theARCHITECTURE.mdfile
(available on GitHub).ConstructionThis library provides the functionsatomicandatomicview, along with the
typesBYTES,INT, andUINT(as well asANYandINTEGRAL) to construct
atomic objects like so:importatomicsa=atomics.atomic(width=4,atype=atomics.INT)print(a)# AtomicInt(value=0, width=4, readonly=False, signed=True)buf=bytearray(2)withatomics.atomicview(buffer=buf,atype=atomics.BYTES)asa:print(a)# AtomicBytesView(value=b'\x00\x00', width=2, readonly=True)You should only need to construct objects with anatypeofBYTES,INT, orUINT. Using anatypeofANYorINTGERALwill require additional kwargs,
and anatypeofANYwill result in an object that doesn't actually expose
any atomic operations (only properties, explained in sections further on).Theatomic()function returns a correspondingAtomic*object.Theatomicview()function returns a correspondingAtomic*ViewContextobject.
You can use this context object in awithstatement to obtain anAtomic*Viewobject. Thebufferparameter may be any object that supports the buffer
protocol.Construction can raiseUnsupportedWidthExceptionandAlignmentError.NOTE:thewidthproperty ofAtomic*Viewobjects is derived from the
buffer's length as if it were contiguous. It is equivalent to callingmemoryview(buf).nbytes.LifetimeObjects ofAtomic*classes (i.e. objects returned by theatomic()function)
have a self-contained buffer which is automatically freed. They can be passed
around and stored liked regular variables, and there is nothing special about
their lifetime.Objects ofAtomic*ViewContextclasses (i.e. objects returned by theatomicview()function) andAtomic*Viewobjects obtained from said objects
have a much stricter usage contract.ContractThe buffer used to construct anAtomic*ViewContextobject (either directly or
throughatomicview())MUST NOTbe invalidated until.release()is
called. This is aided by the fact that.release()is called automatically
in.__exit__(...)and.__del__(). As long as you immediately use the context
object in awithstatement, andDO NOTinvalidate the buffer inside thatwithscope, you will always be safe.The protections implemented are shown in this example:importatomicsbuf=bytearray(4)ctx=atomics.atomicview(buffer=buf,atype=atomics.INT)# ctx.release() here will cause ctx.__enter__() to raise:# ValueError("Cannot open context after calling 'release'.")withctxasa:# this calls ctx.__enter__()# ctx.release() here will raise:# ValueError("Cannot call 'release' while context is open.")# ctx.__enter__() here will raise:# ValueError("Cannot open context multiple times.")print(a.load())# ok# ctx.__exit__(...) now called# we can safely invalidate object 'buf' now# ctx.__enter__() will raise:# ValueError("Cannot open context after calling 'release'.")# accessing object 'a' in any way will also raise an exceptionFurthermore, in CPython, all built-in types supporting the buffer protocol will
throw aBufferErrorexception if you try to invalidate them while they're in
use (i.e. before calling.release()).As a last resort, if you absolutely must invalidate the buffer inside thewithcontext (where you can't call.release()), you may call.__exit__(...)manually on theAtomic*ViewContextobject. This is to force explicitness
about something considered to be bad practice and dangerous.Where it's allowed,.release()may be called multiple times with no
ill-effects. This also applies to.__exit__(...), which has no restrictions
on where it can be called.AlignmentDifferent platforms may each have their own alignment requirements for atomic
operations of given widths. This library provides theAlignmentclass inatomicsto ensure that a given buffer meets these requirements.fromatomicsimportAlignmentbuf=bytearray(8)align=Alignment(len(buf))assertalign.is_valid(buf)If an atomic class is constructed from a misaligned buffer, the constructor will
raiseAlignmentError.By default,.is_validcalls.is_valid_recommended. The classAlignmentalso exposes.is_valid_minimum. Currently, no atomic class makes use of the
minimum alignment, so checking for it is pointless. Support for it will be
added in a future release.PropertiesAllAtomic*andAtomic*Viewclasses have the following properties:width: width in bytes of the underlying buffer (as if it were contiguous)readonly: whether the object supports modifying operationsops_supported: a sorted list ofOpTypeenum values representing which
operations are supported on the objectIntegralAtomic*andAtomic*Viewclasses also have the following property:signed: whether arithmetic operations are signed or unsignedIn both cases, the behaviour on overflow is defined to wraparound.OperationsBaseAtomicandAtomicViewobjects (corresponding toANY) expose no atomic
operations.AtomicBytesandAtomicBytesViewobjects support the following operations:[base]:load,store[xchg]:exchange,cmpxchg_weak,cmpxchg_strong[bitwise]:bit_test,bit_compl,bit_set,bit_reset[binary]:bin_or,bin_xor,bin_and,bin_not[binary]:bin_fetch_or,bin_fetch_xor,bin_fetch_and,bin_fetch_notIntegralAtomic*andAtomic*Viewclasses additionally support the following
operations:[arithmetic]:add,sub,inc,dec,neg[arithmetic]:fetch_add,fetch_sub,fetch_inc,fetch_dec,fetch_negThe usage of (most of) these functions is modelled directly on the C++11std::atomicimplementation foundhere.Compare Exchange (cmpxchg_*)Thecmpxchg_*functions returnCmpxchgResult. This has the attributes.success: boolwhich indicates whether the exchange took place, and.expected: Twhich holds the original value of the atomic object.Thecmpxchg_weakfunction may fail spuriously, even ifexpectedmatches
the actual value. It should be used as shown below:importatomicsdefatomic_mul(a:atomics.INTEGRAL,operand:int):res=atomics.CmpxchgResult(success=False,expected=a.load())whilenotres:desired=res.expected*operandres=a.cmpxchg_weak(expected=res.expected,desired=desired)In a real implementation ofatomic_mul, care should be taken to ensure thatdesiredfits ina(i.e.desired.bit_length() < (a.width * 8), assuming 8
bits in a byte).ExceptionsAll operations can raiseUnsupportedOperationException(so check.ops_supportedif you need to be sure).Operationsload,store, andcmpxchg_*can raiseMemoryOrderErrorif
called with an invalid memory order.MemoryOrderenum values expose the
functionsis_valid_store_order(),is_valid_load_order(), andis_valid_fail_order()to check with.Special MethodsAtomicBytesandAtomicBytesViewimplement the__bytes__special method.IntegralAtomic*andAtomic*Viewclasses implement the__int__special
method. They intentionally do not implement__index__.There is a notable lack of any classes implementing special methods
corresponding to atomic operations; this is intentional. Assignment in Python is
not available as a special method, and we do not want to encourage people to
use other special methods with this class, lest it lead to them accidentally
using assignment when they meant.store(...).Memory OrderTheMemoryOrderenum class is provided inatomics, and the memory orders
are directly copied from C++11'sstd::memory_orderdocumentation foundhere, except forCONSUME(which would be pointless to expose in this library).All operations have a default memory order,SEQ_CST. This will enforce
sequential consistency, and essentially make your multi-threaded and/or
multi-processed program be as correct as if it were to run in a single thread.IF YOU DO NOT UNDERSTAND THE LINKED DOCUMENTATION, DO NOT USE YOUR OWN
MEMORY ORDERS!!!Stick with the defaults to be safe. (And realistically, this is Python, you
won't get a noticeable performance boost from using a more lax memory order).The following helper functions are provided:.is_valid_store_order()(forstoreop).is_valid_load_order()( forloadop).is_valid_fail_order()(for thefailordering incmpxchg_*ops)Passing an invalid memory order to one of these ops will raiseMemoryOrderError.ExceptionsThe following exceptions are available inatomics.exc:AlignmentErrorMemoryOrderErrorUnsupportedWidthExceptionUnsupportedOperationExceptionBuildingIMPORTANT:Make sure you have the latest version ofpipinstalled.Usingsetup.py'sbuildorbdist_wheelcommands will run thebuild_patomiccommand (which you can also run directly).This clones thepatomiclibrary into a temporary directory, builds it, and
then copies the shared library intoatomics._clib.This requires thatgitbe installed on your system (a requirement of theGitPythonmodule). You will also need an ANSI/C90 compliant C compiler
(although ideally a more recent compiler should be used).CMakeis also
required but should be automaticallypip install'd if not available.If you absolutely cannot getbuild_patomicto work, go topatomic, follow the instructions on
building it (making sure to build the shared library version), and then
copy-paste the shared library file intoatomics._clibmanually.NOTE:Currently, the library builds a dummy extension in order to tricksetuptoolsinto building a non-purepython wheel. If you are ok with a purepython wheel,
then feel free to remove the code for that fromsetup.py(at the bottom).Otherwise, you will need a C99 compliant C compiler, and probably the
development libraries/headers for whichever version of Python you're using.Future Thoughtsadd docstringsadd testsadd support forminimumalignmentadd support for constructingAtomicclasses' buffers in shared memoryadd support for passingAtomicobjects to sub-processes and sub-interpretersreimplement in C or Cython for performance gains (preliminary benchmarks
put such implementations at 2x the speed of a rawint)ContributingI don't have a guide for contributing yet. This section is here to make the
following two points:new operations must first be implemented inpatomicbefore this library can
be updatednew architectures, widths, and existing unsupported operations must be
supported inpatomic(no change required in this library)