Threads¶
docshare normally runs while a module is being imported, and two threads
may import two modules that each use it. That is safe.
The cache is the library’s only shared mutable state, and every operation on
it is atomic with respect to other threads. Iteration, and the keys,
values and items views built on it, work from a snapshot, so one thread
may walk the cache while another writes to it.
Everything else is either built once when docshare is imported and only
read afterwards, such as the table of recognized sections, or belongs to the
caller: parsing, composing and rendering keep no state between calls.
Why a lock is needed at all¶
Looking an entry up also marks it as recently used, and discarding entries walks the whole cache, so neither is a single dictionary operation. Two of the resulting races were reproducible even under the GIL:
evict vs. resize KeyError('dictionary is empty')
iterate vs. mutate RuntimeError('OrderedDict mutated during iteration')
Free-threaded builds make them likelier rather than rarer.
Parsing happens outside the lock¶
Two threads that ask for the same unrecorded docstring at once will both parse it, and one will replace the other’s entry. A parsed document depends on nothing but the text it came from, so the two are equivalent. Holding the lock across parsing would serialize exactly the work the cache exists to avoid.
What is not guaranteed¶
Anything about the objects you decorate. Two threads decorating the same
object at the same time race on its __doc__ like any other attribute.
Normal use decorates each object once, while the module defining it is being
imported.