Browse Source

Add Engine::yieldWorker() which turns worker spinlocks into mutex locks. Fix race condition in EngineWorker::run() when changing number of threads.

tags/v1.0.0
Andrew Belt 5 years ago
parent
commit
30c5b24ec5
5 changed files with 67 additions and 14 deletions
  1. +3
    -0
      CHANGELOG.md
  2. +4
    -0
      include/engine/Engine.hpp
  3. +2
    -0
      src/Core/AudioInterface.cpp
  4. +1
    -1
      src/app/Scene.cpp
  5. +57
    -13
      src/engine/Engine.cpp

+ 3
- 0
CHANGELOG.md View File

@@ -28,6 +28,8 @@
- Fix draw order of cable plugs and wires - Fix draw order of cable plugs and wires
- Make Gamepad MIDI driver generate MIDI CC instead of MIDI notes for buttons - Make Gamepad MIDI driver generate MIDI CC instead of MIDI notes for buttons
- Fix Unicode user directories on Windows - Fix Unicode user directories on Windows
- Add ability to change cable colors in `settings.json`
- Add `-p X` flag for dumping a screenshot of each available module


- Core - Core
- Add Core CV-MIDI, CV-CC, and CV-Gate for sending MIDI to external devices - Add Core CV-MIDI, CV-CC, and CV-Gate for sending MIDI to external devices
@@ -35,6 +37,7 @@
- Add polyphony to Core MIDI-CV - Add polyphony to Core MIDI-CV
- Add MPE mode to Core MIDI-CV - Add MPE mode to Core MIDI-CV
- Add "Panic" button to all MIDI modules to reset performance state - Add "Panic" button to all MIDI modules to reset performance state
- Add Core Audio 16


- API - API
- Add [`simd.hpp`](include/dsp/simd.hpp) for generically handling arithmetic and math functions for vectors of floats, accelerated with SSE - Add [`simd.hpp`](include/dsp/simd.hpp) for generically handling arithmetic and math functions for vectors of floats, accelerated with SSE


+ 4
- 0
include/engine/Engine.hpp View File

@@ -25,6 +25,10 @@ struct Engine {
float getSampleRate(); float getSampleRate();
/** Returns the inverse of the current sample rate. */ /** Returns the inverse of the current sample rate. */
float getSampleTime(); float getSampleTime();
/** Causes worker threads to block on a mutex instead of spinlock.
Call this in your Module::step() method to hint that the operation will take more than ~0.1 ms.
*/
void yieldWorkers();


// Modules // Modules
/** Adds a module to the rack engine. /** Adds a module to the rack engine.


+ 2
- 0
src/Core/AudioInterface.cpp View File

@@ -132,6 +132,7 @@ struct AudioInterface : Module {
if (port.active && port.numInputs > 0) { if (port.active && port.numInputs > 0) {
// Wait until inputs are present // Wait until inputs are present
// Give up after a timeout in case the audio device is being unresponsive. // Give up after a timeout in case the audio device is being unresponsive.
APP->engine->yieldWorkers();
std::unique_lock<std::mutex> lock(port.engineMutex); std::unique_lock<std::mutex> lock(port.engineMutex);
auto cond = [&] { auto cond = [&] {
return (!port.inputBuffer.empty()); return (!port.inputBuffer.empty());
@@ -181,6 +182,7 @@ struct AudioInterface : Module {
if (outputBuffer.full()) { if (outputBuffer.full()) {
// Wait until enough outputs are consumed // Wait until enough outputs are consumed
// Give up after a timeout in case the audio device is being unresponsive. // Give up after a timeout in case the audio device is being unresponsive.
APP->engine->yieldWorkers();
std::unique_lock<std::mutex> lock(port.engineMutex); std::unique_lock<std::mutex> lock(port.engineMutex);
auto cond = [&] { auto cond = [&] {
return (port.outputBuffer.size() < (size_t) port.blockSize); return (port.outputBuffer.size() < (size_t) port.blockSize);


+ 1
- 1
src/app/Scene.cpp View File

@@ -148,7 +148,7 @@ void Scene::onHoverKey(const event::HoverKey &e) {
case GLFW_KEY_F11: { case GLFW_KEY_F11: {
APP->window->setFullScreen(!APP->window->isFullScreen()); APP->window->setFullScreen(!APP->window->isFullScreen());
e.consume(this); e.consume(this);
}
} break;
} }
} }
} }


+ 57
- 13
src/engine/Engine.cpp View File

@@ -96,6 +96,50 @@ struct SpinBarrier {
}; };




/** Spinlocks until all `total` threads are waiting.
If `yield` is set to true at any time, all threads will switch to waiting on a mutex instead.
All threads must return before beginning a new phase. Alternating between two barriers solves this problem.
*/
struct HybridBarrier {
std::atomic<int> count {0};
int total = 0;

std::mutex mutex;
std::condition_variable cv;

std::atomic<bool> yield {false};

void wait() {
int id = ++count;

// End and reset phase if this is the last thread
if (id == total) {
count = 0;
if (yield) {
std::unique_lock<std::mutex> lock(mutex);
cv.notify_all();
yield = false;
}
return;
}

// Spinlock
while (!yield) {
if (count == 0)
return;
}

// Wait on mutex
{
std::unique_lock<std::mutex> lock(mutex);
cv.wait(lock, [&]{
return count == 0;
});
}
}
};


struct EngineWorker { struct EngineWorker {
Engine *engine; Engine *engine;
int id; int id;
@@ -117,7 +161,6 @@ struct EngineWorker {
} }


void run(); void run();
void step();
}; };




@@ -146,8 +189,8 @@ struct Engine::Internal {
bool realTime = false; bool realTime = false;
int threadCount = 1; int threadCount = 1;
std::vector<EngineWorker> workers; std::vector<EngineWorker> workers;
SpinBarrier engineBarrier;
SpinBarrier workerBarrier;
HybridBarrier engineBarrier;
HybridBarrier workerBarrier;
std::atomic<int> workerModuleIndex; std::atomic<int> workerModuleIndex;
}; };


@@ -417,6 +460,10 @@ float Engine::getSampleTime() {
return internal->sampleTime; return internal->sampleTime;
} }


void Engine::yieldWorkers() {
internal->workerBarrier.yield = true;
}

void Engine::addModule(Module *module) { void Engine::addModule(Module *module) {
assert(module); assert(module);
VIPLock vipLock(internal->vipMutex); VIPLock vipLock(internal->vipMutex);
@@ -698,17 +745,14 @@ void EngineWorker::run() {
system::setThreadName("Engine worker"); system::setThreadName("Engine worker");
system::setThreadRealTime(engine->internal->realTime); system::setThreadRealTime(engine->internal->realTime);
disableDenormals(); disableDenormals();
while (running) {
step();
}
}


void EngineWorker::step() {
engine->internal->engineBarrier.wait();
if (!running)
return;
Engine_stepModules(engine, id);
engine->internal->workerBarrier.wait();
while (1) {
engine->internal->engineBarrier.wait();
if (!running)
return;
Engine_stepModules(engine, id);
engine->internal->workerBarrier.wait();
}
} }






Loading…
Cancel
Save