From f889e9e754f6f1b243e1fd94ae9efe1be4424d7e Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Wed, 15 Jul 2020 11:55:33 +0200 Subject: [PATCH 1/5] Add the spinlock mutex --- dpf.mk | 1 + src/CMakeLists.txt | 2 ++ src/sfizz/utility/SpinMutex.cpp | 43 +++++++++++++++++++++++++++ src/sfizz/utility/SpinMutex.h | 28 ++++++++++++++++++ tests/CMakeLists.txt | 1 + tests/ConcurrencyT.cpp | 51 +++++++++++++++++++++++++++++++++ 6 files changed, 126 insertions(+) create mode 100644 src/sfizz/utility/SpinMutex.cpp create mode 100644 src/sfizz/utility/SpinMutex.h create mode 100644 tests/ConcurrencyT.cpp diff --git a/dpf.mk b/dpf.mk index 772dd364..ba5d18c3 100644 --- a/dpf.mk +++ b/dpf.mk @@ -101,6 +101,7 @@ SFIZZ_SOURCES = \ src/sfizz/simd/HelpersAVX.cpp \ src/sfizz/Synth.cpp \ src/sfizz/Tuning.cpp \ + src/sfizz/utility/SpinMutex.cpp \ src/sfizz/Voice.cpp \ src/sfizz/Wavetables.cpp diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 8de6d8eb..23d35477 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -20,6 +20,8 @@ set (SFIZZ_HEADERS sfizz/Config.h sfizz/Curve.h sfizz/Debug.h + sfizz/utility/SpinMutex.h + sfizz/utility/SpinMutex.cpp sfizz/effects/impl/ResonantArray.h sfizz/effects/impl/ResonantArrayAVX.h sfizz/effects/impl/ResonantArraySSE.h diff --git a/src/sfizz/utility/SpinMutex.cpp b/src/sfizz/utility/SpinMutex.cpp new file mode 100644 index 00000000..cb416081 --- /dev/null +++ b/src/sfizz/utility/SpinMutex.cpp @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: BSD-2-Clause + +// This code is part of the sfizz library and is licensed under a BSD 2-clause +// license. You should have receive a LICENSE.md file along with the code. +// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz + +#include "SpinMutex.h" +#include +#include + +// based on Timur Doumler's implementation advice for spinlocks + +void SpinMutex::lock() noexcept +{ + for (int i = 0; i < 5; ++i) { + if (try_lock()) + return; + } + + for (int i = 0; i < 10; ++i) { + if (try_lock()) + return; + atomic_queue::spin_loop_pause(); + } + + for (;;) { + for (int i = 0; i < 3000; ++i) { + if (try_lock()) + return; + atomic_queue::spin_loop_pause(); + atomic_queue::spin_loop_pause(); + atomic_queue::spin_loop_pause(); + atomic_queue::spin_loop_pause(); + atomic_queue::spin_loop_pause(); + atomic_queue::spin_loop_pause(); + atomic_queue::spin_loop_pause(); + atomic_queue::spin_loop_pause(); + atomic_queue::spin_loop_pause(); + atomic_queue::spin_loop_pause(); + } + std::this_thread::yield(); + } +} diff --git a/src/sfizz/utility/SpinMutex.h b/src/sfizz/utility/SpinMutex.h new file mode 100644 index 00000000..5e1aa347 --- /dev/null +++ b/src/sfizz/utility/SpinMutex.h @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: BSD-2-Clause + +// This code is part of the sfizz library and is licensed under a BSD 2-clause +// license. You should have receive a LICENSE.md file along with the code. +// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz + +#pragma once +#include + +class SpinMutex { +public: + void lock() noexcept; + bool try_lock() noexcept; + void unlock() noexcept; + +private: + std::atomic_flag flag_ = ATOMIC_FLAG_INIT; +}; + +inline bool SpinMutex::try_lock() noexcept +{ + return !flag_.test_and_set(std::memory_order_acquire); +} + +inline void SpinMutex::unlock() noexcept +{ + flag_.clear(std::memory_order_release); +} diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 31e134c8..21bc72d1 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -35,6 +35,7 @@ set(SFIZZ_TEST_SOURCES SemaphoreT.cpp SwapAndPopT.cpp TuningT.cpp + ConcurrencyT.cpp ) add_executable(sfizz_tests ${SFIZZ_TEST_SOURCES}) diff --git a/tests/ConcurrencyT.cpp b/tests/ConcurrencyT.cpp new file mode 100644 index 00000000..9ec72cd5 --- /dev/null +++ b/tests/ConcurrencyT.cpp @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: BSD-2-Clause + +// This code is part of the sfizz library and is licensed under a BSD 2-clause +// license. You should have receive a LICENSE.md file along with the code. +// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz + +#include "sfizz/utility/SpinMutex.h" +#include "catch2/catch.hpp" +#include +#include +#include + +TEST_CASE("[SpinMutex] Basic synchronization") +{ + constexpr size_t num_threads = 8; + constexpr size_t num_iterations = 1000000; + + volatile size_t counter = 0; + SpinMutex counter_mutex; + + std::thread threads[num_threads]; + + volatile bool ready = false; + std::condition_variable ready_cv; + std::mutex ready_mtx; + + auto thread_run = [&]() + { + std::unique_lock lock(ready_mtx); + ready_cv.wait(lock, [&]() -> bool { return ready; }); + lock.unlock(); + + for (size_t i = 0; i < num_iterations; ++i) { + std::unique_lock lock(counter_mutex); + ++counter; + } + }; + + for (unsigned i = 0; i < num_threads; ++i) + threads[i] = std::thread(thread_run); + + std::unique_lock lock(ready_mtx); + ready = true; + ready_cv.notify_all(); + lock.unlock(); + + for (unsigned i = 0; i < num_threads; ++i) + threads[i].join(); + + REQUIRE(counter == num_threads * num_iterations); +} From 4c1de84c0f15f6c1ce3406b9962f798f7786332e Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Tue, 4 Aug 2020 19:31:52 +0200 Subject: [PATCH 2/5] Replace the callback mutex with a spin-lock --- src/sfizz/Synth.cpp | 32 ++++++++++++++++---------------- src/sfizz/Synth.h | 4 ++-- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index c2c7779c..59ca5957 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -30,7 +30,7 @@ sfz::Synth::Synth() sfz::Synth::Synth(int numVoices) { - const std::lock_guard disableCallback { callbackGuard }; + const std::lock_guard disableCallback { callbackGuard }; parser.setListener(this); effectFactory.registerStandardEffectTypes(); effectBuses.reserve(5); // sufficient room for main and fx1-4 @@ -39,7 +39,7 @@ sfz::Synth::Synth(int numVoices) sfz::Synth::~Synth() { - const std::lock_guard disableCallback { callbackGuard }; + const std::lock_guard disableCallback { callbackGuard }; for (auto& voice : voices) voice->reset(); @@ -166,7 +166,7 @@ void sfz::Synth::buildRegion(const std::vector& regionOpcodes) void sfz::Synth::clear() { - const std::lock_guard disableCallback { callbackGuard }; + const std::lock_guard disableCallback { callbackGuard }; for (auto& voice : voices) voice->reset(); @@ -396,7 +396,7 @@ bool sfz::Synth::loadSfzFile(const fs::path& file) { clear(); - const std::lock_guard disableCallback { callbackGuard }; + const std::lock_guard disableCallback { callbackGuard }; std::error_code ec; fs::path realFile = fs::canonical(file, ec); @@ -417,7 +417,7 @@ bool sfz::Synth::loadSfzString(const fs::path& path, absl::string_view text) { clear(); - const std::lock_guard disableCallback { callbackGuard }; + const std::lock_guard disableCallback { callbackGuard }; parser.parseString(path, text); if (parser.getErrorCount() > 0) return false; @@ -643,7 +643,7 @@ void sfz::Synth::setSamplesPerBlock(int samplesPerBlock) noexcept { ASSERT(samplesPerBlock < config::maxBlockSize); - const std::lock_guard disableCallback { callbackGuard }; + const std::lock_guard disableCallback { callbackGuard }; this->samplesPerBlock = samplesPerBlock; for (auto& voice : voices) @@ -659,7 +659,7 @@ void sfz::Synth::setSamplesPerBlock(int samplesPerBlock) noexcept void sfz::Synth::setSampleRate(float sampleRate) noexcept { - const std::lock_guard disableCallback { callbackGuard }; + const std::lock_guard disableCallback { callbackGuard }; this->sampleRate = sampleRate; for (auto& voice : voices) @@ -698,7 +698,7 @@ void sfz::Synth::renderBlock(AudioSpan buffer) noexcept if (resources.synthConfig.freeWheeling) resources.filePool.waitForBackgroundLoading(); - const std::unique_lock lock { callbackGuard, std::try_to_lock }; + const std::unique_lock lock { callbackGuard, std::try_to_lock }; if (!lock.owns_lock()) return; @@ -797,7 +797,7 @@ void sfz::Synth::noteOn(int delay, int noteNumber, uint8_t velocity) noexcept ScopedTiming logger { dispatchDuration, ScopedTiming::Operation::addToDuration }; resources.midiState.noteOnEvent(delay, noteNumber, normalizedVelocity); - const std::unique_lock lock { callbackGuard, std::try_to_lock }; + const std::unique_lock lock { callbackGuard, std::try_to_lock }; if (!lock.owns_lock()) return; @@ -813,7 +813,7 @@ void sfz::Synth::noteOff(int delay, int noteNumber, uint8_t velocity) noexcept ScopedTiming logger { dispatchDuration, ScopedTiming::Operation::addToDuration }; resources.midiState.noteOffEvent(delay, noteNumber, normalizedVelocity); - const std::unique_lock lock { callbackGuard, std::try_to_lock }; + const std::unique_lock lock { callbackGuard, std::try_to_lock }; if (!lock.owns_lock()) return; @@ -966,7 +966,7 @@ void sfz::Synth::hdcc(int delay, int ccNumber, float normValue) noexcept ScopedTiming logger { dispatchDuration, ScopedTiming::Operation::addToDuration }; resources.midiState.ccEvent(delay, ccNumber, normValue); - const std::unique_lock lock { callbackGuard, std::try_to_lock }; + const std::unique_lock lock { callbackGuard, std::try_to_lock }; if (!lock.owns_lock()) return; @@ -1277,7 +1277,7 @@ int sfz::Synth::getNumVoices() const noexcept void sfz::Synth::setNumVoices(int numVoices) noexcept { ASSERT(numVoices > 0); - const std::lock_guard disableCallback { callbackGuard }; + const std::lock_guard disableCallback { callbackGuard }; // fast path if (numVoices == this->numVoices) @@ -1325,7 +1325,7 @@ void sfz::Synth::applySettingsPerVoice() void sfz::Synth::setOversamplingFactor(sfz::Oversampling factor) noexcept { - const std::lock_guard disableCallback { callbackGuard }; + const std::lock_guard disableCallback { callbackGuard }; // fast path if (factor == oversamplingFactor) @@ -1348,7 +1348,7 @@ sfz::Oversampling sfz::Synth::getOversamplingFactor() const noexcept void sfz::Synth::setPreloadSize(uint32_t preloadSize) noexcept { - const std::lock_guard disableCallback { callbackGuard }; + const std::lock_guard disableCallback { callbackGuard }; // fast path if (preloadSize == resources.filePool.getPreloadSize()) @@ -1381,7 +1381,7 @@ void sfz::Synth::resetAllControllers(int delay) noexcept { resources.midiState.resetAllControllers(delay); - const std::unique_lock lock { callbackGuard, std::try_to_lock }; + const std::unique_lock lock { callbackGuard, std::try_to_lock }; if (!lock.owns_lock()) return; @@ -1436,7 +1436,7 @@ void sfz::Synth::disableLogging() noexcept void sfz::Synth::allSoundOff() noexcept { - const std::lock_guard disableCallback { callbackGuard }; + const std::lock_guard disableCallback { callbackGuard }; for (auto& voice : voices) voice->reset(); diff --git a/src/sfizz/Synth.h b/src/sfizz/Synth.h index bcd79542..58d335ad 100644 --- a/src/sfizz/Synth.h +++ b/src/sfizz/Synth.h @@ -17,10 +17,10 @@ #include "AudioSpan.h" #include "parser/Parser.h" #include "VoiceStealing.h" +#include "utility/SpinMutex.h" #include "absl/types/span.h" #include #include -#include #include #include #include @@ -736,7 +736,7 @@ private: // Distribution used to generate random value for the *rand opcodes std::uniform_real_distribution randNoteDistribution { 0, 1 }; - std::mutex callbackGuard; + SpinMutex callbackGuard; // Singletons passed as references to the voices Resources resources; From 5928218b62903528993e0f0080de63eb5a086a96 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Thu, 6 Aug 2020 16:44:39 +0200 Subject: [PATCH 3/5] Use the spinlock mutex in FilePool --- src/sfizz/FilePool.cpp | 4 ++-- src/sfizz/FilePool.h | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/sfizz/FilePool.cpp b/src/sfizz/FilePool.cpp index 9bec98ef..8c474e9f 100644 --- a/src/sfizz/FilePool.cpp +++ b/src/sfizz/FilePool.cpp @@ -361,7 +361,7 @@ void sfz::FilePool::setPreloadSize(uint32_t preloadSize) noexcept void sfz::FilePool::tryToClearPromises() { - const std::lock_guard promiseLock { promiseGuard }; + const std::lock_guard promiseLock { promiseGuard }; for (auto& promise: promisesToClear) { if (promise->dataStatus != FilePromise::DataStatus::Wait) @@ -442,7 +442,7 @@ void sfz::FilePool::clear() void sfz::FilePool::cleanupPromises() noexcept { - const std::unique_lock lock { promiseGuard, std::try_to_lock }; + const std::unique_lock lock { promiseGuard, std::try_to_lock }; if (!lock.owns_lock()) return; diff --git a/src/sfizz/FilePool.h b/src/sfizz/FilePool.h index 2547591f..cbec5f8f 100644 --- a/src/sfizz/FilePool.h +++ b/src/sfizz/FilePool.h @@ -32,6 +32,7 @@ #include "AudioSpan.h" #include "FileId.h" #include "SIMDHelpers.h" +#include "utility/SpinMutex.h" #include "ghc/fs_std.hpp" #include #include @@ -288,7 +289,7 @@ private: std::vector emptyPromises; std::vector temporaryFilePromises; std::vector promisesToClear; - std::mutex promiseGuard; + SpinMutex promiseGuard; // Preloaded data absl::flat_hash_map preloadedFiles; From 76654d56eacd16c49c03c39f5d84c54c9a94eef4 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Thu, 6 Aug 2020 16:45:56 +0200 Subject: [PATCH 4/5] Use the spinlock mutex in FilterPool --- src/sfizz/FilterPool.cpp | 4 ++-- src/sfizz/FilterPool.h | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/sfizz/FilterPool.cpp b/src/sfizz/FilterPool.cpp index 164b3ea8..70811320 100644 --- a/src/sfizz/FilterPool.cpp +++ b/src/sfizz/FilterPool.cpp @@ -109,7 +109,7 @@ sfz::FilterPool::FilterPool(const MidiState& state, int numFilters) sfz::FilterHolderPtr sfz::FilterPool::getFilter(const FilterDescription& description, unsigned numChannels, int noteNumber, float velocity) { - const std::unique_lock lock { filterGuard, std::try_to_lock }; + const std::unique_lock lock { filterGuard, std::try_to_lock }; if (!lock.owns_lock()) return {}; @@ -133,7 +133,7 @@ size_t sfz::FilterPool::getActiveFilters() const size_t sfz::FilterPool::setNumFilters(size_t numFilters) { - const std::lock_guard filterLock { filterGuard }; + const std::lock_guard filterLock { filterGuard }; swapAndPopAll(filters, [](sfz::FilterHolderPtr& filter) { return filter.use_count() == 1; }); diff --git a/src/sfizz/FilterPool.h b/src/sfizz/FilterPool.h index 36db8f51..197eb8ad 100644 --- a/src/sfizz/FilterPool.h +++ b/src/sfizz/FilterPool.h @@ -3,6 +3,7 @@ #include "FilterDescription.h" #include "MidiState.h" #include "Defaults.h" +#include "utility/SpinMutex.h" #include #include #include @@ -120,7 +121,7 @@ public: */ void setSampleRate(float sampleRate); private: - std::mutex filterGuard; + SpinMutex filterGuard; float sampleRate { config::defaultSampleRate }; const MidiState& midiState; std::vector filters; From f3e37cc713368b2b30bd7d9fdacaac6b188c017a Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Thu, 6 Aug 2020 16:46:56 +0200 Subject: [PATCH 5/5] Use the spinlock mutex in EQPool --- src/sfizz/EQPool.cpp | 4 ++-- src/sfizz/EQPool.h | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/sfizz/EQPool.cpp b/src/sfizz/EQPool.cpp index 6d3d75b2..cc608a12 100644 --- a/src/sfizz/EQPool.cpp +++ b/src/sfizz/EQPool.cpp @@ -108,7 +108,7 @@ sfz::EQPool::EQPool(const MidiState& state, int numEQs) sfz::EQHolderPtr sfz::EQPool::getEQ(const EQDescription& description, unsigned numChannels, float velocity) { - const std::unique_lock lock { eqGuard, std::try_to_lock }; + const std::unique_lock lock { eqGuard, std::try_to_lock }; if (!lock.owns_lock()) return {}; @@ -132,7 +132,7 @@ size_t sfz::EQPool::getActiveEQs() const size_t sfz::EQPool::setnumEQs(size_t numEQs) { - const std::lock_guard eqLock { eqGuard }; + const std::lock_guard eqLock { eqGuard }; swapAndPopAll(eqs, [](sfz::EQHolderPtr& eq) { return eq.use_count() == 1; }); diff --git a/src/sfizz/EQPool.h b/src/sfizz/EQPool.h index 7c70af22..d1f7cebb 100644 --- a/src/sfizz/EQPool.h +++ b/src/sfizz/EQPool.h @@ -2,6 +2,7 @@ #include "SfzFilter.h" #include "EQDescription.h" #include "MidiState.h" +#include "utility/SpinMutex.h" #include #include #include @@ -115,7 +116,7 @@ public: */ void setSampleRate(float sampleRate); private: - std::mutex eqGuard; + SpinMutex eqGuard; float sampleRate { config::defaultSampleRate }; const MidiState& midiState; std::vector eqs;