Package: cereggii
cereggiiThread synchronization utilities for free-threaded Python.This library provides some atomic data types which, in a multithreaded context, are generally more performant compared
to CPython's builtin types.Cereus greggiiThePeniocereus Greggii(also known asCereus Greggii) is a flower native to Arizona, New Mexico, Texas, and some
parts of northern Mexico.This flower blooms just one summer night every year and in any given area, all these flowers bloom in synchrony.WikipediaImage credits: Patrick Alexander, Peniocereus greggii var. greggii, south of Cooke's Range, Luna County, New Mexico, 10
May 2018, CC0.sourceInstallingThis library is experimentalUsing@colesbury's original nogil forkis required
to use this library.
You can get it with pyenv:pyenvinstallnogil-3.9.10-1Then, you may fetch this libraryfrom PyPI:pipinstallcereggiiIf you happened to use a non-free-threaded interpreter, this library may not be able to run, and if it does, you will
see poor performance.AtomicIntIn Python (be it free-threaded or not), the following piece of code is not thread-safe:a=0a+=1That is, ifais shared with multiple threads, and they attempt to modifya, the value ofaafter any
number (> 1) of writes, is undefined.The following piece of code is instead thread-safe:a=cereggii.AtomicInt(0)a+=1Also, consider the following piece of code:importthreadingclassSpam:def__init__(self):self.counter=0spam=Spam()print(f"{spam.counter=}")defincr():foriinrange(1_000_000):spam.counter+=1t1=threading.Thread(target=incr)t2=threading.Thread(target=incr)t1.start()t2.start()t1.join()t2.join()print(f"{spam.counter=}")The output you'll see onscreen is not known.
If you subsitituteself.counter = 0withself.counter = AtomicInt(0), you'll be guaranteed to see2_000_000, and the program should run slightly faster.If you make an additional modification, your program will run much faster:defincr():h=spam.counter.get_handle()foriinrange(1_000_000):h+=1When usingAtomicIntHandle, you should see your CPUs being fully used.AtomicIntborrows part of its API from Java'sAtomicInteger, so that it should feel familiar to use, if you're
coming to Python from Java.
It also implements most numeric magic methods, so that it should feel comfortable to use for Pythonistas.
It tries to mimic Python'sintas close as possible, with some caveats:it is bound to 64-bit integers, so you may encounterOverflowError;hashing is based on theAtomicInt's address in memory, so two distinctAtomicInts will have distinct hashes, even
when they hold the same value (bonus feature: anAtomicIntHandlehas the same hash of its
correspondingAtomicInt); [^1]the following operations are not supported:__itruediv__(anAtomicIntcannot be used to store floats)as_integer_ratiobit_lengthconjugatefrom_bytesto_bytesdenominatornumeratorimagreal[^1]: This behavior ensures the hashing property that identity implies hash equality.An explanation of these claimsFirst,a += 1in CPython actually translates to more than one bytecode instruction, namely:0 LOAD_CONST               2 (1)
              2 INPLACE_ADD              0 (a)
              4 STORE_FAST               0 (a)This means that between theINPLACE_ADDand theSTORE_FASTinstructions, the value ofamay have been changed by
another thread, so that one or multiple increments may be lost.Second, the performance problem.
How come the speedup?If we look again at the code, there are a couple of implicit memory locations which are being contended by threads:the reference count ofspam.count;the reference counts of theintobjects themselves; andthe lock protecting the implicitspam.__dict__.These contentions are eliminated byAtomicInt, cf.:spam.countis accessed indirectly through anAtomicIntHandlewhich avoids contention on its reference count (it
is contended only during the.get_handle()call);this is avoided by not creatingintobjects during the increment;again, using the handle instead of theAtomicIntitself avoids spurious contention.Also see colesbury/nogil#121.AtomicDictCurrently, the implementation ofAtomicDictis quite limited:(dpdani/cereggii#3) it can hold at most $2^{25}$ keys (~33.5M);(dpdani/cereggii#4) it does not support deletions (del d[k]);(dpdani/cereggii#5) it does not support dynamic resizing; andseveral common functionalities are missing.You can see that there is some more performance to be gained by simply usingAtomicDict, comparing the following two
programs.The usage ofAtomicIntprovides correctness, regardless of the hashmap implementation.
But usingAtomicDictinstead ofdictimproves performance, even without using handles: writes to distinct keys do
not generate contention.Withdict:importthreadingfromcereggiiimportAtomicDict,AtomicIntclassSpam:def__init__(self):self.d={k:AtomicInt(0)forkinrange(10)}spam=Spam()print(f"{spam.d[0].get()=}{spam.d[1].get()=}{spam.d[2].get()=}{spam.d[3].get()=}{spam.d[4].get()=}"f"{spam.d[5].get()=}{spam.d[6].get()=}{spam.d[7].get()=}{spam.d[8].get()=}{spam.d[9].get()=}")defincr():keys=list(range(10))for_inrange(5_000_000):spam.d[keys[_%len(keys)]]+=1threads=[threading.Thread(target=incr)for_inrange(3)]fortinthreads:t.start()fortinthreads:t.join()print(f"{spam.d[0].get()=}{spam.d[1].get()=}{spam.d[2].get()=}{spam.d[3].get()=}{spam.d[4].get()=}"f"{spam.d[5].get()=}{spam.d[6].get()=}{spam.d[7].get()=}{spam.d[8].get()=}{spam.d[9].get()=}")To useAtomicDict, substitute inself.d = AtomicDict({k: AtomicInt(0) for k in range(10)}).AtomicRefYou can use anAtomicRefwhen you have a shared variable that points to an object, and you need to change the
referenced object concurrently.This is not available in the Python standard library and was initially implemented as part ofAtomicDict.importcereggiio=object()d={}i=0r=cereggii.AtomicRef()assertr.get()isNoneassertr.compare_and_set(None,o)assertr.get_and_set(d)==or.set(i)# always returns NoneExperimentalThis library is experimental and should not be used in a production environment.After all, as of now, it requires a non-official fork in order to run.Porting to free-threaded Python 3.13 (3.13t) is planned.