Hi,
There are some (forum
) threads about it but with broken links.
What are your approaches to de/allocate memory for samples in the background?
Any recommendations for a one consumer-one producer lock free queue without having to integrate boost?
One-Consumer, One-Producer can be achieved easily with the AbstractFifo class. It doesn’t actually act as the container. It just implements the algorithm to determine the read/write save indices into a container of your choice.
The container should have a fast index-based access, so linked list is probably an inconvenient choice.
Thank you rince. That’s a good idea. Silly me, I hadn’t thought of AbstractFifo because I was only planning to add/process one lambda-job at a time but it is definitely a good and simple way! A std::deque or std:: vector are both probably good choices to store the lambda-jobs right?
std::vector should be fine. But with lambdas you have a different problem. It is most likely allocating on creation and copy, and even if it doesn’t, I don’t believe there is a way to do a static assert. However, JUCE has a FixedSizedFunction class which would be perfect for this case. If your producer is not the realtime thread (probably not the case when scheduling sample loading) you can safely use std::function.
The function object itself will always be the same size, as it’s only (if at all) determined by the template args. But that doesn’t mean those 64 bytes are the only memory that it needs. It will allocate further memory if necessary. How much and under which circumstances is up to the implementation. There’s not really a good way to always make sure that handling your std::function objects doesn’t allocate/deallocate.
juce::FixedSizeFunction fixes this by replacing the dynamic allocation with a static one, where you set the amount of memory reserved for each object. If it’s too small and you have a lambda with many large captures, it might not fit, forcing you to reserve more. Then you’ll use more memory than necessary for other smaller lambdas. But in practice, that should work. And it looks like the compiler will tell you when things don’t fit.
Here’s the doc:
https://docs.juce.com/master/classFixedSizeFunction_3_01len_00_01Ret_07Args_8_8_8_08_4.html
One trap’s still remaining: don’t capture objects by value that might allocate on copy!
FixedSizeFunction doesn’t seem to exist anymore in JUCE7
It makes sense that std::function might reserve more memory if needed and we are the mercy of its implementation anyway.
I don’t think you are looking hard enough. If it is documented, it should still be in JUCE 7. You should make sure, that you are actually replicating a compiler error with the use of FixedSizeFunction and a large lambda, so you know your compiler guards are working.
It’s for sure in the latest develop branch
JUCE/modules/juce_core/containers/juce_FixedSizeFunction.h at master · juce-framework/JUCE · GitHub
Strangely I found it in juce_dsp
(using JUCE 7.0.3)
Seems it got moved into the core module a few months ago History for modules/juce_core/containers/juce_FixedSizeFunction.h - juce-framework/JUCE · GitHub
LGTM, last would be to try to add a lambda, that doesn’t fit. I.e. create a struct with sizeof(T) > 64 and try to capture that struct by value. If the compiler does not strike an error, we would need to rethink the use of FixedSizeFunction (probably with the JUCE team?)
EDIT: would be great if you can also share that assertion / test code, so someone else would now have the somewhat perfect tutorial on how to use FixedSizeFunction ![]()
The problem is that in my system (VS2022, Windows 10) std::function<void() > is always 64bytes no matter how many stuff I put in the capture of the lambda passed to it.
Then you probably need to try passing the FixedSizeFunction also as a parameter.
That is a good idea to make the compile error clearer.
In my system (VS2022, Windows 10) std::function still remains 64 no matter what I put in the capture though.
int a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,v;
auto lambda = [a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,v]() { std::vector<int> b(2000); logDebug(b[0]); };
logDebug(sizeof(lambda),"lambda size");
std::function<void()> func=lambda;
logDebug(sizeof(func),"function size");
Output:
lambda size = 84
function size = 64
std::function always stays at 64, because it calls malloc and just stores the pointer to the allocated memory fitting the arbitrary sized lambda. But that is what you are trying to avoid.
Are you sure that std::function calls malloc?
If you pass the lambda (that is 84 bytes big) by copy to std::function, std::function is for sure calling malloc and storing the pointer to that memory inside it’s 64 byte storage. If the lambda is small enough, it may not call malloc but use that space to actually store the data (similar to what std::string might do, if the string is small enough), but in your example it is for sure calling malloc.
(Maybe the compiler in this exact case is smart enough to figure something out without malloc. With all the latest additions of constexpr and what not, that might be the case. But this is the thing that you cannot assert and really don’t want in your real time code. You can set a break point inside malloc and know for sure.)
I confirm. I just have debugged this example and it allocates it. Here’s the final code correcting this:
/*
Example of use:
void processBlock(AudioSampleBuffer& buffer, MidiBuffer& midiMessages)
{
if (getPlayHead() && getPlayHead()->getPosition()
&& getPlayHead()->getPosition()->getTimeInSamples()
&& *(getPlayHead()->getPosition()->getTimeInSamples())==0)
tp.addJob([]{ Logger::outputDebugString("test function"); });
}
OneProducerLockFreeThreadPool tp;
Important:
* jobs can only be added from the same thread (one producer)
* increase JOBSIZE if you call addJob with lambdas with big captures and get compile errors
*/
template <int JOBSIZE = 64>
class OneProducerLockFreeThreadPool : public Thread
{
public:
OneProducerLockFreeThreadPool(int maxAccumulatedJobs = 1000)
: Thread("")
, abstractFifo(maxAccumulatedJobs)
, jobs(maxAccumulatedJobs)
{
start();
}
~OneProducerLockFreeThreadPool()
{
stop();
}
void addJob(dsp::FixedSizeFunction<JOBSIZE,void()> &&job)
{
int startIndex1, blockSize1, startIndex2, blockSize2;
abstractFifo.prepareToWrite(1, startIndex1, blockSize1, startIndex2, blockSize2);
if (blockSize1 + blockSize2 == 1)
{
const int writeIndex = (blockSize1 > 0)?startIndex1:startIndex2;
jobs[writeIndex] = std::forward<dsp::FixedSizeFunction<JOBSIZE,void()> >(job);
abstractFifo.finishedWrite(1);
}
else
{
// the pool can't hold as many jobs
// increase maxAccumulatedJobs
jassertfalse;
}
}
void start()
{
loopJobs.test_and_set();
startThread(Priority::normal);
}
void stop(int timeOutMilliseconds = 100)
{
loopJobs.clear();
stopThread(timeOutMilliseconds);
}
private:
void run() override
{
while (loopJobs.test())
{
int startIndex1, blockSize1, startIndex2, blockSize2;
abstractFifo.prepareToRead(1, startIndex1, blockSize1, startIndex2, blockSize2);
if (blockSize1 + blockSize2 == 1)
{
const int readIndex = (blockSize1 > 0)?startIndex1:startIndex2;
jobs[readIndex]();
abstractFifo.finishedRead (1);
}
}
}
AbstractFifo abstractFifo;
std::vector<dsp::FixedSizeFunction<JOBSIZE,void()> > jobs;
std::atomic_flag loopJobs;
};

