AbstractFifo + Thread Sanitizer + VST3 + AudioPluginHost = Sad Panda

I’m using a background thread to create a Path based on some audio data, and am passing that Path to the GUI thread via a Fifo built on AbstractFifo. When I run as Standalone with Thread Sanitizer turned on, there are no data races. When I run as VST3 in Audio Plugin Host, I get lots of Data Races, centered around the Path::operator= and Path(const Path& other) functions:
12%20PM

I’ve created a PIP demonstrating the behavior:
FifoPIP.h (7.6 KB)

Does anyone have any idea where to even begin trying to solve this? @reuk was kind enough to put together a simple program checking out my PathFifo to see if that was problem, and it had no data races:

#include "../JuceLibraryCode/JuceHeader.h"

#include <array>
#include <future>

struct PathFifo final {
  bool push(Path const &pathToClone) {
    auto write = fifo.write(1);

    if (write.blockSize1 >= 1) {
      buffers[write.startIndex1] = pathToClone;
      return true;
    }

    return false;
  }

  bool pull(Path &pathToFill) {
    auto read = fifo.read(1);

    if (read.blockSize1 >= 1) {
      pathToFill = buffers[read.startIndex1];
      return true;
    }

    return false;
  }

private:
  static constexpr int Capacity = 30;
  std::array<Path, Capacity> buffers;
  AbstractFifo fifo{Capacity};
};

int main(int argc, char *argv[]) {
  std::promise<void> promise;
  PathFifo fifo;

  auto const writer =
      std::async(std::launch::async, [&fifo, fut = promise.get_future()] {
        while (fut.wait_for(std::chrono::seconds{0}) !=
               std::future_status::ready) {
          Path p;
          p.startNewSubPath(rand(), rand());
          p.lineTo(rand(), rand());
          p.lineTo(rand(), rand());
          p.closeSubPath();
          fifo.push(std::move(p));
        }
      });

  Path pulled;

  for (auto i = 0; i != 1000000; ++i)
    fifo.pull(pulled);

  promise.set_value();

  return 0;
}

I’m at a loss for what could be causing this, if it’s a bug in AudioPluginHost, or AbstractFifo or what…

Do you have multiple threads at either end of the FIFO? The increments aren’t implemented atomically for AbstractFifo, and that might cause some weirdness in your constructors/assignment operators when you push/pull from it.

Wait.

AbstractFifo’s private position holders are:

Atomic<int> validStart, validEnd;

are you referring to these implementations not being atomic?

void AbstractFifo::finishedWrite (int numWritten) noexcept
{
    jassert (numWritten >= 0 && numWritten < bufferSize);

    auto newEnd = validEnd.get() + numWritten;

    if (newEnd >= bufferSize)
        newEnd -= bufferSize;

    validEnd = newEnd;
}
void AbstractFifo::finishedRead (int numRead) noexcept
{
    jassert (numRead >= 0 && numRead <= bufferSize);

    auto newStart = validStart.get() + numRead;

    if (newStart >= bufferSize)
        newStart -= bufferSize;

    validStart = newStart;
}

regarding threads, I have the Background thread calling PathFifo::push and the GUI thread is calling PathFifo::pull. that’s all that is happening in the PIP file.

FWIW I can’t repro the races when I load the vst3 in the plugin host after building both with tsan under Xcode 10.3.

Yeah, @Holy_City said the same thing. I’m using Xcode 10.1 on OS X 10.13.6. Maybe they are false positives?

I can’t reproduce it either. False positives are possible.

Do any of you have a machine not running the latest Xcode that you could try on?

When Tsan identifies an issue you you should be able to see two stack traces of the race access in question (might be easier if you enable “Pause on Issues”).

Can you post these?

I know. I sent them to @t0m but like he and @Holy_City said, they’re on newer versions of xcode/OS X and can’t reproduce.

Here are the stack traces though: