Atomic swap resources onto the audio thread

There is lots of discussion here about how to get resources onto the audio thread without using locks. One solution that comes up quite often is to use immutable resouces and then rebuild and swap in a new one atomically after every change. I.e.

(Mentioned also here, on the sixth paragraph).

I would like to try this approach, but I can’t find much guidance about how to do it properly. Is std::atomic::exchange<T*> the right way to do it? Or should I use shared_ptr instead of raw pointers? And how can I work out a safe solution for deleting the old resource? Any advice or working examples would be helpful.

One of the most common ways is to just pass a pointer in an atomic (like you’ve got there) but make sure you don’t let it dangle. You need to take ownership back when you swap in a new one and delete the old one.

The main gotcha there is that you need some extra synchronisation to avoid swapping it out whilst it’s in use by the audio thread. Me and Fabian talked about (in “Realtime 101”) making it nullptr whilst it’s in use so the message thread can wait for it to be not null.


You can build layers on top of this where you effectively build two queues, one for passing objects to the audio thread and one for passing them back to be deleted by the audio thread.

How much abstraction you need really depends on your use case, primarily around is there only ever a single T in use by the audio thread or could there be many?

The use case is a sample manager with n number of AudioSampleBuffers loaded. But as I said, I was thinking of just rebuilding the whole thing every time, so yes, only one single instance. Maybe I could get away with your first solution, just swapping the pointer when it’s not in use on the audio thread?

Generally for lock-free stuff, the simpler the better. It gets really complicated really quickly.

2 Likes

How’s this for simple? It’s a lock free Fifo that pretends to be a single object wrapper with get() and set() methods. No smart pointers, and destructors are only called with set().

template <typename T, int SIZE>
class ThreadSafeSetter
{
public:
    static_assert(SIZE > 2);

    ThreadSafeSetter() : ThreadSafeSetter({}) {}

    ThreadSafeSetter(T&& init) :
        fifo(SIZE)
    {
        set(std::move(init));
    }

    bool set(T&& t)
    {
        int start1, size1, start2, size2;
        fifo.prepareToWrite(1, start1, size1, start2, size2);

        if (size1 > 0)
        {
            internalArray[start1] = std::move(t);
            fifo.finishedWrite(1);
            return true;
        }

        else if (size2 > 2)
        {
            internalArray[start2] = std::move(t);
            fifo.finishedWrite(1);
            return false;
        }
        else return false;
    }

    const T& get() const
    {
        const auto ready = fifo.getNumReady();
        if (ready > 1)
            fifo.finishedRead(ready - 1);

        int start1, size1, start2, size2;

        fifo.prepareToRead(1, start1, size1, start2, size2);

        return internalArray[size1 ? start1 : start2];

        // putting finishedRead() before prepareToRead() looks strange I know,
        // but the point is to increment the read head iff the fifo has been written to
    }

private:
    mutable juce::AbstractFifo fifo;
    std::array<T, SIZE> internalArray;
};

Only caveat is that the resource has to be default constructable. And the write method might fail, but you can always increase the array size to diminish the chance of this.

Simple test function:

void testIt()
{
    class Resource
    {
    public:
        int i = 0;
    };


    auto t = std::make_shared<ThreadSafeSetter<Resource, 10>>();

    const auto setter = std::async([t]()
        {
            for (int i = 0; i < 1000; ++i)
            {
                auto r = Resource();
                r.i = i;
                t->set(std::move(r));
                std::this_thread::sleep_for(std::chrono::milliseconds(1));
            }
        });

    const auto getter = std::async([t]()
        {
            for (int i = 0; i < 10000; ++i)
            {
                DBG(t->get().i);
            }
        });

    setter.wait();
    getter.wait();
    DBG("done!");
}

Edit: I removed the RAII scope class, so if you looked at this already, it’s even simpler than it was.