%global _empty_manifest_terminate_build 0 Name: python-atomics Version: 1.0.2 Release: 1 Summary: Atomic lock-free primitives License: GNU General Public License v3 (GPLv3) URL: https://github.com/doodspav/atomics Source0: https://mirrors.nju.edu.cn/pypi/web/packages/cf/15/317e77646da3b316d49c93b8ea89e57c21b128e918f640c7130e7bff1c9e/atomics-1.0.2.tar.gz Requires: python3-cffi %description # atomics This library implements a wrapper around the lower level [patomic](https://github.com/doodspav/patomic) C library (which is provided as part of this library through the `build_patomic` command in `setup.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 Contents * [Installing](#installing) * [Examples](#examples) * [Incorrect](#incorrect) * [Multi-Threading](#multi-threading) * [Multi-Processing](#multi-processing) * [Docs](#docs) * [Types](#types) * [Construction](#construction) * [Lifetime](#lifetime) * [Contract](#contract) * [Alignment](#alignment) * [Properties](#properties) * [Operations](#operations) * [Special Methods](#special-methods) * [Memory Order](#memory-order) * [Exceptions](#exceptions) * [Building](#building) * [Future Thoughts](#future-thoughts) * [Contributing](#contributing) ## Installing Linux/MacOS: ```shell $ python3 -m pip install atomics ``` Windows: ```shell $ py -m pip install atomics ``` This library requires Python3.6+, and has a dependency on the `cffi` library. While the code here has no dependency on any implementation specific features, the `cffi` library 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 and `pip` tries to build from source or fails to install, make sure that you have the latest version of `pip` installed. This can be done like so: Linux/MacOS: ```shell $ python3 -m pip install --upgrade pip ``` Windows: ```shell $ py -m pip install --upgrade pip ``` If you need to build from source, check out the [Building](#building) section as there are additional requirements for that. ## Examples ### Incorrect The following example has a data race (`a`is modified from multiple threads). The program is not correct, and `a`'s value will not equal `total` at the end. ```python from threading import Thread a = 0 def fn(n: int) -> None: global a for _ in range(n): a += 1 if __name__ == "__main__": # setup total = 10_000_000 # run threads to completion t1 = Thread(target=fn, args=(total // 2,)) t2 = Thread(target=fn, args=(total // 2,)) t1.start(), t2.start() t1.join(), t2.join() # print results print(f"a[{a}] != total[{total}]") ``` ### Multi-Threading This example implements the previous example but `a` is now an `AtomicInt` which can be safely modified from multiple threads (as opposed to `int` which can't). The program is correct, and `a` will equal `total` at the end. ```python import atomics from threading import Thread def fn(ai: atomics.INTEGRAL, n: int) -> None: for _ in range(n): ai.inc() if __name__ == "__main__": # setup a = atomics.atomic(width=4, atype=atomics.INT) total = 10_000 # run threads to completion t1 = Thread(target=fn, args=(a, total // 2)) t2 = Thread(target=fn, args=(a, total // 2)) t1.start(), t2.start() t1.join(), t2.join() # print results print(f"a[{a.load()}] == total[{total}]") ``` ### Multi-Processing This 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, and `a` will equal `total` at the end. It is also how one might communicate with processes written in other languages such as C/C++. ```python import atomics from multiprocessing import Process, shared_memory def fn(shmem_name: str, width: int, n: int) -> None: shmem = shared_memory.SharedMemory(name=shmem_name) buf = shmem.buf[:width] with atomics.atomicview(buffer=buf, atype=atomics.INT) as a: for _ in range(n): a.inc() del buf shmem.close() if __name__ == "__main__": # setup width = 4 shmem = shared_memory.SharedMemory(create=True, size=width) buf = shmem.buf[:width] total = 10_000 # run processes to completion p1 = 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 cleanup with atomics.atomicview(buffer=buf, atype=atomics.INT) as a: print(f"a[{a.load()}] == total[{total}]") del buf shmem.close() shmem.unlink() ``` **NOTE:** Although `shared_memory` is showcased here, `atomicview` accepts any type that supports the buffer protocol as its buffer argument, so other sources of shared memory such as `mmap` could be used instead. ## Docs ### Types The following helper (abstract-ish base) types are available in `atomics`: - [`ANY`, `INTEGRAL`, `BYTES`, `INT`, `UINT`] This library provides the following `Atomic` classes in `atomics.base`: - `Atomic --- ANY` - `AtomicIntegral --- INTEGRAL` - `AtomicBytes --- BYTES` - `AtomicInt --- INT` - `AtomicUint --- UINT` These `Atomic` classes are constructable on their own, but it is strongly suggested using the `atomic()` function to construct them. Each class corresponds to one of the above helper types (as indicated). This library also provides `Atomic*View` (in `atomics.view`) and `Atomic*ViewContext` (in `atomics.ctx`) counterparts to the `Atomic*` 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 the `atomicview()` 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 corresponding `Atomic*ViewContext` object. 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 the [ARCHITECTURE.md](ARCHITECTURE.md) file (available on GitHub). ### Construction This library provides the functions `atomic` and `atomicview`, along with the types `BYTES`, `INT`, and `UINT` (as well as `ANY` and `INTEGRAL`) to construct atomic objects like so: ```python import atomics a = atomics.atomic(width=4, atype=atomics.INT) print(a) # AtomicInt(value=0, width=4, readonly=False, signed=True) buf = bytearray(2) with atomics.atomicview(buffer=buf, atype=atomics.BYTES) as a: print(a) # AtomicBytesView(value=b'\x00\x00', width=2, readonly=True) ``` You should only need to construct objects with an `atype` of `BYTES`, `INT`, or `UINT`. Using an `atype` of `ANY` or `INTGERAL` will require additional kwargs, and an `atype` of `ANY` will result in an object that doesn't actually expose any atomic operations (only properties, explained in sections further on). The `atomic()` function returns a corresponding `Atomic*` object. The `atomicview()` function returns a corresponding `Atomic*ViewContext` object. You can use this context object in a `with` statement to obtain an `Atomic*View` object. The `buffer` parameter may be any object that supports the buffer protocol. Construction can raise `UnsupportedWidthException` and `AlignmentError`. **NOTE:** the `width` property of `Atomic*View` objects is derived from the buffer's length as if it were contiguous. It is equivalent to calling `memoryview(buf).nbytes`. ### Lifetime Objects of `Atomic*` classes (i.e. objects returned by the `atomic()` 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 of `Atomic*ViewContext` classes (i.e. objects returned by the `atomicview()` function) and `Atomic*View` objects obtained from said objects have a much stricter usage contract. #### Contract The buffer used to construct an `Atomic*ViewContext` object (either directly or through `atomicview()`) **MUST NOT** be 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 a `with` statement, and **DO NOT** invalidate the buffer inside that `with` scope, you will always be safe. The protections implemented are shown in this example: ```python import atomics buf = 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'.") with ctx as a: # 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 exception ``` Furthermore, in CPython, all built-in types supporting the buffer protocol will throw a `BufferError` exception 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 the `with` context (where you can't call `.release()`), you may call `.__exit__(...)` manually on the `Atomic*ViewContext` object. 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. ### Alignment Different platforms may each have their own alignment requirements for atomic operations of given widths. This library provides the `Alignment` class in `atomics` to ensure that a given buffer meets these requirements. ```python from atomics import Alignment buf = bytearray(8) align = Alignment(len(buf)) assert align.is_valid(buf) ``` If an atomic class is constructed from a misaligned buffer, the constructor will raise `AlignmentError`. By default, `.is_valid` calls `.is_valid_recommended`. The class `Alignment` also 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. ### Properties All `Atomic*` and `Atomic*View` classes have the following properties: - `width`: width in bytes of the underlying buffer (as if it were contiguous) - `readonly`: whether the object supports modifying operations - `ops_supported`: a sorted list of `OpType` enum values representing which operations are supported on the object Integral `Atomic*` and `Atomic*View` classes also have the following property: - `signed`: whether arithmetic operations are signed or unsigned In both cases, the behaviour on overflow is defined to wraparound. ### Operations Base `Atomic` and `AtomicView` objects (corresponding to `ANY`) expose no atomic operations. `AtomicBytes` and `AtomicBytesView` objects 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_not` Integral `Atomic*` and `Atomic*View` classes additionally support the following operations: - **[arithmetic]**: `add`, `sub`, `inc`, `dec`, `neg` - **[arithmetic]**: `fetch_add`, `fetch_sub`, `fetch_inc`, `fetch_dec`, `fetch_neg` The usage of (most of) these functions is modelled directly on the C++11 `std::atomic` implementation found [here](https://en.cppreference.com/w/cpp/atomic/atomic). #### Compare Exchange (`cmpxchg_*`) The `cmpxchg_*` functions return `CmpxchgResult`. This has the attributes `.success: bool` which indicates whether the exchange took place, and `.expected: T` which holds the original value of the atomic object. The `cmpxchg_weak` function may fail spuriously, even if `expected` matches the actual value. It should be used as shown below: ```python import atomics def atomic_mul(a: atomics.INTEGRAL, operand: int): res = atomics.CmpxchgResult(success=False, expected=a.load()) while not res: desired = res.expected * operand res = a.cmpxchg_weak(expected=res.expected, desired=desired) ``` In a real implementation of `atomic_mul`, care should be taken to ensure that `desired` fits in `a` (i.e. `desired.bit_length() < (a.width * 8)`, assuming 8 bits in a byte). #### Exceptions All operations can raise `UnsupportedOperationException` (so check `.ops_supported` if you need to be sure). Operations `load`, `store`, and `cmpxchg_*` can raise `MemoryOrderError` if called with an invalid memory order. `MemoryOrder` enum values expose the functions `is_valid_store_order()`, `is_valid_load_order()`, and `is_valid_fail_order()` to check with. ### Special Methods `AtomicBytes` and `AtomicBytesView` implement the `__bytes__` special method. Integral `Atomic*` and `Atomic*View` classes 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 Order The `MemoryOrder` enum class is provided in `atomics`, and the memory orders are directly copied from C++11's `std::memory_order` documentation found [here](https://en.cppreference.com/w/cpp/atomic/memory_order), except for `CONSUME` (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()` (for `store` op) - `.is_valid_load_order()` ( for `load` op) - `.is_valid_fail_order()` (for the `fail` ordering in `cmpxchg_*` ops) Passing an invalid memory order to one of these ops will raise `MemoryOrderError`. ### Exceptions The following exceptions are available in `atomics.exc`: - `AlignmentError` - `MemoryOrderError` - `UnsupportedWidthException` - `UnsupportedOperationException` ## Building **IMPORTANT:** Make sure you have the latest version of `pip` installed. Using `setup.py`'s `build` or `bdist_wheel` commands will run the `build_patomic` command (which you can also run directly). This clones the `patomic` library into a temporary directory, builds it, and then copies the shared library into `atomics._clib`. This requires that `git` be installed on your system (a requirement of the `GitPython` module). You will also need an ANSI/C90 compliant C compiler (although ideally a more recent compiler should be used). `CMake` is also required but should be automatically `pip install`'d if not available. If you absolutely cannot get `build_patomic` to work, go to [patomic](https://github.com/doodspav/patomic), follow the instructions on building it (making sure to build the shared library version), and then copy-paste the shared library file into `atomics._clib` manually. **NOTE:** Currently, the library builds a dummy extension in order to trick `setuptools` into building a non-purepython wheel. If you are ok with a purepython wheel, then feel free to remove the code for that from `setup.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 Thoughts - add docstrings - add tests - add support for `minimum` alignment - add support for constructing `Atomic` classes' buffers in shared memory - add support for passing `Atomic` objects to sub-processes and sub-interpreters - reimplement in C or Cython for performance gains (preliminary benchmarks put such implementations at 2x the speed of a raw `int`) ## Contributing I don't have a guide for contributing yet. This section is here to make the following two points: - new operations must first be implemented in `patomic` before this library can be updated - new architectures, widths, and existing unsupported operations must be supported in `patomic` (no change required in this library) %package -n python3-atomics Summary: Atomic lock-free primitives Provides: python-atomics BuildRequires: python3-devel BuildRequires: python3-setuptools BuildRequires: python3-pip BuildRequires: python3-cffi BuildRequires: gcc BuildRequires: gdb %description -n python3-atomics # atomics This library implements a wrapper around the lower level [patomic](https://github.com/doodspav/patomic) C library (which is provided as part of this library through the `build_patomic` command in `setup.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 Contents * [Installing](#installing) * [Examples](#examples) * [Incorrect](#incorrect) * [Multi-Threading](#multi-threading) * [Multi-Processing](#multi-processing) * [Docs](#docs) * [Types](#types) * [Construction](#construction) * [Lifetime](#lifetime) * [Contract](#contract) * [Alignment](#alignment) * [Properties](#properties) * [Operations](#operations) * [Special Methods](#special-methods) * [Memory Order](#memory-order) * [Exceptions](#exceptions) * [Building](#building) * [Future Thoughts](#future-thoughts) * [Contributing](#contributing) ## Installing Linux/MacOS: ```shell $ python3 -m pip install atomics ``` Windows: ```shell $ py -m pip install atomics ``` This library requires Python3.6+, and has a dependency on the `cffi` library. While the code here has no dependency on any implementation specific features, the `cffi` library 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 and `pip` tries to build from source or fails to install, make sure that you have the latest version of `pip` installed. This can be done like so: Linux/MacOS: ```shell $ python3 -m pip install --upgrade pip ``` Windows: ```shell $ py -m pip install --upgrade pip ``` If you need to build from source, check out the [Building](#building) section as there are additional requirements for that. ## Examples ### Incorrect The following example has a data race (`a`is modified from multiple threads). The program is not correct, and `a`'s value will not equal `total` at the end. ```python from threading import Thread a = 0 def fn(n: int) -> None: global a for _ in range(n): a += 1 if __name__ == "__main__": # setup total = 10_000_000 # run threads to completion t1 = Thread(target=fn, args=(total // 2,)) t2 = Thread(target=fn, args=(total // 2,)) t1.start(), t2.start() t1.join(), t2.join() # print results print(f"a[{a}] != total[{total}]") ``` ### Multi-Threading This example implements the previous example but `a` is now an `AtomicInt` which can be safely modified from multiple threads (as opposed to `int` which can't). The program is correct, and `a` will equal `total` at the end. ```python import atomics from threading import Thread def fn(ai: atomics.INTEGRAL, n: int) -> None: for _ in range(n): ai.inc() if __name__ == "__main__": # setup a = atomics.atomic(width=4, atype=atomics.INT) total = 10_000 # run threads to completion t1 = Thread(target=fn, args=(a, total // 2)) t2 = Thread(target=fn, args=(a, total // 2)) t1.start(), t2.start() t1.join(), t2.join() # print results print(f"a[{a.load()}] == total[{total}]") ``` ### Multi-Processing This 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, and `a` will equal `total` at the end. It is also how one might communicate with processes written in other languages such as C/C++. ```python import atomics from multiprocessing import Process, shared_memory def fn(shmem_name: str, width: int, n: int) -> None: shmem = shared_memory.SharedMemory(name=shmem_name) buf = shmem.buf[:width] with atomics.atomicview(buffer=buf, atype=atomics.INT) as a: for _ in range(n): a.inc() del buf shmem.close() if __name__ == "__main__": # setup width = 4 shmem = shared_memory.SharedMemory(create=True, size=width) buf = shmem.buf[:width] total = 10_000 # run processes to completion p1 = 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 cleanup with atomics.atomicview(buffer=buf, atype=atomics.INT) as a: print(f"a[{a.load()}] == total[{total}]") del buf shmem.close() shmem.unlink() ``` **NOTE:** Although `shared_memory` is showcased here, `atomicview` accepts any type that supports the buffer protocol as its buffer argument, so other sources of shared memory such as `mmap` could be used instead. ## Docs ### Types The following helper (abstract-ish base) types are available in `atomics`: - [`ANY`, `INTEGRAL`, `BYTES`, `INT`, `UINT`] This library provides the following `Atomic` classes in `atomics.base`: - `Atomic --- ANY` - `AtomicIntegral --- INTEGRAL` - `AtomicBytes --- BYTES` - `AtomicInt --- INT` - `AtomicUint --- UINT` These `Atomic` classes are constructable on their own, but it is strongly suggested using the `atomic()` function to construct them. Each class corresponds to one of the above helper types (as indicated). This library also provides `Atomic*View` (in `atomics.view`) and `Atomic*ViewContext` (in `atomics.ctx`) counterparts to the `Atomic*` 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 the `atomicview()` 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 corresponding `Atomic*ViewContext` object. 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 the [ARCHITECTURE.md](ARCHITECTURE.md) file (available on GitHub). ### Construction This library provides the functions `atomic` and `atomicview`, along with the types `BYTES`, `INT`, and `UINT` (as well as `ANY` and `INTEGRAL`) to construct atomic objects like so: ```python import atomics a = atomics.atomic(width=4, atype=atomics.INT) print(a) # AtomicInt(value=0, width=4, readonly=False, signed=True) buf = bytearray(2) with atomics.atomicview(buffer=buf, atype=atomics.BYTES) as a: print(a) # AtomicBytesView(value=b'\x00\x00', width=2, readonly=True) ``` You should only need to construct objects with an `atype` of `BYTES`, `INT`, or `UINT`. Using an `atype` of `ANY` or `INTGERAL` will require additional kwargs, and an `atype` of `ANY` will result in an object that doesn't actually expose any atomic operations (only properties, explained in sections further on). The `atomic()` function returns a corresponding `Atomic*` object. The `atomicview()` function returns a corresponding `Atomic*ViewContext` object. You can use this context object in a `with` statement to obtain an `Atomic*View` object. The `buffer` parameter may be any object that supports the buffer protocol. Construction can raise `UnsupportedWidthException` and `AlignmentError`. **NOTE:** the `width` property of `Atomic*View` objects is derived from the buffer's length as if it were contiguous. It is equivalent to calling `memoryview(buf).nbytes`. ### Lifetime Objects of `Atomic*` classes (i.e. objects returned by the `atomic()` 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 of `Atomic*ViewContext` classes (i.e. objects returned by the `atomicview()` function) and `Atomic*View` objects obtained from said objects have a much stricter usage contract. #### Contract The buffer used to construct an `Atomic*ViewContext` object (either directly or through `atomicview()`) **MUST NOT** be 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 a `with` statement, and **DO NOT** invalidate the buffer inside that `with` scope, you will always be safe. The protections implemented are shown in this example: ```python import atomics buf = 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'.") with ctx as a: # 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 exception ``` Furthermore, in CPython, all built-in types supporting the buffer protocol will throw a `BufferError` exception 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 the `with` context (where you can't call `.release()`), you may call `.__exit__(...)` manually on the `Atomic*ViewContext` object. 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. ### Alignment Different platforms may each have their own alignment requirements for atomic operations of given widths. This library provides the `Alignment` class in `atomics` to ensure that a given buffer meets these requirements. ```python from atomics import Alignment buf = bytearray(8) align = Alignment(len(buf)) assert align.is_valid(buf) ``` If an atomic class is constructed from a misaligned buffer, the constructor will raise `AlignmentError`. By default, `.is_valid` calls `.is_valid_recommended`. The class `Alignment` also 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. ### Properties All `Atomic*` and `Atomic*View` classes have the following properties: - `width`: width in bytes of the underlying buffer (as if it were contiguous) - `readonly`: whether the object supports modifying operations - `ops_supported`: a sorted list of `OpType` enum values representing which operations are supported on the object Integral `Atomic*` and `Atomic*View` classes also have the following property: - `signed`: whether arithmetic operations are signed or unsigned In both cases, the behaviour on overflow is defined to wraparound. ### Operations Base `Atomic` and `AtomicView` objects (corresponding to `ANY`) expose no atomic operations. `AtomicBytes` and `AtomicBytesView` objects 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_not` Integral `Atomic*` and `Atomic*View` classes additionally support the following operations: - **[arithmetic]**: `add`, `sub`, `inc`, `dec`, `neg` - **[arithmetic]**: `fetch_add`, `fetch_sub`, `fetch_inc`, `fetch_dec`, `fetch_neg` The usage of (most of) these functions is modelled directly on the C++11 `std::atomic` implementation found [here](https://en.cppreference.com/w/cpp/atomic/atomic). #### Compare Exchange (`cmpxchg_*`) The `cmpxchg_*` functions return `CmpxchgResult`. This has the attributes `.success: bool` which indicates whether the exchange took place, and `.expected: T` which holds the original value of the atomic object. The `cmpxchg_weak` function may fail spuriously, even if `expected` matches the actual value. It should be used as shown below: ```python import atomics def atomic_mul(a: atomics.INTEGRAL, operand: int): res = atomics.CmpxchgResult(success=False, expected=a.load()) while not res: desired = res.expected * operand res = a.cmpxchg_weak(expected=res.expected, desired=desired) ``` In a real implementation of `atomic_mul`, care should be taken to ensure that `desired` fits in `a` (i.e. `desired.bit_length() < (a.width * 8)`, assuming 8 bits in a byte). #### Exceptions All operations can raise `UnsupportedOperationException` (so check `.ops_supported` if you need to be sure). Operations `load`, `store`, and `cmpxchg_*` can raise `MemoryOrderError` if called with an invalid memory order. `MemoryOrder` enum values expose the functions `is_valid_store_order()`, `is_valid_load_order()`, and `is_valid_fail_order()` to check with. ### Special Methods `AtomicBytes` and `AtomicBytesView` implement the `__bytes__` special method. Integral `Atomic*` and `Atomic*View` classes 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 Order The `MemoryOrder` enum class is provided in `atomics`, and the memory orders are directly copied from C++11's `std::memory_order` documentation found [here](https://en.cppreference.com/w/cpp/atomic/memory_order), except for `CONSUME` (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()` (for `store` op) - `.is_valid_load_order()` ( for `load` op) - `.is_valid_fail_order()` (for the `fail` ordering in `cmpxchg_*` ops) Passing an invalid memory order to one of these ops will raise `MemoryOrderError`. ### Exceptions The following exceptions are available in `atomics.exc`: - `AlignmentError` - `MemoryOrderError` - `UnsupportedWidthException` - `UnsupportedOperationException` ## Building **IMPORTANT:** Make sure you have the latest version of `pip` installed. Using `setup.py`'s `build` or `bdist_wheel` commands will run the `build_patomic` command (which you can also run directly). This clones the `patomic` library into a temporary directory, builds it, and then copies the shared library into `atomics._clib`. This requires that `git` be installed on your system (a requirement of the `GitPython` module). You will also need an ANSI/C90 compliant C compiler (although ideally a more recent compiler should be used). `CMake` is also required but should be automatically `pip install`'d if not available. If you absolutely cannot get `build_patomic` to work, go to [patomic](https://github.com/doodspav/patomic), follow the instructions on building it (making sure to build the shared library version), and then copy-paste the shared library file into `atomics._clib` manually. **NOTE:** Currently, the library builds a dummy extension in order to trick `setuptools` into building a non-purepython wheel. If you are ok with a purepython wheel, then feel free to remove the code for that from `setup.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 Thoughts - add docstrings - add tests - add support for `minimum` alignment - add support for constructing `Atomic` classes' buffers in shared memory - add support for passing `Atomic` objects to sub-processes and sub-interpreters - reimplement in C or Cython for performance gains (preliminary benchmarks put such implementations at 2x the speed of a raw `int`) ## Contributing I don't have a guide for contributing yet. This section is here to make the following two points: - new operations must first be implemented in `patomic` before this library can be updated - new architectures, widths, and existing unsupported operations must be supported in `patomic` (no change required in this library) %package help Summary: Development documents and examples for atomics Provides: python3-atomics-doc %description help # atomics This library implements a wrapper around the lower level [patomic](https://github.com/doodspav/patomic) C library (which is provided as part of this library through the `build_patomic` command in `setup.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 Contents * [Installing](#installing) * [Examples](#examples) * [Incorrect](#incorrect) * [Multi-Threading](#multi-threading) * [Multi-Processing](#multi-processing) * [Docs](#docs) * [Types](#types) * [Construction](#construction) * [Lifetime](#lifetime) * [Contract](#contract) * [Alignment](#alignment) * [Properties](#properties) * [Operations](#operations) * [Special Methods](#special-methods) * [Memory Order](#memory-order) * [Exceptions](#exceptions) * [Building](#building) * [Future Thoughts](#future-thoughts) * [Contributing](#contributing) ## Installing Linux/MacOS: ```shell $ python3 -m pip install atomics ``` Windows: ```shell $ py -m pip install atomics ``` This library requires Python3.6+, and has a dependency on the `cffi` library. While the code here has no dependency on any implementation specific features, the `cffi` library 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 and `pip` tries to build from source or fails to install, make sure that you have the latest version of `pip` installed. This can be done like so: Linux/MacOS: ```shell $ python3 -m pip install --upgrade pip ``` Windows: ```shell $ py -m pip install --upgrade pip ``` If you need to build from source, check out the [Building](#building) section as there are additional requirements for that. ## Examples ### Incorrect The following example has a data race (`a`is modified from multiple threads). The program is not correct, and `a`'s value will not equal `total` at the end. ```python from threading import Thread a = 0 def fn(n: int) -> None: global a for _ in range(n): a += 1 if __name__ == "__main__": # setup total = 10_000_000 # run threads to completion t1 = Thread(target=fn, args=(total // 2,)) t2 = Thread(target=fn, args=(total // 2,)) t1.start(), t2.start() t1.join(), t2.join() # print results print(f"a[{a}] != total[{total}]") ``` ### Multi-Threading This example implements the previous example but `a` is now an `AtomicInt` which can be safely modified from multiple threads (as opposed to `int` which can't). The program is correct, and `a` will equal `total` at the end. ```python import atomics from threading import Thread def fn(ai: atomics.INTEGRAL, n: int) -> None: for _ in range(n): ai.inc() if __name__ == "__main__": # setup a = atomics.atomic(width=4, atype=atomics.INT) total = 10_000 # run threads to completion t1 = Thread(target=fn, args=(a, total // 2)) t2 = Thread(target=fn, args=(a, total // 2)) t1.start(), t2.start() t1.join(), t2.join() # print results print(f"a[{a.load()}] == total[{total}]") ``` ### Multi-Processing This 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, and `a` will equal `total` at the end. It is also how one might communicate with processes written in other languages such as C/C++. ```python import atomics from multiprocessing import Process, shared_memory def fn(shmem_name: str, width: int, n: int) -> None: shmem = shared_memory.SharedMemory(name=shmem_name) buf = shmem.buf[:width] with atomics.atomicview(buffer=buf, atype=atomics.INT) as a: for _ in range(n): a.inc() del buf shmem.close() if __name__ == "__main__": # setup width = 4 shmem = shared_memory.SharedMemory(create=True, size=width) buf = shmem.buf[:width] total = 10_000 # run processes to completion p1 = 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 cleanup with atomics.atomicview(buffer=buf, atype=atomics.INT) as a: print(f"a[{a.load()}] == total[{total}]") del buf shmem.close() shmem.unlink() ``` **NOTE:** Although `shared_memory` is showcased here, `atomicview` accepts any type that supports the buffer protocol as its buffer argument, so other sources of shared memory such as `mmap` could be used instead. ## Docs ### Types The following helper (abstract-ish base) types are available in `atomics`: - [`ANY`, `INTEGRAL`, `BYTES`, `INT`, `UINT`] This library provides the following `Atomic` classes in `atomics.base`: - `Atomic --- ANY` - `AtomicIntegral --- INTEGRAL` - `AtomicBytes --- BYTES` - `AtomicInt --- INT` - `AtomicUint --- UINT` These `Atomic` classes are constructable on their own, but it is strongly suggested using the `atomic()` function to construct them. Each class corresponds to one of the above helper types (as indicated). This library also provides `Atomic*View` (in `atomics.view`) and `Atomic*ViewContext` (in `atomics.ctx`) counterparts to the `Atomic*` 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 the `atomicview()` 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 corresponding `Atomic*ViewContext` object. 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 the [ARCHITECTURE.md](ARCHITECTURE.md) file (available on GitHub). ### Construction This library provides the functions `atomic` and `atomicview`, along with the types `BYTES`, `INT`, and `UINT` (as well as `ANY` and `INTEGRAL`) to construct atomic objects like so: ```python import atomics a = atomics.atomic(width=4, atype=atomics.INT) print(a) # AtomicInt(value=0, width=4, readonly=False, signed=True) buf = bytearray(2) with atomics.atomicview(buffer=buf, atype=atomics.BYTES) as a: print(a) # AtomicBytesView(value=b'\x00\x00', width=2, readonly=True) ``` You should only need to construct objects with an `atype` of `BYTES`, `INT`, or `UINT`. Using an `atype` of `ANY` or `INTGERAL` will require additional kwargs, and an `atype` of `ANY` will result in an object that doesn't actually expose any atomic operations (only properties, explained in sections further on). The `atomic()` function returns a corresponding `Atomic*` object. The `atomicview()` function returns a corresponding `Atomic*ViewContext` object. You can use this context object in a `with` statement to obtain an `Atomic*View` object. The `buffer` parameter may be any object that supports the buffer protocol. Construction can raise `UnsupportedWidthException` and `AlignmentError`. **NOTE:** the `width` property of `Atomic*View` objects is derived from the buffer's length as if it were contiguous. It is equivalent to calling `memoryview(buf).nbytes`. ### Lifetime Objects of `Atomic*` classes (i.e. objects returned by the `atomic()` 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 of `Atomic*ViewContext` classes (i.e. objects returned by the `atomicview()` function) and `Atomic*View` objects obtained from said objects have a much stricter usage contract. #### Contract The buffer used to construct an `Atomic*ViewContext` object (either directly or through `atomicview()`) **MUST NOT** be 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 a `with` statement, and **DO NOT** invalidate the buffer inside that `with` scope, you will always be safe. The protections implemented are shown in this example: ```python import atomics buf = 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'.") with ctx as a: # 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 exception ``` Furthermore, in CPython, all built-in types supporting the buffer protocol will throw a `BufferError` exception 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 the `with` context (where you can't call `.release()`), you may call `.__exit__(...)` manually on the `Atomic*ViewContext` object. 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. ### Alignment Different platforms may each have their own alignment requirements for atomic operations of given widths. This library provides the `Alignment` class in `atomics` to ensure that a given buffer meets these requirements. ```python from atomics import Alignment buf = bytearray(8) align = Alignment(len(buf)) assert align.is_valid(buf) ``` If an atomic class is constructed from a misaligned buffer, the constructor will raise `AlignmentError`. By default, `.is_valid` calls `.is_valid_recommended`. The class `Alignment` also 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. ### Properties All `Atomic*` and `Atomic*View` classes have the following properties: - `width`: width in bytes of the underlying buffer (as if it were contiguous) - `readonly`: whether the object supports modifying operations - `ops_supported`: a sorted list of `OpType` enum values representing which operations are supported on the object Integral `Atomic*` and `Atomic*View` classes also have the following property: - `signed`: whether arithmetic operations are signed or unsigned In both cases, the behaviour on overflow is defined to wraparound. ### Operations Base `Atomic` and `AtomicView` objects (corresponding to `ANY`) expose no atomic operations. `AtomicBytes` and `AtomicBytesView` objects 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_not` Integral `Atomic*` and `Atomic*View` classes additionally support the following operations: - **[arithmetic]**: `add`, `sub`, `inc`, `dec`, `neg` - **[arithmetic]**: `fetch_add`, `fetch_sub`, `fetch_inc`, `fetch_dec`, `fetch_neg` The usage of (most of) these functions is modelled directly on the C++11 `std::atomic` implementation found [here](https://en.cppreference.com/w/cpp/atomic/atomic). #### Compare Exchange (`cmpxchg_*`) The `cmpxchg_*` functions return `CmpxchgResult`. This has the attributes `.success: bool` which indicates whether the exchange took place, and `.expected: T` which holds the original value of the atomic object. The `cmpxchg_weak` function may fail spuriously, even if `expected` matches the actual value. It should be used as shown below: ```python import atomics def atomic_mul(a: atomics.INTEGRAL, operand: int): res = atomics.CmpxchgResult(success=False, expected=a.load()) while not res: desired = res.expected * operand res = a.cmpxchg_weak(expected=res.expected, desired=desired) ``` In a real implementation of `atomic_mul`, care should be taken to ensure that `desired` fits in `a` (i.e. `desired.bit_length() < (a.width * 8)`, assuming 8 bits in a byte). #### Exceptions All operations can raise `UnsupportedOperationException` (so check `.ops_supported` if you need to be sure). Operations `load`, `store`, and `cmpxchg_*` can raise `MemoryOrderError` if called with an invalid memory order. `MemoryOrder` enum values expose the functions `is_valid_store_order()`, `is_valid_load_order()`, and `is_valid_fail_order()` to check with. ### Special Methods `AtomicBytes` and `AtomicBytesView` implement the `__bytes__` special method. Integral `Atomic*` and `Atomic*View` classes 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 Order The `MemoryOrder` enum class is provided in `atomics`, and the memory orders are directly copied from C++11's `std::memory_order` documentation found [here](https://en.cppreference.com/w/cpp/atomic/memory_order), except for `CONSUME` (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()` (for `store` op) - `.is_valid_load_order()` ( for `load` op) - `.is_valid_fail_order()` (for the `fail` ordering in `cmpxchg_*` ops) Passing an invalid memory order to one of these ops will raise `MemoryOrderError`. ### Exceptions The following exceptions are available in `atomics.exc`: - `AlignmentError` - `MemoryOrderError` - `UnsupportedWidthException` - `UnsupportedOperationException` ## Building **IMPORTANT:** Make sure you have the latest version of `pip` installed. Using `setup.py`'s `build` or `bdist_wheel` commands will run the `build_patomic` command (which you can also run directly). This clones the `patomic` library into a temporary directory, builds it, and then copies the shared library into `atomics._clib`. This requires that `git` be installed on your system (a requirement of the `GitPython` module). You will also need an ANSI/C90 compliant C compiler (although ideally a more recent compiler should be used). `CMake` is also required but should be automatically `pip install`'d if not available. If you absolutely cannot get `build_patomic` to work, go to [patomic](https://github.com/doodspav/patomic), follow the instructions on building it (making sure to build the shared library version), and then copy-paste the shared library file into `atomics._clib` manually. **NOTE:** Currently, the library builds a dummy extension in order to trick `setuptools` into building a non-purepython wheel. If you are ok with a purepython wheel, then feel free to remove the code for that from `setup.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 Thoughts - add docstrings - add tests - add support for `minimum` alignment - add support for constructing `Atomic` classes' buffers in shared memory - add support for passing `Atomic` objects to sub-processes and sub-interpreters - reimplement in C or Cython for performance gains (preliminary benchmarks put such implementations at 2x the speed of a raw `int`) ## Contributing I don't have a guide for contributing yet. This section is here to make the following two points: - new operations must first be implemented in `patomic` before this library can be updated - new architectures, widths, and existing unsupported operations must be supported in `patomic` (no change required in this library) %prep %autosetup -n atomics-1.0.2 %build %py3_build %install %py3_install install -d -m755 %{buildroot}/%{_pkgdocdir} if [ -d doc ]; then cp -arf doc %{buildroot}/%{_pkgdocdir}; fi if [ -d docs ]; then cp -arf docs %{buildroot}/%{_pkgdocdir}; fi if [ -d example ]; then cp -arf example %{buildroot}/%{_pkgdocdir}; fi if [ -d examples ]; then cp -arf examples %{buildroot}/%{_pkgdocdir}; fi pushd %{buildroot} if [ -d usr/lib ]; then find usr/lib -type f -printf "/%h/%f\n" >> filelist.lst fi if [ -d usr/lib64 ]; then find usr/lib64 -type f -printf "/%h/%f\n" >> filelist.lst fi if [ -d usr/bin ]; then find usr/bin -type f -printf "/%h/%f\n" >> filelist.lst fi if [ -d usr/sbin ]; then find usr/sbin -type f -printf "/%h/%f\n" >> filelist.lst fi touch doclist.lst if [ -d usr/share/man ]; then find usr/share/man -type f -printf "/%h/%f.gz\n" >> doclist.lst fi popd mv %{buildroot}/filelist.lst . mv %{buildroot}/doclist.lst . %files -n python3-atomics -f filelist.lst %dir %{python3_sitearch}/* %files help -f doclist.lst %{_docdir}/* %changelog * Fri May 05 2023 Python_Bot - 1.0.2-1 - Package Spec generated