When there are not enough locks from the standard library
Project description
It contains several useful additions to the standard thread synchronization tools, such as lock protocols and locks with advanced functionality.
Table of contents
Installation
Get the locklib from the pypi:
pip install locklib
... or directly from git:
pip install git+https://github.com/pomponchik/locklib.git
You can also quickly try out this and other packages without having to install using instld.
Lock protocols
Protocols are needed so that you can write typed code without being bound to specific classes. Protocols from this library allow you to "equalize" locks from the standard library and third-party locks, including those provided by this library.
We consider the basic characteristic of the lock protocol to be the presence of two methods for an object:
def acquire() -> None: pass
def release() -> None: pass
All the locks from the standard library correspond to this, as well as the locks presented in this one.
To check for compliance with this minimum standard, locklib contains the LockProtocol. You can check for yourself that all the locks match it:
from multiprocessing import Lock as MLock
from threading import Lock as TLock, RLock as TRLock
from asyncio import Lock as ALock
from locklib import SmartLock, LockProtocol
print(isinstance(MLock(), LockProtocol)) # True
print(isinstance(TLock(), LockProtocol)) # True
print(isinstance(TRLock(), LockProtocol)) # True
print(isinstance(ALock(), LockProtocol)) # True
print(isinstance(SmartLock(), LockProtocol)) # True
However! Most idiomatic python code using locks uses them as context managers. If your code is like that too, you can use one of the two inheritors of the regular LockProtocol: ContextLockProtocol or AsyncContextLockProtocol. Thus, the protocol inheritance hierarchy looks like this:
LockProtocol
├── ContextLockProtocol
└── AsyncContextLockProtocol
ContextLockProtocol describes the objects described by LockProtocol, which are also context managers. AsyncContextLockProtocol, by analogy, describes objects that are instances of LockProtocol, as well as asynchronous context managers.
Almost all the locks from the standard library are instances of ContextLockProtocol, as well as SmartLock.
from multiprocessing import Lock as MLock
from threading import Lock as TLock, RLock as TRLock
from locklib import SmartLock, ContextLockProtocol
print(isinstance(MLock(), ContextLockProtocol)) # True
print(isinstance(TLock(), ContextLockProtocol)) # True
print(isinstance(TRLock(), ContextLockProtocol)) # True
print(isinstance(SmartLock(), ContextLockProtocol)) # True
However, the Lock from asyncio belongs to a separate category and AsyncContextLockProtocol is needed to describe it:
from asyncio import Lock
from locklib import AsyncContextLockProtocol
print(isinstance(Lock(), AsyncContextLockProtocol)) # True
If you use type hints and static verification tools like mypy, we highly recommend using the narrowest of the presented categories for lock protocols, which describe the requirements for your locales.
SmartLock - deadlock is impossible with it
locklib contains a lock that cannot get into the deadlock - SmartLock, based on Wait-for Graph. You can use it as a usual Lock from the standard library. Let's check that it can protect us from the race condition in the same way:
from threading import Thread
from locklib import SmartLock
lock = SmartLock()
counter = 0
def function():
global counter
for _ in range(1000):
with lock:
counter += 1
thread_1 = Thread(target=function)
thread_2 = Thread(target=function)
thread_1.start()
thread_2.start()
assert counter == 2000
Yeah, in this case the lock helps us not to get a race condition, as the standard Lock does. But! Let's trigger a deadlock and look what happens:
from threading import Thread
from locklib import SmartLock
lock_1 = SmartLock()
lock_2 = SmartLock()
def function_1():
while True:
with lock_1:
with lock_2:
pass
def function_2():
while True:
with lock_2:
with lock_1:
pass
thread_1 = Thread(target=function_1)
thread_2 = Thread(target=function_2)
thread_1.start()
thread_2.start()
And... We have an exception like this:
...
locklib.errors.DeadLockError: A cycle between 1970256th and 1970257th threads has been detected.
Deadlocks are impossible for this lock!
If you want to catch the exception, import this from the locklib too:
from locklib import DeadLockError
Test your locks
Sometimes, when testing a code, you may need to detect if some action is taking place inside the lock. How to do this with a minimum of code? There is the LockTraceWrapper for this. It is a wrapper around a regular lock, which records it every time the code takes a lock or releases it. At the same time, the functionality of the wrapped lock is fully preserved.
It's easy to create an object of such a lock. Just pass any other lock to the class constructor:
from threading import Lock
from locklib import LockTraceWrapper
lock = LockTraceWrapper(Lock())
You can use it in the same way as the wrapped lock:
with lock:
...
Anywhere in your program, you can "inform" the lock that the action you need is being performed here:
lock.notify('event_name')
And! Now you can easily identify if there were cases when an event with this identifier did not occur under the mutex. To do this, use the was_event_locked method:
lock.was_event_locked('event_name')
If the notify method was called with the same parameter only when the lock activated, it will return True. If not, that is, if there was at least one case when the c method was called with such an identifier without an activated mutex, False will be returned.
How does it work? A modified algorithm for determining the correct parenthesis sequence is used here. For each thread for which any events were registered (taking the mutex, releasing the mutex, and also calling the notify method), the check takes place separately, that is, we determine that it was the same thread that held the mutex when notify was called, and not some other one.
⚠️ The thread id is used to identify the streams. This id may be reused if the current thread ends, which in some cases may lead to incorrect identification of lock coverage for operations that were not actually covered by the lock. Make sure that this cannot happen during your test.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file locklib-0.0.18.tar.gz.
File metadata
- Download URL: locklib-0.0.18.tar.gz
- Upload date:
- Size: 11.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
2d622db4c80984d81f186acb4db33e61978ab362f4535381b313282fcef6dc5f
|
|
| MD5 |
a28b125dc8808a66bbcc7db5bac6a8ea
|
|
| BLAKE2b-256 |
4d260b2917af8ce3b24a2ab422b9e92225d940f7b2addb3bd333247c2596be1e
|
Provenance
The following attestation bundles were made for locklib-0.0.18.tar.gz:
Publisher:
publish.yml on pomponchik/locklib
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
locklib-0.0.18.tar.gz -
Subject digest:
2d622db4c80984d81f186acb4db33e61978ab362f4535381b313282fcef6dc5f - Sigstore transparency entry: 598040253
- Sigstore integration time:
-
Permalink:
pomponchik/locklib@feae2af13ef412d2b6d5595a7219a2eab80e4eee -
Branch / Tag:
refs/heads/main - Owner: https://github.com/pomponchik
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@feae2af13ef412d2b6d5595a7219a2eab80e4eee -
Trigger Event:
push
-
Statement type:
File details
Details for the file locklib-0.0.18-py3-none-any.whl.
File metadata
- Download URL: locklib-0.0.18-py3-none-any.whl
- Upload date:
- Size: 10.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
63d2891b8473d5d8afb4b47cb70e4b809a2c5e0d4149519aa294b1e9beb2bff7
|
|
| MD5 |
80da15fb657c718dca0974cdecccf9e4
|
|
| BLAKE2b-256 |
9366f9057ca6935d629d48a9bfc495fd015f544f64bab387cc9530ab375e8a6f
|
Provenance
The following attestation bundles were made for locklib-0.0.18-py3-none-any.whl:
Publisher:
publish.yml on pomponchik/locklib
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
locklib-0.0.18-py3-none-any.whl -
Subject digest:
63d2891b8473d5d8afb4b47cb70e4b809a2c5e0d4149519aa294b1e9beb2bff7 - Sigstore transparency entry: 598040257
- Sigstore integration time:
-
Permalink:
pomponchik/locklib@feae2af13ef412d2b6d5595a7219a2eab80e4eee -
Branch / Tag:
refs/heads/main - Owner: https://github.com/pomponchik
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@feae2af13ef412d2b6d5595a7219a2eab80e4eee -
Trigger Event:
push
-
Statement type: