Yves
Yves

Reputation: 12371

When should you use std::atomic instead of std::mutex?

In the question How to use std::atomic<>, obviously we can just use std::mutex to keep thread safety. I want to know when to use which one.

struct A {
    std::atomic<int> x{0};
    void Add() {
        x++;
    }
    void Sub() {
        x--;
    }
};

vs.

std::mutex mtx;
struct A {
    int x = 0;
    void Add() {
        std::lock_guard<std::mutex> guard(mtx);
        x++;
    }
    void Sub() {
        std::lock_guard<std::mutex> guard(mtx);
        x--;
    }     
};

Upvotes: 9

Views: 4921

Answers (2)

Alex Guteniev
Alex Guteniev

Reputation: 13634

std::atomic has methods is_lock_free (non-static) and is_always_lock_free (static).

When is_lock_free returns true, it means atomic does not have locks, and expected to perform better than the equivalent code with locks. When is_lock_free returns false, it means that atomic has a lock, and equivalent performance with code with locks.

This does not mean that you should always use atomic instead of mutex-based approach, conversely, if you expect is_lock_free to be always false, you should not use atomic:

  • Use of atomic for such cases would be misleading first of all.
  • Also the lock inside lock-based atomic may be suboptimal (it may be shared with another atomic due to sharing hash table entry (libc++) or may nor use OS wait (MSVC STL)).
  • And atomic underlying type has some restrictions that you don't need when you use a mutex on your own.

CPUs that support multithreading provide atomic instructions of CPU register size. Often they provide atomic instructions of double CPU register size.

Currently most programs are run in 64-bit mode on a 64-bit CPU, but some programs still run in a 32-bit mode or on 32-bit CPU.

STL implementation can do an smaller-size atomic type using a larger sized CPU instructions, if there are no exact-sized instructions.

There are restricted set of operations that can be done on arbitrary type as underlying atomic type, there are more ops for integers (and floats).

Together it means that a type of 64-bit size or less is likely to be a good candidate for atomic, provided that it is either a native integer/floating type, or a small structure that is updated as a whole with a new version.


You should be looking into a bigger picture.

Acquiring a mutex in ideal (and normally frequent) scenario is nothing more than an atomic operation, ditto releasing a mutex.

On the other hand, atomic operations are often an order of magnitude slower than non-atomic equivalent. E.g. i++ will be way slower for atomic<int> than for int.

So, despite updating one counter using atomic is likely to be good, doing something more using atomics, like updating five counters, may be more complex and slower than using a single mutex to protect the whole operation.

Upvotes: 1

Bathsheba
Bathsheba

Reputation: 234635

As a rule of thumb, use std::atomic for POD types where the underlying specialisation will be able to use something clever like a bus lock on the CPU (which will give you no more overhead than a pipeline dump), or even a spin lock. On some systems, an int might already be atomic, so std::atomic<int> will specialise out effectively to an int.

Use std::mutex for non-POD types, bearing in mind that acquiring a mutex is at least an order of magnitude slower than a bus lock.

If you're still unsure, measure the performance.

Upvotes: 10

Related Questions