The usage is obvious:
// in some object class declaration
static UIDPool s_uidPool;
UID m_id;
...
// in constructor of this object
m_id = s_uidPool.acquire();
...
// in destructor of this object
s_uidPool.release(m_id);
What I don't like about this code is the lock guards, even though they use a (fast) spinlock and contention is expected to occur rarely. So I'm wondering if it is possible to implement such (or similar) unique id generator using std::atomic's magic. But simply increasing some counter atomically and using its current value as id won't do because unused ids must be returned to the "pool" of unused ids.
Repeater
The thing is that the largest possible size of this "pool" is more or less unknown. The worst case I think will be 10^7 (and even about this I'm not sure, but definitely less than 10^9). And allocating 40MB+ just for this queue is too wasteful.
So use a lock free queue containing 1000 IDs (or some other sensible allocation). As IDs are released, add them back into the queue. If the queue is ever down to some small size, you need more IDs so add another thousand into the queue. The queue thus seems infinite to the user, but never takes up too much space, and released IDs are put back into the queue to be reused.
Why do you need to reuse IDs? Since you could have 10^7 items at once, why so keen to limit the number of IDs?
JLBorges
My code needs to also run on 32-bit systems (x86/ARMv7). I think that atomicity of operations on unsigned long long variables is realized internally using locks (mutexes) on 32-bit systems. Am I wrong?
Repeater
I was just hoping that this could be done without external data structures like in my 1st post for example (although its kinda unsafe, even considering that all objects will be relatively short-lived).
But such queue of ids seems to be the good and safe choice. Thanks.
If the queue is ever down to some small size, you need more IDs so add another thousand into the queue.
Indeed. I was thinking that many threads can drastically increase this queue's size when they see it being depleted and start adding new ids to it all at once (e.g. 1024 ids at a time each thread). But this can be adjusted.
Why do you need to reuse IDs?
Because objects with these ids will be coming and going. And overflowing unsigned int by simply increasing the counter is quite possible.
Since you could have 10^7 items at once, why so keen to limit the number of IDs?
I don't know how many objects there will be at one time. 10^7 is just a "soft top". But the probability of some number of objects existing at one time decreases greatly as the number of these objects increases. So I just don't want to waste memory considering that there will be several independent id-generators of that kind.
I suspect that the interaction with a 64 bit int value on a 32 bit system will be orders of magnitude faster than anything done involving a class and queues and all that sort of thing.
Something along the lines of this would seem to do everything needed to generate the ID.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
#include <atomic>
#include <iostream>
int getNextSerial()
{
static std::atomic<uint64_t> i;
return i++;
}
int main()
{
for (int a = 0; a < 34; ++a)
{
std::cout << getNextSerial() << std::endl;
}
}
Simple. Fast. Easy to understand. Easy to maintain. Ticks all the boxes.
> My code needs to also run on 32-bit systems (x86/ARMv7). I think that atomicity of operations
> on unsigned long long variables is realized internally using locks (mutexes) on 32-bit systems.
> Am I wrong?
Check it out by running this program (on a 32-bit platform):
Well I've tested atomicity of 64-bit variables on needed architectures and yes they're lock-free. So it seems that I was deluding myself. :P
Thanks everyone.