Skip to content

Wiki

Mutex and RAII Lock

About 40 min read

Existing English translations were reused. 4 current source blocks remain in Simplified Chinese, without triggering paid translation.

When multiple threads run simultaneously, if they access the same shared data and at least one of them modifies this data, thread synchronization must be considered.

C++ One of the most basic and most commonly used synchronization tools is:

std::mutex

The Chinese term for mutex is usually:

Mutex

Its most core function can be simply understood as:

Only one thread is allowed to enter a certain section of code that needs protection at the same time.

This section focuses on:

  • 什么是共享数据、数据竞争和临界区;
  • std::mutex 到底“锁”的是什么;
  • lock()、try_lock()、unlock();
  • 为什么不推荐手动成对调用 lock() / unlock();
  • RAII 为什么特别适合管理锁;
  • std::lock_guard 的基本使用;
  • std::unique_lock 为什么更加灵活;
  • std::scoped_lock 为什么适合多把锁;
  • 什么是死锁,以及怎样避免;
  • std::recursive_mutex 和 std::timed_mutex;
  • std::shared_mutex 的多读单写;
  • std::call_once 的一次性初始化。

1.Why are mutexes needed?

Let's start with a classic example:

#include <print>
#include <thread>

int counter = 0;

void add_many()
{
    for (int i = 0; i < 100000; ++i)
    {
        ++counter;
    }
}

int main()
{
    std::thread t1(add_many);
    std::thread t2(add_many);

    t1.join();
    t2.join();

    std::println("{}", counter);
}

Example run result (due to data races, the actual numbers may differ each time):

104563

Here, two threads are created:

线程 t1
线程 t2

Each thread executes:

++counter;

100,000 times.

Intuitively:

100000 + 100000 = 200000

So it seems that in the end it should definitely output:

200000

However, this program has a serious concurrency problem.

The problem lies in:

++counter;

1.1.++counter is not an indivisible step

We usually observe:

++counter;

It feels like it is just:

counter 加 1

One operation.

However, from the perspective of program execution, it usually roughly needs to go through:

读取 counter 当前值
        ↓
当前值 + 1
        ↓
把结果写回 counter

Assume now:

counter = 10

Two threads executing simultaneously:

++counter;

The following may occur:

线程 A:读取 counter,得到 10
线程 B:读取 counter,得到 10

线程 A:计算 10 + 1 = 11
线程 B:计算 10 + 1 = 11

线程 A:写回 11
线程 B:写回 11

Final:

counter = 11

But both threads clearly performed one addition each, so the correct result should be:

counter = 12

One of the modifications was lost just like that.

This indicates:

Even if C++ appears to contain only one statement in the source code, it does not mean that it is naturally immune to interference from other threads in a multithreaded environment.


1.2.What is a data race?

If:

  • Multiple threads access the same memory location;
  • At least one thread will write;
  • There is no proper synchronization between these accesses;

the following may be produced:

Data Race

Previous:

++counter;

is a typical example.

Both thread A and thread B are in:

读取 counter
修改 counter

and there are no synchronization measures.

In C++, a data race usually means:

Undefined Behavior

Therefore, it cannot be understood merely as:

"The final result is occasionally off by 1."

The real question is:

Once a data race occurs, C++ the standard no longer guarantees program behavior.

Therefore, the first very important thing in multithreaded programming is:

Do not allow multiple threads to access shared mutable data simultaneously without protection.


1.3.What is a critical section?

Assume that:

++counter;

This code accesses variables shared by multiple threads:

counter

And we hope:

Only one thread can execute it at the same time.

Then this piece of code can be called:

Critical Section

Simple understanding:

临界区
=
不能让多个线程同时执行的代码区域

For example:

++counter;

It may also be a whole set of operations:

++count;
average = calculate_average();

As long as these operations must be protected as a whole, they can all belong to the same critical section.


2.std::mutex Basics

2.1.std::mutex What exactly is it

mutex is:

mutual exclusion

That is:

Mutual exclusion

You can think of it as a door lock with only one key.

Suppose there is a room:

共享数据

At the door, there is:

一把 mutex

Thread A wants to enter:

线程 A
  ↓
尝试获得 mutex
  ↓
成功
  ↓
进入临界区

At this point, thread B also wants to enter:

线程 B
  ↓
尝试获得 mutex
  ↓
发现 mutex 已经被 A 持有
  ↓
等待

After thread A leaves:

线程 A 释放 mutex

Only then does Thread B have a chance to acquire it.

So the core effect of a mutex is:

线程 A ─┐
线程 B ─┼→ 同一时刻只能有一个线程通过
线程 C ─┘

2.2.A mutex does not automatically "lock a variable".

This is a point that is very easily misunderstood when first learning about mutexes.

For example:

int counter = 0;
std::mutex mutex;

mutex does not know:

counter

Existence.

There is no automatic binding relationship between it and counter.

For example, thread A:

mutex.lock();
++counter;
mutex.unlock();

But if thread B directly:

++counter;

A mutex simply cannot stop it.

So a more accurate understanding is:

A mutex does not lock a particular variable; rather, it prevents all threads that follow the same locking rules from entering a certain section of code at the same time.

We artificially stipulate:

凡是访问 counter
都必须先获得 counter_mutex

Like so:

std::mutex counter_mutex;

only then does it truly serve the purpose of protecting counter.


2.3.std::mutexBasic usage of

std::mutex Defined in:

#include <mutex>

The three most basic operations are:

mutex.lock();
mutex.try_lock();
mutex.unlock();

2.3.1.lock(): Acquire the lock (not lock, but acquire the lock)

Basic usage:

mutex.lock();

If the mutex is not currently held by another thread:

mutex 空闲
    ↓
当前线程获得 mutex
    ↓
继续执行

If the mutex is already held by another thread:

线程 A 已经持有 mutex
          ↓
线程 B 调用 lock()
          ↓
暂时拿不到
          ↓
线程 B 阻塞等待

Wait for thread A:

mutex.unlock();

Only then will thread B have a chance to continue.

Therefore:

lock();

It can be simply remembered as:

If you can't get it, wait. (Very important)


2.3.2.unlock() : Release the lock

After the thread acquires the mutex:

mutex.lock();

After completing the critical section operation, you should:

mutex.unlock();

For example:

mutex.lock();

++counter;

mutex.unlock();

The execution process is roughly as follows:

获得 mutex(获得不了就阻塞,卡在这里)
    ↓
进入临界区
    ↓
修改 counter
    ↓
离开临界区
    ↓
释放 mutex

After it is released, other threads waiting on this mutex have a chance to continue execution.


2.3.3.try_lock():If you can't get it, then forget it.

Sometimes, we do not want to:

拿不到锁
↓
一直等(阻塞)

Instead, the hope is:

拿得到就执行
拿不到就先干别的(不想阻塞卡在这里)

In this case, you can use:

try_lock()

For example:

if (mutex.try_lock())
{
    ++counter;

    mutex.unlock();
}
else
{
    // 这一次没有获得 mutex
}

The difference can be simply remembered as:

lock()

拿不到
↓
等待(阻塞)

And:

try_lock()

拿不到
↓
立即返回 false(不会阻塞)

On success:

true

On failure:

false

Therefore:

if (mutex.try_lock())
{
    // 当前线程已经获得锁
}

Note that the standard permits try_lock() sporadic failures.

So do not put:

try_lock() == false

Absolutely understood as:

There must now be another thread holding the mutex.

A simpler way to understand it is:

This time the lock was not acquired.


2.4.Fix with mutex counter

The previous data race can be resolved as follows:

#include <print>
#include <mutex>
#include <thread>

int counter = 0;
std::mutex counter_mutex;

void add_many()
{
    for (int i = 0; i < 100000; ++i)
    {
        counter_mutex.lock();

        ++counter;

        counter_mutex.unlock();
    }
}

int main()
{
    std::thread t1(add_many);
    std::thread t2(add_many);

    t1.join();
    t2.join();

    std::println("{}", counter);
}

Operation result:

200000

As shown above, only the first one with a mutex mechanism outputs correctly200000; the outputs of the others without a mutex mechanism are non-deterministic.

All parties now wanting to modify:

counter

thread must first:

counter_mutex.lock();

Thus it may occur:

线程 A 获得 counter_mutex
        ↓
    ++counter
        ↓
线程 A 释放 counter_mutex
        ↓
线程 B 获得 counter_mutex
        ↓
    ++counter

without two threads modifying it at the same time.

However, this approach still has a major problem:

lock();
...
unlock();

Fully managed by the programmers themselves.


2.5.Why is it not recommended to handwrite lock() / unlock()

The following code looks fine:

mutex.lock();

change_shared_state();

mutex.unlock();

But in the future, the code might become:

mutex.lock();

if (something_wrong())
{
    return;
}

change_shared_state();

mutex.unlock();

If:

something_wrong()

true:

return;

it will directly exit the function.

Therefore:

mutex.unlock();

will never be executed.

The mutex will remain locked.

If other threads again:

mutex.lock();

it may just stay stuck there.

There are also exceptions:

mutex.lock();

some_function();

mutex.unlock();

If:

some_function();

Throw an exception; the following:

mutex.unlock();

It will likewise not execute normally.

Therefore manually manage:

lock()
unlock()

The biggest problem is:

It must be ensured that every exit path of the program correctly releases the lock.

Once the code becomes complex, it is very easy to overlook things.

This is very similar to the problem faced in memory management bynewanddelete, and in memory management, we use the RAII approach to solve this problem, that is, smart pointers.

This is exactly the kind of problem that RAII is particularly well suited to solve.


3.RAII lock management

3.1.Why RAII is suitable for managing locks

The core idea of RAII has been learned before:

Let the resource lifecycle be managed by the object lifecycle.

Ownership of a mutex can also be viewed as a kind of resource.

Therefore, we hope to have such an object:

对象构造
↓
mutex.lock()

对象析构
↓
mutex.unlock()

This way, only the following is needed:

{
    某个 RAII 锁对象 lock(mutex);

    // 临界区
}

When the scope ends:

}
↓
lock 对象自动析构
↓
自动释放 mutex

there is no need for programmers to write it everywhere themselves:

unlock();

This is:

RAII lock


3.2.std::lock_guard

One of the simplest and most commonly used RAII locks is:

std::lock_guard

For example:

std::mutex mutex;

void foo()
{
    std::lock_guard<std::mutex> lock(mutex);

    // 临界区
}

Execute:

std::lock_guard<std::mutex> lock(mutex);

it can be simply understood as:

lock_guard 构造
        ↓
mutex.lock()

When leaving the scope:

lock_guard 析构
        ↓
mutex.unlock()

Therefore:

void foo()
{
    std::lock_guard<std::mutex> lock(mutex);

    change_shared_state();

} // 自动释放 mutex

No need to write:

mutex.unlock();

3.3.Rewrite the counter using lock_guard.

The previous code can be written as:

#include <print>
#include <mutex>
#include <thread>

int counter = 0;
std::mutex counter_mutex;

void add_many()
{
    for (int i = 0; i < 100000; ++i)
    {
        std::lock_guard<std::mutex> lock(counter_mutex);

        ++counter;
    }
}

int main()
{
    std::thread t1(add_many);
    std::thread t2(add_many);

    t1.join();
    t2.join();

    std::println("{}", counter);
}

Operation result:

200000

Each cycle:

创建 lock_guard
      ↓
获得 counter_mutex
      ↓
++counter
      ↓
这一轮作用域结束
      ↓
lock_guard 析构
      ↓
自动释放 mutex

In this way, even if the following appears in the code later:

return;

Or with normal exception stack unwinding, it is also less easy to forget to release the lock.

C++17 Initially, it can also be written using class template argument deduction as:

std::lock_guard lock(counter_mutex);

Not necessarily required to write:

std::lock_guard<std::mutex>

3.4.You can use {} to actively reduce the scope of the lock

When is the RAII lock released?

The answer is:

When the lock object is destructed, the mutex is released.

So we can use {} to create a local scope:

void foo()
{
    do_something();

    {
        std::lock_guard lock(mutex);

        change_shared_state();
    }

    do_other_work();
}

Execution logic:

do_something()

获得 mutex
↓
修改共享数据
↓
释放 mutex

do_other_work()

Like so:

do_other_work();

it will not continue to hold the mutex.


3.5.The critical section should be as small as possible.

Assume it is written as:

std::lock_guard lock(mutex);

do_big_calculation();
save_file();
send_network_data();

update_shared_state();

Then throughout the entire process:

mutex 一直被当前线程持有

Other threads that need this mutex can only:

等待

However:

do_big_calculation();
save_file();
send_network_data();

It may not be necessary to access shared data at all.

A more reasonable approach is usually:

do_big_calculation();
save_file();
send_network_data();

{
    std::lock_guard lock(mutex);

    update_shared_state();
}

Therefore, typically:

Place only the operations that truly require mutual exclusion protection into the critical section.

Be especially cautious about performing the following while holding a lock:

  • 长时间计算;
  • sleep;
  • 文件 IO;
  • 网络 IO;
  • 等待其他线程;
  • 执行耗时未知的函数。

Otherwise, other threads will be unable to acquire the lock for a long time.


3.6.But critical sections also cannot be split arbitrarily.

"Keep the critical section as small as possible" does not mean:

Each line of code is locked separately once.

Assume that:

int count;
double average;

And these two values must always maintain a corresponding relationship.

For example:

++count;
average = calculate_average();

Then these two operations should probably belong to the same critical section:

std::lock_guard lock(state_mutex);

++count;
average = calculate_average();

If split into:

{
    std::lock_guard lock(state_mutex);
    ++count;
}

{
    std::lock_guard lock(state_mutex);
    average = calculate_average();
}

Between the two locks, another thread may interleave and read:

新的 count
+
旧的 average

Thus, a logically inconsistent state is observed.

Therefore:

The critical section should be as small as possible, but it must ensure that a complete modification of shared state is not split apart.


3.7.Locks protect "shared state"

Assumption:

struct User
{
    std::string name;
    int age;
};

If:

name
age

Together they form a complete user state, so it is more reasonable that:

std::mutex user_mutex;
User user;

Then:

void update_user()
{
    std::lock_guard lock(user_mutex);

    user.name = "Tom";
    user.age = 20;
}

rather than:

name 一把锁
age 一把锁

So when designing a mutex, do not only think:

Which line of code should be locked?

More importantly, consider:

Which data collectively form a shared state that must remain consistent?


3.8.Reducing shared data is usually better than going crazy with locking.

Look at this example:

for (int i = 0; i < 100000; ++i)
{
    std::lock_guard lock(counter_mutex);
    ++counter;
}

Although it is correct, it will repeat:

lock
unlock
lock
unlock
lock
unlock
...

Consider changing it to:

void add_many()
{
    int local_counter = 0;

    for (int i = 0; i < 100000; ++i)
    {
        ++local_counter;
    }

    std::lock_guard lock(counter_mutex);

    counter += local_counter;
}

Here:

local_counter

It is used only by the current thread, so no lock is needed.

The entire thread first completes on its own:

100000 次本地计算

Finally:

加一次锁
↓
更新共享 counter

This reflects an important idea in concurrent programming:

If data can be not shared, then try not to share it.

Because:

没有共享
↓
通常也就不需要同步

4.std::unique_lock (common)

lock_guardThe characteristic is very simple:

创建
↓
加锁

销毁
↓
解锁

But sometimes we want more flexibility.

For example:

先获得锁
↓
读取共享数据
↓
提前释放锁
↓
继续执行耗时操作

In this case, you can use:

std::unique_lock

The simplest way to write it:

std::unique_lock<std::mutex> lock(mutex);

Same as lock_guard :

构造时获得 mutex
析构时自动释放 mutex

However, unique_lock provides more control capabilities.


4.1.unique_lock Can be unlocked in advance

For example:

std::unique_lock lock(mutex);

read_shared_state();

lock.unlock();

do_expensive_work();

Execution logic:

获得 mutex
↓
访问共享数据
↓
主动释放 mutex
↓
执行耗时操作

However, lock_guard does not provide this manual unlocking capability.


4.2.unique_lock It can be locked again

For example:

std::unique_lock lock(mutex);

read_shared_state();

lock.unlock();

do_something();

lock.lock();

change_shared_state();

Therefore, unique_lock can perform:

lock
↓
unlock
↓
lock
↓
unlock

However, the more complex this code is, the more prone the lock design is to problems.

Therefore, use this only when such flexibility is truly needed.


4.3.Deferred locking:std::defer_lock

Default:

std::unique_lock lock(mutex);

The mutex is acquired immediately.

But it can be written as:

std::unique_lock lock(mutex, std::defer_lock);

At this point:

unique_lock 对象已经创建
但是 mutex 还没有被锁住

Afterward:

lock.lock();

For example:

std::unique_lock lock(mutex, std::defer_lock);

do_something();

lock.lock();

change_shared_state();

4.4.Attempt to lock: std::try_to_lock

Also available:

std::unique_lock lock(mutex, std::try_to_lock);

It attempts to acquire the mutex, but does not wait indefinitely if the lock is temporarily unavailable.

Afterward, you can:

if (lock.owns_lock())
{
    // 已经获得 mutex
}
else
{
    // 当前没有获得 mutex
}

4.5.owns_lock()

Since unique_lock is relatively flexible, so one:

unique_lock

An object does not necessarily always hold the mutex.

For example:

std::unique_lock lock(mutex, std::defer_lock);

When newly created:

lock.owns_lock()

As:

false

Execute:

lock.lock();

Later:

lock.owns_lock()

It is only:

true

If again:

lock.unlock();

It will change back to:

false

4.6.lock_guard and unique_lock how to choose

At the beginner stage, you can directly remember:

Tool Feature Common scenario
std::lock_guard Simple, the entire scope always holds the lock Ordinary critical section
std::unique_lock Can actively unlock, relock, delay locking, try locking Need flexible management of the lock lifecycle

So usually:

普通情况
↓
lock_guard

Requires:

unlock()
重新 lock()
defer_lock
try_to_lock

and other capabilities:

unique_lock

Do not assume that because unique_lock has more features:

It is always better than lock_guard.

More features also mean more complex state.

When a simple solution is available, the simple solution is generally preferred.


项目 lock_guard unique_lock
自动加锁/解锁 ✅ ✅
手动 unlock() / lock() ❌ ✅
延迟加锁 ❌ ✅
try_lock ❌ ✅
move ❌ ✅
condition_variable ❌ ✅
对象大小 更小 通常更大
理论运行开销 最低 略高
灵活性 低 高

In terms of efficiency alone, std::lock_guard is slightly higher than std::unique_lock, but the difference is usually so small that it can basically be ignored.

5.Deadlocks and Multiple Locks

5.1.What is a deadlock?

A mutex can solve the problem of many threads accessing shared data simultaneously.

However, if multiple mutexes are used simultaneously, new problems may arise:

Deadlock, of course, is not the game developed by Valvedeadlock.

Assumption:

std::mutex m1;
std::mutex m2;

Thread A:

void task1()
{
    std::lock_guard lock1(m1);
    std::lock_guard lock2(m2);
}

Thread B:

void task2()
{
    std::lock_guard lock2(m2);
    std::lock_guard lock1(m1);
}

The lock order of the two threads is different:

线程 A:
m1 → m2

线程 B:
m2 → m1

Thus it may happen:

线程 A 获得 m1

线程 B 获得 m2

线程 A 想获得 m2
但是 m2 在线程 B 手里
↓
A 等待 B

线程 B 想获得 m1
但是 m1 在线程 A 手里
↓
B 等待 A

Final:

A 等 B
B 等 A

No one could continue.

This is:

Deadlock


5.2.An intuitive example of deadlock

You can think of two mutexes as two chopsticks.

Two people eating must simultaneously have:

左筷子
+
右筷子

A got the left chopstick.

B got the right chopstick.

Then:

A:等 B 放下右筷子

B:等 A 放下左筷子

But neither of the two was willing to be the first to put down the chopsticks they had already picked up.

Therefore:

谁也吃不上饭

This is very similar to thread deadlock.


5.3.Avoid deadlocks: fixed lock ordering

A very important method is:

The entire program enforces a unified lock acquisition order.

For example, it is stipulated:

永远先获得 m1
再获得 m2

Then all threads must:

std::lock_guard lock1(m1);
std::lock_guard lock2(m2);

rather than writing in some places:

m1 → m2

Yet in other places it is written:

m2 → m1

A unified lock ordering can avoid many typical deadlocks.


5.4.std::scoped_lock

If an operation originally requires:

Acquire multiple mutexes simultaneously

C++17 Provided by:

std::scoped_lock

For example:

#include <mutex>

std::mutex m1;
std::mutex m2;

void task()
{
    std::scoped_lock lock(m1, m2);

    // 当前同时持有 m1 和 m2
}

It acquires these locks using the deadlock avoidance strategy provided by the standard library.

Compared to:

std::lock_guard lock1(m1);
std::lock_guard lock2(m2);

If your intention was originally:

我要一起拿到 m1 和 m2

Then:

std::scoped_lock lock(m1, m2);

usually more direct.

After the scope ends:

scoped_lock 析构
↓
自动释放它管理的锁

5.4.1.scoped_lock You can also manage a single lock

It does not necessarily have to pass more than two mutexes.

Alternatively:

std::scoped_lock lock(mutex);

For an ordinary scenario with a lock, it and:

std::lock_guard lock(mutex);

Very close.

At the beginner stage, you can simply remember:

普通单锁
→ lock_guard

需要灵活控制
→ unique_lock

同时管理多把锁
→ scoped_lock

5.5.std::lock()

C++11 also provides:

std::lock(m1, m2);

It can use a deadlock avoidance algorithm to attempt to acquire multiple lockable objects.

For example:

std::lock(m1, m2);

After successful execution:

m1 已经被当前线程获得
m2 也已经被当前线程获得

Next, they usually need to be handed over to an RAII object for management.

For example:

std::lock(m1, m2);

std::lock_guard lock1(m1, std::adopt_lock);
std::lock_guard lock2(m2, std::adopt_lock);

Here:

std::adopt_lock

It can be understood as telling lock_guard:

I have already locked this mutex in advance, so you should not call lock() again; just take it over from now on and be responsible for releasing it at the end.

Therefore:

std::adopt_lock

The premise is:

The current thread must already hold this lock.

C++17 Later, if it is only:

同时获得多把 mutex
+
自动管理它们的生命周期

Usually:

std::scoped_lock lock(m1, m2);

Even simpler.


Lock Single mutex performance Flexibility Multiple mutexes
lock_guard ⭐⭐⭐⭐⭐ Low Inconvenient
scoped_lock ⭐⭐⭐⭐⭐ Low Best
unique_lock ⭐⭐⭐⭐≈ Highest Can be combined with std::lock

6.Other mutex types

6.1.std::recursive_mutex

Normal:

std::mutex

It is not appropriate for the same thread to repeatedly acquire the same mutex.

For example:

void A()
{
    std::lock_guard lock(mutex);

    B();
}

void B()
{
    std::lock_guard lock(mutex);
}

Call:

A()
↓
已经获得 mutex
↓
A() 调用 B()
↓
B() 又尝试获得同一把 mutex

This will cause problems.

If the program indeed requires:

The same thread repeatedly enters a region protected by the same lock

Can be used:

std::recursive_mutex

For example:

std::recursive_mutex mutex;

It allows the same thread to repeatedly acquire this lock.

However:

recursive_mutex should normally not be the first choice.

If the code relies heavily on recursive locks, it sometimes indicates:

函数职责
或
锁的边界

Designed to be overly complex.

When recursion-induced locking can be avoided by reorganizing the code structure, it is usually easier to maintain.


6.2.std::timed_mutex

Normal:

mutex.lock();

It can be understood as:

If it cannot be obtained for the time being, then keep waiting.

However, sometimes a program does not want to wait indefinitely.

For example:

最多等 100ms

In this case, you can use:

std::timed_mutex

It supports:

try_lock_for()
try_lock_until()

For example:

#include <chrono>
#include <mutex>

std::timed_mutex mutex;

if (mutex.try_lock_for(std::chrono::milliseconds(100)))
{
    // 成功获得 mutex

    mutex.unlock();
}
else
{
    // 在等待时间内没有获得 mutex
}

Among them:

try_lock_for()

Emphasis:

Maximum waiting time.

For example:

100ms
2s

And:

try_lock_until()

Emphasis:

Wait until which point in time.

They and those learned previously:

sleep_for()
sleep_until()

The approach is relatively similar in terms of "relative time / absolute time".


6.3.std::shared_mutex: Multiple read, single write

A regular mutex has one characteristic:

Whether you are reading data or writing data, only one thread can hold the lock at any given time.

Suppose the program contains:

std::string config;

Many threads are simply:

读取 config

There are very few cases where it is actually modified.

If a normal mutex is used:

线程 A 读
↓
线程 B 即使也只是读
也必须等待

However:

读 + 读

Usually, they do not corrupt each other's data.

What is truly dangerous is:

读 + 写

Or:

写 + 写

So C++17 provides:

std::shared_mutex

It can support:

Multiple reader threads can enter simultaneously, but a writer thread must have exclusive access.


6.3.1.shared_lock and unique_lock

When using shared_mutex:

Reading typically uses:

std::shared_lock

Writing typically uses:

std::unique_lock

For example:

#include <mutex>
#include <shared_mutex>
#include <string>

std::string config;
std::shared_mutex config_mutex;

std::string read_config()
{
    std::shared_lock lock(config_mutex);

    return config;
}

void write_config(std::string value)
{
    std::unique_lock lock(config_mutex);

    config = std::move(value);
}

Multiple threads all read:

线程 A:shared_lock
线程 B:shared_lock
线程 C:shared_lock

They can exist simultaneously.

But the writer thread:

std::unique_lock lock(config_mutex);

Must be exclusive.

It can be simply remembered as:

shared_lock + shared_lock
✓ 可以同时存在

shared_lock + unique_lock
✗ 不可以

unique_lock + unique_lock
✗ 不可以

This is:

Multiple readers, single writer.


6.3.2.Why might reads also require locking?

There is a very common incorrect way of writing:

void write()
{
    std::lock_guard lock(mutex);

    value = 10;
}

int read()
{
    return value;
}

Some might think:

It is enough to lock when writing; reading will not modify the variable.

Actually, that is not the case.

If:

线程 A 正在写 value

Simultaneously:

线程 B 正在读 value

data races may still occur.

Therefore:

If there is concurrent writing to shared data, the corresponding reads usually must also participate in the same synchronization rules.

Cannot only lock writes without considering reads.


6.3.3.shared_mutex Not necessarily faster than an ordinary mutex

See:

多个线程可以同时读

It is easy to produce a misunderstanding:

Then wouldn't it be better to just use shared_mutex for everything from now on?

Actually, shared_mutex more complex state needs to be maintained by oneself, for example:

现在有几个读线程?
有没有写线程?
什么时候允许写线程进入?

These themselves all have additional overhead.

Therefore:

std::shared_mutex

More suitable:

读很多
写很少
并且确实存在大量并发读取

scenarios.

Do not simply assume:

shared_mutex 一定比 mutex 快

They simply apply to different scenarios.


7.One-time initialization

7.1.std::call_once

There is also a special type of multithreading requirement:

A certain piece of initialization code can only be executed successfully once.

For example:

initialize();

There may be three threads entering simultaneously for the first time:

线程 A
线程 B
线程 C

But we hope:

initialize()

In the end, it was successfully completed only once.

The standard library provides:

std::once_flag
std::call_once

For example:

#include <print>
#include <mutex>
#include <thread>

std::once_flag flag;

void initialize()
{
    std::println("initialize once");
}

void worker()
{
    std::call_once(flag, initialize);
}

int main()
{
    std::thread t1(worker);
    std::thread t2(worker);
    std::thread t3(worker);

    t1.join();
    t2.join();
    t3.join();
}

Operation result:

initialize once

All three threads will execute:

std::call_once(flag, initialize);

However:

initialize();

只会成功完成一次。

It can be roughly understood as:

线程 A ─┐
线程 B ─┼→ call_once → initialize 成功一次
线程 C ─┘

If an exception is thrown during the execution of the initialization function, this time will not be considered successfully completed, and subsequent calls can still attempt again.


7.2.Function-local static initialization is also thread-safe

C++11 Start:

MyObject& instance()
{
    static MyObject object;

    return object;
}

Here:

static MyObject object;

The initialization itself already has a thread-safety guarantee.

So if the purpose is only:

Lazily initialize a static object inside a function

Usually, there is no need to additionally write:

std::call_once

8.Engineering Practice (Important)

8.1.It is best to encapsulate the mutex together with the data it protects

Not recommended to design as:

函数 A:
调用前必须先锁 mutex

函数 B:
自己内部会锁

函数 C:
只能在已经持锁时调用

函数 D:
有时候锁,有时候不锁

Once there is a lot of code, it easily turns into:

Who exactly is responsible for locking?

A better design is usually:

Let classes that own shared data manage synchronization themselves.

For example:

class Counter
{
public:
    void increment()
    {
        std::lock_guard lock(mutex_);

        ++value_;
    }

    int get() const
    {
        std::lock_guard lock(mutex_);

        return value_;
    }

private:
    mutable std::mutex mutex_;
    int value_ = 0;
};

Externally, only the following is needed:

counter.increment();

There is no need to know:

内部用了哪把 mutex
什么时候 lock
什么时候 unlock

This makes the synchronization rules more centralized and also makes it less likely to miss a lock.


8.2.Why is mutex often written as mutable (Important)

Previous:

int get() const

is a const member function.

However, for mutex:

mutex_.lock();
mutex_.unlock();

will modify the mutex's own internal state.

So usually write:

mutable std::mutex mutex_;

Thus, even if the current member function is:

const

It is still possible to lock the mutex.

This does not mean:

get()

The externally observable logical state of the Counter is actually modified.

It only modifies the internal synchronization state.

So the mutex is written as:

mutable

is a very common practice.


8.3.Do not casually call unknown code while holding a lock

For example:

std::lock_guard lock(mutex);

some_callback();

If:

some_callback();

Internally, it also attempts to obtain:

mutex

Or if other mutexes are acquired as well, very complex lock dependency relationships may result.

Therefore, a very practical principle is:

While holding a lock, try not to call external code whose locking behavior is unknown.

In particular:

回调函数
虚函数
第三方复杂函数
用户传入函数

All should be cautious.


8.4.How to remember the three commonly used RAII locks

At the beginner stage, it can be remembered as:

std::lock_guard
│
├─ 简单
├─ 构造时加锁
├─ 析构时解锁
└─ 普通单锁场景优先考虑
std::unique_lock
│
├─ 更灵活
├─ 可以 unlock()
├─ 可以重新 lock()
├─ 支持 defer_lock
└─ 支持 try_to_lock
std::scoped_lock
│
├─ C++17
├─ RAII
├─ 可以管理一把锁
└─ 特别适合同时管理多把锁

The simplest way to remember:

普通单锁
→ lock_guard

灵活控制
→ unique_lock

同时多锁
→ scoped_lock

9.Common Errors

9.1.Lock only write operations

void write()
{
    std::lock_guard lock(mutex);

    value = 10;
}

int read()
{
    return value;
}

If there are concurrent writes, unsynchronized reads can likewise produce a data race.


9.2.Manually lock() forgot after unlock()

For example:

mutex.lock();

if (error)
{
    return;
}

mutex.unlock();

Therefore, in ordinary scenarios, RAII locks are preferred.


9.3.Inconsistent order of multiple locks

线程 A:
m1 → m2

线程 B:
m2 → m1

may cause deadlock.


9.4.The critical section is too large

For example:

std::lock_guard lock(mutex);

sleep();
network_io();
file_io();
big_calculation();

Other threads may be unable to acquire the mutex for a long time.


9.5.Critical section too small

The data that should have been updated as a whole was split apart:

锁
修改 A
解锁

锁
修改 B
解锁

may cause other threads to see an inconsistent state in between.


9.6.Calling an unknown function while holding a lock

std::lock_guard lock(mutex);

callback();

If the callback internally involves locks, it may cause deadlocks or complex lock dependencies.


9.7.Adding locks everywhere for the sake of “safety”

More locks do not necessarily mean greater security.

More locks may bring about:

更复杂的锁关系
↓
更多线程竞争
↓
更差的性能
↓
更高的死锁风险

What should really be done is:

Specify which shared state is protected by which mutex.


9.8.Assuming that a mutex automatically protects variables

Below:

std::mutex mutex;
int counter;

will not be established automatically:

mutex 保护 counter

relationship.

All threads must comply with:

访问 counter 前
必须按照约定获得 mutex

mutex really takes effect.


9.9.think shared_mutex is necessarily faster

shared_mutex It allows multiple threads to read simultaneously, but the management cost is also higher.

Only truly:

读很多
写很少
存在明显并发读取需求

only then is it worth considering.


10.The Most Important Mutex Mental Model

See:

std::mutex mutex;

Do not simply understand it as:

Created a lock that can lock variables.

A better understanding is:

Created a synchronization object in which multiple threads compete for the qualification to enter a certain critical section.

See:

std::lock_guard lock(mutex);

It can be understood as:

线程想进入临界区
        ↓
先获得 mutex
        ↓
获得成功
        ↓
进入临界区
        ↓
离开作用域
        ↓
自动释放 mutex

Other threads:

暂时拿不到 mutex
↓
不能同时进入

This is the most core idea of a mutex.


11.Summary

std::mutexThe most core function is:

Allow multiple threads to mutually exclusive access shared mutable state.

The three most basic operations:

lock()
try_lock()
unlock()

However, in actual development, manual writing is not recommended for ordinary scenarios:

mutex.lock();

...

mutex.unlock();

Instead, RAII objects should be given priority to manage locks.

Ordinary single-lock scenario:

std::lock_guard

Need more flexible control over lock lifecycle:

std::unique_lock

Need to manage multiple locks simultaneously:

std::scoped_lock

Requires multiple reads and single writes:

std::shared_mutex

Need to wait with a timeout:

std::timed_mutex

Indeed, this is needed when the same thread must repeatedly enter the same critical section:

std::recursive_mutex

It is necessary to make a certain piece of initialization logic execute successfully only once:

std::call_once

What truly needs to be considered in concurrent code is usually not:

mutex API 怎么写?

Instead:

哪些数据会被多个线程共享?
哪些操作必须作为一个整体?
哪把 mutex 负责保护这些状态?
临界区应该有多大?
有没有同时获取多把锁?
不同地方的锁顺序是否一致?

Once these issues are thought through, the mutex's own code is usually not complex.

For the synchronization of a "single simple shared variable" and the two types of problems of "making a thread wait for a certain condition to occur", the later std::atomic and std::condition_variable chapters will continue to introduce them respectively.