Avoid deadlocks and nondeterministic results when using the same AudioFile in multiple threads. #298
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
This one's a big one. Sorry in advance.
Prior to this PR, Pedalboard allowed multiple threads to call methods on
AudioFile
simultaneously.AudioFile
objects included anobjectLock
, intended to serialize this access to ensure "thread safety" (although this was vacuous, as how meaningful are the results returned by a file-like object being manipulated by multiple threads simultaneously?).This was not a problem when reading files from disk. However,
AudioFile
permits the caller to provide a file-like object (io.BytesIO
, etc) which can be implemented in Python. In this case, concurrent access to the sameAudioFile
object caused hard deadlocks in Python.Consider the following example, in which the thread holding the GIL is annotated with 🐟, and the thread holding the
AudioFile
object's lock is annotated with 🔒:AudioFile.read(...)
AudioFile
'sobjectLock 🔒
AudioFile.read(...)
AudioFile
'sobjectLock 🔒
.read(...)
on the file-like objectThis situation resulted in an uninterruptible Python interpreter (i.e.:
Ctrl-C
would not work), as the GIL was held by a thread that was in C++ code, and no Python signal handlers could run.This PR makes significant changes to how
AudioFile
handles locking:AudioFile
objects now have read-write locks, instead of instance-wide locks. This allows us to have finer-grained control over locking and - say - avoid locking the entire object if two threads do want to readconst
properties of the object simultaneously.AudioFile
objects now use a helper class,ScopedDowngradeToReadLockWithGIL
, which:AudioFile
object concurrently, Pedalboard can now detect this and throws an error, so that users will be alerted that the results of eachread()
call would have been non-deterministic:This PR also includes some new and comprehensive testing around locking; however, these tests are beasts. Again, sorry for the complexity there.