A lock-free, concurrent, generic queue in 32 bits
nullprogram.com
nullprogram.com
The mpmc variant looks non-serialising (threads push multiple things before another manages to push one) so it's not a queue, and it has a cas loop in it so lock-free is a bit of a stretch.
Many lock-free structures use CAS loops. It would not be lock-free if a consumer pre-empted at the wrong time could prevent other consumers to make progress, but I don't think this is the case here.
When I transitioned from trading to tech, I realized that HFT data structures have limited usefulness when you are not also doing HFT. You can do a lot more things lock-free when you don't have to worry about adverse scheduling.
By the way, there is no easy way to fix this with an MPMC ring buffer. You have to accept some chance of adverse behavior. There may be something complicated you can do, but it's easier to just split into multiple rings.
This is why newer versions of LMAX offer additional wait strategies beyond BusyWait. The one I prefer to use, unless latency is absolutely insanely critical, is the YieldingWait strategy.
If YieldingWait does a proper yield, by calling futex, well then it's not really lock free any more.
In rough terms, every push or pop from the queue also causes head or tail to be written, thus dirtying a line of cache. That’s the bit I misunderstood.
For the multiple-consumer variant, however, consistency between head and tail is required so packing them in the same atomic is the simplest solution.
To be fair, if that's your standard, there are virtually no lock-free algorithms. The multi-consumer has a wait-free retry loop. That's pretty much as good as it gets.
And the SPSC case is lock-free under even the strictest definitions, though of course that's not particularly difficult to achieve.
In fact the other side value is normally cached in a local memory location so to amortize the cost of roundtrips over multiple reads and writes.
Cmon AMD/Intel, give us hardware queues already :)
These do a bit more though, as they also allow batch stealing operations.
[1] https://github.com/golang/go/blob/master/src/runtime/proc.go...
[2] https://tokio.rs/blog/2019-10-scheduler#a-better-run-queue
And is the acquire ordering on push/pop and release on commit the right thing?
Also, my C skills are lacking but I recall that C's support for atomic data structures is a higher-level abstraction that could very well use locks under the hood.
C's support for atomics even provides a atomic_is_lock_free function which is used to check whether a data structure is lock-free or not.
https://en.cppreference.com/w/c/atomic/atomic_is_lock_free
Using ints with C's atomics does not ensure the atomic data type is lock-free. C even provides a macro to verify whether the primitive data type has lock-free access or not.
https://en.cppreference.com/w/c/atomic/ATOMIC_LOCK_FREE_cons...
Using C's atomic in this context to claim lock-free status sounds a lot like hand-waving over the problem domain to pretend locks aren't there. It's a cool read, but the underlying claim doesn't really hold.
Don't know if there is data to back that up, though.
So basically, writing a shared line is expensive. Atomic reads are free, as are atomic writes to a cache line that no one else is using.
- Less memory - a pthread mutex is around 40 bytes, compared to 4 bytes for this queue.
- No waiting - in some cases you don't want to wait, just do a try-push and if you can't, do some other work with the thread and then try again. Eventually you will have to either wait or discard the data, depending on your situation.
- No syscalls - mutexes weren't always as light as they are now, older implementations would always do a syscall and context switch to take or release a mutex.
That's an oxymoron.
This sort of thing is pretty ugly, and is one place where "portable" C and C++ really betrays the value of the language. Chances are many (often all) your uses are Linux, and so a futex is all you actually needed, which costs 4 bytes like this structure. But to be portable you can't ask for a futex, and it's deliberately not made easy to try to directly call into Linux system calls for futex since you'll probably get it wrong and then blame everybody else.
So you ask for pthread_mutex and now it's ten times bigger and you're regretting your life choices.
It's 36 bytes bigger.
Unless you're for some reason doing embedded development with a resource constrained system running a UNIX-like OS, that is not a concern that registers anywhere.
And you get standard code that runs anywhere.
Arguing about 36 bytes per critical section makes no sense. You waste far more than that by handling a string.
C puts atomic on the type, not the operation. That's conservative and occasionally very annoying (can't use normal stores in some parts of the program and atomic in others, thanks to aliasing rules) but doesn't hurt this particular application.
“The single producer and single consumer didn’t require locks nor atomic accesses to the storage array since the queue guaranteed that accesses at the specified index were not concurrent. However, this is not the case with multiple-consumers. Consumers race when popping. The loser’s access might occur after the winner’s commit, making its access concurrent with the producer. Both producer and consumers must account for this.”
Maybe TFA is for embedded use?
You can use the entire buffer and not waste one slot by doing:
push: buf[writeidx % bufsize] = x;
writeidx++;
pop: x = buf[readidx % bufsize];
readidx++;
Unsigned integers, you get size by doing: writeidx - readidx; Then you check size==0 or size==bufsize to detect empty/full.
Modular arithmetic makes it work. Try a few examples close to UINT_MAX if you want to convince yourself.
With non-power-2 bufsize you need some extra logic for write/readix++, but the rest of the code can stay the same.
If bufsize is a power of 2, doing the modulo would be exactly the same as just using a smaller integer type (like 8 bits on a microcontroller). What we want is the integer wraparound to never coincide with the buffer position wrapping. For that we would need a size that has some other factor than 2, and thus a "real" modulo operation in the indexing (which is slow).
Or is there an error in my thinking?
edit: Duh, I understand now. If the counter has at least twice the range as the buffer size, the subtraction will always give the correct number of elements used. Power of 2 or not doesn't matter.
Not having to do the subtraction could still give a performance benefit, and if the buffer elements aren't very large, one wasted slot is worth the tradeoff.
On a desktop/laptop computer, wasting a queue slot matters very little, but it may be useful to achieve the maximum speed, so this method does not seem preferable.
On the other hand, in many microcontroller applications there is no other read/write RAM, but a few kilobytes of internal MCU RAM. So the amount of used RAM may be the most important resource and there may be many FIFO queues for various peripheral interfaces.
For such MCU applications, it is valuable to be aware of this alternative FIFO queue implementation, as it may be the best choice.
Ring buffers work exactly the same way
Keep in mind that one of the reasons behind the arbitrary "power of two" requirement is that the method proposed by the author abuses a single atomic integer to hold two shorts representing the indices for the head and tail elements.
I don't think it makes much sense to talk about overhead given this method requires doing a bunch of bitmasks to operate over the high and low short.
Larger CPUs can do many such operations during every clock cycle and even in very small CPUs they are usually single-cycle operations.
Depending on the CPU, integer divisions are 10 to 100 times slower, so in a completely different range of overhead.
He has a companion post about a "magic" ring buffer that uses a virtual memory mapping to make a ring buffer where you can always address the entire sizes as contiguous memory, but I think it has a bit of minor bit rot in the linked implementations. https://fgiesen.wordpress.com/2012/07/21/the-magic-ring-buff...
Using two distinct 16bit _Atomic variables would have been an improvement, but I guess having both pointers in the same cacheline is going through be the bottleneck anyway.
Imagine I have a Gui where the user presses a button, upon which a message is sent to a queue, upon which a consumer reads it and performs some long-running task. Why should that queue be bounded?
If they are just batch background tasks (i.e. there is no expectation of seeing some immediate UI update as a response), then having a larger queue might be fine. There's not really that much reason to put a (small) limit on say the number of files to be downloaded in a queue. And having a really large queue bound is not that dissimilar from having an arbitrarily large queue (limited be memory of course), in terms of buffer bloat and added latency.
"Larger" is not good enough; you really want an unbounded queue (unbounded in theory of course), because otherwise the GUI thread might block at some point. Unless you want to spend a lot of effort on exception handling (at which point unbounded is still the best choice, see below).
Also, unnecessary restrictions in your program might cause problems in the future when your software is ported to a larger architecture, or when demands change.
Bounds let you have a `try_push` function that can obviously fail, whereas with unbounded you can only really know it failed because of OOM, or your producer thread gets blocked.
When `try_push` failure happens you know something strange is happening in your code.
Unbounded is preferable because that's how we do everything else, and best prepares for porting to larger architectures. Do your strings have a maximum size, for example?
The size of the queue can be read from a config file, and is tuned to the capability of the machine.
When you have an out-of-memory situation.
The thing you are trying to express is that you want a queue that stops working only when it physically can't work. Putting arbitrary large numbers in config files is not the way to express it.
For (;;) … While (condition…) …
Why not use while in both situations, so while(1)… instead of for(;;) ?
for (;;) is less typing.
Because some compilers warn that the condition is constant and suggest you rewrite it as for (;;)
Not a problem now.
They lack the code to remove a tautological test.
But approximately all compilers these days are optimising compilers, so don’t worry about it.
Because normalising is an optimization, and these compilers didn't have any optimisations. That was normal back then. You had a whole separate product that was specifically an 'optimising compiler'. These days that's implicit and we just say 'compiler'.
These days, even -O0 will canonicalise the condition! That's how normalised it is! But I guess this is done during translation to IR, rather than as an 'optimisation'.
We’re talking compiler implementation and optimisation - not coding aesthetics.
> Your link even shows this.
Yes the link shows how it doesn't matter today! Because almost all compilers are now 'optimising compilers'!
> There is no `cmp` opcode used. A simple unconditional `jmp` is used.
Oh my god that's the entire point! That's how compilers work now. They always optimise. They didn't used to do that.
The compiler checks the truthiness of the value in the while condition. Modern compilers remove it with either a tautology check, or a shortcut during IR building. They didn't used to do that, so there used to be a cost, which is why people started cargo-culting the for equivalent.
Here's a worked example using a niche compiler with a very different architecture that unusually still doesn't canonicalise during building.
Compare
https://godbolt.org/z/3v3anacM8
and
https://godbolt.org/z/chWjsen4s
See the difference? See the 'cmp eax, 1' and then how it goes away under optimisation?
for (;;) has always been the idiomatic way to do an infinite loop.
FWIW https://github.com/robohack/ucb-csrg-bsd contains both variants.
There’s no other occasion I can think of where an expression becomes vacuously true by being omitted. If it were conceptually coherent you’d write for(;true;). I guess maybe the reason they made C accept for(;;) but not while() is because while() might confuse a compiler by looking like a function call.
Syntax wise, it doesn't really matter. "for" just happens to be the keyword that allows omitting the condition. "While" effectively means "during which". "For" on the other hand has a significantly more flexible meaning; it seems reasonable to me that the for keyword would be more flexible. Syntax wise, the most explicit thing to have in a language would be a "forever" keyword of some sort.