How to implement an atomic counter

58.1k views Asked by At

Trying to make a unique id generating function, and came up with this:

std::atomic<int> id{0};
int create_id() {
    id++;
    return id.load();
}

But I assume it's possible for that function to return the same value twice, right? For example, thread A calls the function, increments the value, but then halts while thread B comes in and also increments the value, finally A and B both return the same value.

So using mutexes, the function might look like this:

std::mutex mx;
int id = 0;
int create_id() {
    std::lock_guard<std::mutex> lock{mx};
    return id++;
}

My question: Is it possible to create the behavior of spawning unique int values from a counter using only atomics? The reason I'm asking is because I need to spawn a lot of id's, but read that mutex is slow.

3

There are 3 answers

2
Jarod42 On BEST ANSWER

Simply use:

std::atomic<int> id{};

int create_id() {
    return id++;
}

See http://en.cppreference.com/w/cpp/atomic/atomic/operator_arith

0
Pete Becker On

Your two code snippets do two different things.

id++;
return id.load();

that code increments id, then returns the incremented value.

std::lock_guard<std::mutex> lock{mx};
return id++;

that code returns the value before the increment.

The correct code to do what the first tries to do is

return ++id;

The correct code to do what the second does is

return id++;
2
Carlo Wood On

A mutex is overkill.

There is no pre-increment atomic operation (but you can return the previous value and add one to that, of course).

As pointed out by Pete, your first code block attempts to do a pre-increment (return the result of the increment).

Doing return ++id works, but is equivalent to return id.fetch_add(1) + 1; which uses the slow default sequentially-consistent memory order. This is not required here, in fact you can do with a relaxed memory order.

If you really mean to use a global variable for the atomic, the correct (and fastest) code that does what your first code block attempts is:

int create_id() {
    static std::atomic<int> id{0};
    return id.fetch_add(1, std::memory_order_relaxed) + 1;
}

Notes:

You can leave away the + 1 if you want post-increment.

Using std::memory_relaxed doesn't make a difference on Intel CPU's (x86) because fetch_add is a Read-Modify-Write operation and the bus must be locked anyway (lock assembly instruction). But on a more relaxed architecture it does make a difference.

I didn't want to pollute global namespace with 'id', so I put it as a static in the function; however in that case you must make sure that on your platform that doesn't lead to actual initialization code. E.g. if a constructor that isn't constexpr needs to be called then a test is necessary to see if the static was already initialized or not. Fortunately, the value initializing constructor of an integral atomic is constexpr, so the above leads to constant initialization.

Otherwise you'd want to make it -say- a static member of a class that is wrapping this and put the initialization somewhere else.