Concurrency and Arrays


#1

I’ve an Array of “modules”, each module has his own processBlock method.
The end user can change the order, add and remove modules.

Ok, in my plugin AudioProcessor as member:
Array <std::shared_ptr <Module>> modules;

then in the AudioProcessor::ProcessBlock:

    for (int i = 0; i < modules.size(); ++i)
    {
        std::shared_ptr<Module> module = std::atomic_load (&modules[i]);
        module->processBlock (buffer);
    }

So, the only way is to use a lock to add or move items in the array<?>

PS: Modules are stored in a ReleasePool, but if I use locks, I believe that no longer make sense.
Here is the release Pool:

template <typename objType>
class ReleasePool : private Timer
{
public:
    ReleasePool () { startTimer (5000); };

    void add (const std::shared_ptr<objType>& object)
    {
        if (object == nullptr)
            return;
    
        std::lock_guard<std::mutex> lock (m);
        pool.emplace_back (object);
    };
    
private:

void timerCallback() override
{
    std::lock_guard<std::mutex> lock (m);

    pool.erase (
        std::remove_if (
            pool.begin(),
            pool.end(),
            [] (std::shared_ptr<objType>& object) { return object.use_count() <= 1; }
        ),
        pool.end());

};

std::vector<std::shared_ptr<objType>> pool;
std::mutex m;

JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ReleasePool)
};

Any help is welcome
Thanks


#2

So the release pool is like a garbage collector, working on the message thread on a timer.

Are you asking if a lock is required for changing the modules array? I think it depends on how you are updating it and from which thread. But presumably you need a non-blocking solution (so the audio doesn’t stop) for changing it when the user makes changes?


#3

Yes.

Yes, (sorry for my bad english) I’m asking if locks are only way to use dynamic size arrays.
That I want is to use a lock-free dynamic size array.

For example, the array has 9 items.
thread A is running:

for (int i = 0; i < modules.size(); ++i)
{
    std::shared_ptr<Module> module = std::atomic_load (&modules[i]);
    module->processBlock (buffer);
}

While thread A is running, thread B removes two items from the array

So, the for loop in thread A, “suppose” that modules.size() is 9, but there are 7 items because while A was running, thread B had removed two items.

Ok, with locks I can solve that problem, but my question is if there is a lock-free way.

Another way is to use a fixed size array:

std::shared_ptr<Module> modules[MAX_NUM_MODULES];

and check if every item is nullptr, I think this will work:

for (int i = 0; i < MAX_NUM_MODULES; ++i)
{
    std::shared_ptr<Module> module = std::atomic_load (&modules[i]);
    
    if (module != nullptr)
        module->processBlock (&buffer);
}

But I want to use dynamic size arrays.
Thanks


#4

As with most other problems in computer science, you can solve it with another layer of indirection. If you always load your array through a pointer, you can swap the array the pointer is pointing at. Doing it this way is completely lock-free and you don’t even have to use shared pointers.

However, it will only work in a SPSC model. As for the problem of deleting the memory, it can be proven that its safe to delete an old object IFF the new object has been used by the consumer.

Here’s an example implementation I use:
https://bitbucket.org/Mayae/cpl/src/master/ConcurrentServices.h


Controlling IIRFilters with a slider
#5

Hi Mayae!

I think it’s a good solution
Thank you! :slight_smile: