@@ -595,6 +595,158 @@ with C code to initialize global variables.
595595The actual ``lib.*() `` function calls should be obvious: it's like C.
596596
597597
598+ .. _thread-safety :
599+
600+ Thread Safety
601+ -------------
602+
603+ Multithreading can be a powerful but tricky way to exploit the many cores on
604+ modern CPUs. Combining CFFI with the Python `threading ` module is a convenient
605+ way to use multithreaded parallelism with a C library.
606+
607+ On the GIL-enabled build, CFFI will release the GIL before calling into a C
608+ library. That means that it is possible to get multithreaded speedups using CFFI
609+ on both the free-threaded and GIL-enabled builds of Python. However, that also
610+ means that the GIL does not protect multithreaded shared use of C data
611+ structures exposed via FFI.
612+
613+ If the C library you are wrapping is not thread-safe, then it is not thread-safe
614+ to use the library via Python without adding some kind of locking. If the
615+ library *is * thread-safe, then no additional locking is necessary to ensure the
616+ thread safety of CFFI itself. As of version 2.0, CFFI generates thread-safe
617+ bindings.
618+
619+ Let's make that concrete by wrapping some code that is not thread-safe due to
620+ use of a C global variable:
621+
622+ .. code-block :: python
623+
624+ from cffi import FFI
625+ ffibuilder = FFI()
626+
627+ ffibuilder.set_source(" _thread_safety_example" ,
628+ r """
629+ # include <stdint.h>
630+
631+ static int64_t value = 0;
632+ static int64_t increment( void) {
633+ value++ ;
634+ return value;
635+ }
636+ """ ,
637+ libraries = []
638+ )
639+
640+ ffibuilder.cdef(r """
641+ int64_t increment( void) ;
642+ """
643+ )
644+
645+ if __name__ == " __main__" :
646+ ffibuilder.compile(verbose = True )
647+
648+ The way that the ``increment `` uses the ``value `` global variable is not
649+ thread-safe. `Data races
650+ <https://en.wikipedia.org/wiki/Race_condition#Data_race> `_ are possible if two
651+ threads simultaneously call ``increment ``. We can engineer that situation with a
652+ Python script that calls into the wrapper like so:
653+
654+ .. code-block :: python
655+
656+ import sys
657+
658+ from concurrent.futures import ThreadPoolExecutor, wait
659+ import threading
660+
661+ from _thread_safety_example import ffi, lib
662+
663+ # Make races more likely by switching threads more often
664+ # on the GIL-enabled build. This has no effect on the
665+ # free-threaded build.
666+ sys.setswitchinterval(.0000001 )
667+
668+ N_WORKERS = 4
669+
670+ l = threading.Lock()
671+
672+ def work ():
673+ lib.increment()
674+
675+ def run_thread_pool ():
676+ with ThreadPoolExecutor(max_workers = N_WORKERS ) as tpe:
677+ try :
678+ futures = [tpe.submit(work) for _ in range (100000 )]
679+ # block until all work finishes
680+ wait(futures)
681+ finally :
682+ # check for exceptions in worker threads
683+ [f.result() for f in futures]
684+
685+
686+ run_thread_pool()
687+
688+ print (lib.increment())
689+
690+ On the system used to run this example by the author, this script prints random
691+ results, with possible result values ranging from 99960 to 99980, indicating
692+ that, on average, races happen a few dozen times over the hundred thousand loop
693+ iterations. The results you get will depend on your hardware, system
694+ configuration, and Python interpreter version.
695+
696+ Note that races are relatively rare. The CFFI bindings and Python interpreter
697+ add enough overhead that it is not very likely for two threads to simultaneously
698+ increment the static integer. This can make code *appear * to be sequentially
699+ consistent for small sample sizes, when it is in fact not consistent. See `this
700+ tutorial
701+ <https://github.com/facebookincubator/ft_utils/blob/main/docs/fine_grained_synchronization.md#understanding-the-gil> `_
702+ for more examples of how the GIL and Python overhead can mask thread safety
703+ issues that only manifest under production load.
704+
705+ We can make the above example script thread-safe by using a lock:
706+
707+ .. code-block :: python
708+
709+ l = threading.Lock()
710+
711+ def work ():
712+ l.acquire()
713+ lib.increment()
714+ l.release()
715+
716+ The `threading.Lock ` ensures only one thread can call into the wrapped C library
717+ at a time. Any thread that calls ``l.acquire() `` while another thread has
718+ already acquired the lock will block until the lock is released.
719+
720+ Using a global lock like this is necessary it is not safe for more than one
721+ thread to simultaneously call into any part of the library. This is the case if
722+ the library relies on global state its implementation that does not have any
723+ explicit synchronization. Libraries like this are not re-entrant.
724+
725+ For re-entrant libraries, where two threads can simultaneously use the library
726+ so long as the threads do not share references to an object, generally you will
727+ want to use a per-object lock instead of a global lock. Keep in mind in this
728+ case that any program with more than one lock can lead to a deadlock and care
729+ must be taken to avoid situations where two threads can deadlock.
730+
731+ If you do not expect to use the bindings for a thread-unsafe library in a
732+ multithreaded program, locking is not necessary. Similarly if you know that you
733+ are using the library in a thread-safe manner by construction, it is not
734+ necessary to add locking. Also, if you know that the C library you are wrapping
735+ is thread-safe, no additional locking is necessary to make the CFFI bindings
736+ thread-safe. As of version 2.0, CFFI generates thread-safe bindings to C
737+ libraries.
738+
739+ If you publish CFFI bindings for a library, you should document the thread
740+ safety guarantees of your bindings. It may make sense to add locking into the
741+ bindings but it might also make sense to clearly document the bindings are not
742+ thread-safe and it is up to users to ensure appropriate synchronization or
743+ exclusive access if users do want to use the bindings in a thread pool.
744+
745+ See the Python free-threading guide page on `improving the thread safety of
746+ Python code
747+ <https://py-free-threading.github.io/porting/#thread-safety-of-pure-python-code> `_
748+ for more information about updating a Python library with thread safety in mind.
749+
598750.. _abi-versus-api :
599751
600752ABI versus API
0 commit comments