C++并发库介绍(上)

本期内容建立在你已经对多线程有一定基础的情况下学习。

之前我们已经了解过<pthread.h>这个库文件。但是它是在Linux上运行的,无法做到跨平台。那么如果我们要做C++跨平台开发的话该怎么做呢?C++11引入了一个新的库------<thread>。真正的做到了跨平台开发。

本期我们就来学习这个库文件,了解现代C++多线程开发。

相关的代码上传至作者的个人gitee:楼田莉子/CPP代码学习喜欢的话请点个赞谢谢。

相关的参考文档:std::thread - cppreference.comthread - C++ Reference

目录

thread库

std::thread

代码示例

this_thread

get_id

yield

sleep_until

sleep_for

mutex

[std::mutex ------ 最基础的排他锁](#std::mutex —— 最基础的排他锁)

[std::recursive_mutex ------ 允许同一线程递归加锁](#std::recursive_mutex —— 允许同一线程递归加锁)

[std::timed_mutex ------ 支持超时的非递归锁](#std::timed_mutex —— 支持超时的非递归锁)

[std::recursive_timed_mutex ------ "全能型"但最昂贵](#std::recursive_timed_mutex —— “全能型”但最昂贵)

lock_guard

unique_lock

unique_lock相关的接口

[unique_lock与 std::condition_variable 配合](#unique_lock与 std::condition_variable 配合)

lock和try_lock

lock

try_lock

atomic

atomic模板特化

atomic相关接口

atomic中的CAS操作

atomic特化成员函数

atomic内存序

内存序

内存序与同步关系总结

总结表格

condition_variable

基本概念

相关接口

wait家族

通知家族

总结与建议

与互斥锁的示例代码


thread库

C++11的thread库是对各个系统的线程库的封装。因此thread库是跨平台的。

其次之前我们学习的<pthread.h>是面向对象的(C语言),而thread库是面向对象的。

而在C++11的 <thread> 库中,创建线程的核心工具是 std::thread 类。接下来我们将着重讲解。

std::thread

作用 :创建一个新的执行线程,并立即开始执行指定的可调用对象(函数、函数对象、lambda 表达式等)。新线程与构造出的 std::thread 对象关联,允许后续控制(如等待、分离)该线程。

表达式

  1. 默认构造函数
cpp 复制代码
std::thread() noexcept;

创建一个不表示任何线程的 std::thread 对象(不可汇合,not joinable)。

  1. 带可调用对象的构造函数
cpp 复制代码
template <class F, class... Args>
explicit thread(F&& f, Args&&... args);

这是创建线程的主要形式。F 是可调用对象类型,Args 是传递给 f 的参数类型。

  1. 移动构造函数
cpp 复制代码
thread(thread&& other) noexcept;

other 所管理的线程所有权转移给新对象,other 变为不可汇合。

  1. 删除的拷贝构造函数
cpp 复制代码
thread(const thread&) = delete;

禁止复制,确保每个线程对象独占一个底层线程。

参数

  1. 可调用对象 f
  • 可以是普通函数、函数对象(仿函数)、lambda 表达式、成员函数指针等。

  • 对于成员函数,第一个参数必须是指向对象的指针或引用(使用 this 语义)。

例如:

cpp 复制代码
void func(int);
struct Functor { void operator()(int); };
class MyClass { public: void method(int); };

std::thread t1(func, 42);
std::thread t2(Functor(), 42);
std::thread t3([](int x){ /*...*/ }, 42);
MyClass obj;
std::thread t4(&MyClass::method, &obj, 42); // 成员函数
  1. 参数包 args...
  • 传递给可调用对象的参数。

  • 参数传递方式 :默认情况下,所有参数(包括可调用对象)都以拷贝的方式传递给新线程的内部存储。这是为了保证线程安全:即使原始对象在创建线程后被销毁,新线程依然持有其副本。

  • 若需要按引用传递,必须使用 std::refstd::cref 包装。

cpp 复制代码
void update(int& x) { ++x; }
int a = 0;
std::thread t(update, std::ref(a));  // a 被引用传递
t.join();
std::cout << a;  // 输出 1
  • 如果参数是只能移动的类型(如 std::unique_ptr),需要使用 std::move 显式转移所有权。
cpp 复制代码
void process(std::unique_ptr<int> p) { /*...*/ }
auto p = std::make_unique<int>(10);
std::thread t(process, std::move(p));
  1. 构造函数的额外注意点
  • 可调用对象 f 和所有参数 args 的 decay-copy 要求:它们必须可拷贝(或可移动)到线程的内部存储中。

  • 如果 f 是成员函数,则指向对象的指针参数也必须可拷贝;若需要引用对象,使用 std::ref(obj) 而不是裸指针。

代码示例

下面的代码可以自己运行(这里的代码比较混乱,就不作为结果参考了,仅作为作用展示)

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS
#include<thread>
#include<iostream>
using namespace std;	

void Print(int n,int i)
{
	for (; i < n; ++i)
	{
		cout << this_thread::get_id() <<" : " << i << endl;
	}
}
int main()
{
	//thread创建出来必须等待,否则会报错
	//每次运行结果都不一样,这里就不做参考了
	thread t1(Print,10,0);
	thread t2(Print,20, 10);
	cout << t1.get_id() << endl;
	cout << t2.get_id() << endl;	
	t1.join();
	t2.join();
	return 0;
}

this_thread

相关文档:this_thread - C++ Reference

this_thread是一个命名空间,内部封装了四个函数

get_id

作用

获取当前线程的唯一标识符。该标识符可用于比较、输出等操作,常用于日志或调试。

表达式

cpp 复制代码
std::thread::id get_id() noexcept;

**参数:**无。

返回值

一个 std::thread::id 类型的对象,代表当前线程的 ID。每个可汇合(joinable)线程都有唯一的 ID,即使线程函数已执行完毕,其 ID 仍然有效。主线程也有一个有效的 ID。

示例代码:

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS
#include <thread>
#include <iostream>
using namespace std;
//this_thread::get_id() 
// returns the id of the current thread. The main thread and 
// worker thread will have different ids.
void getid()
{
    cout << "Main thread ID: " << this_thread::get_id() << endl;
    thread t([] {
        cout << "Worker thread ID: " << this_thread::get_id() << endl;
        });
    t.join();
}

int main() 
{
    getid();
    return 0;
}

结果为:

yield

作用

向调度器提示当前线程愿意主动放弃其剩余的时间片,以便让其他等待的线程运行。这是一种协作式调度提示,常用于自旋锁或忙等待循环中,以降低 CPU 占用。

表达式

cpp 复制代码
void yield() noexcept;

**参数:**无。

返回值: void

  • 注意事项

    • yield 的具体行为由操作系统调度器决定:可能立即切换到另一个线程,也可能忽略该提示(如果没有其他可运行线程,当前线程会继续执行)。

    • 不适合精确控制线程执行顺序,仅作为性能优化的轻量级让步。

示例代码:

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS
#include <thread>
#include <iostream>
#include <atomic>
using namespace std;
//yield()
std::atomic<bool> flag(false);

void wait_for_flag() 
{
    while (!flag.load()) 
    {
        std::this_thread::yield();  // 让步,避免忙等待耗尽 CPU
    }
}
void yiledTest()
{
    std::thread t(wait_for_flag);
    // ... 某处设置 flag = true
    t.join();
}
int main() 
{
    yiledTest();
    return 0;
}

运行后代码只会等待,不会有结果

sleep_until

作用

阻塞当前线程直到指定的绝对时间点。适合需要定时唤醒的场景(如"等到下午三点执行")。

表达式

cpp 复制代码
template< class Clock, class Duration >
void sleep_until( const std::chrono::time_point<Clock,Duration>& sleep_time );

参数
sleep_time:一个 std::chrono::time_point 对象,表示希望唤醒的绝对时间点。常用时钟有 std::chrono::system_clockstd::chrono::steady_clock 等。

返回值: void

  • 注意事项

    • 如果指定的时间点已经过去,函数立即返回。

    • sleep_for 类似,实际唤醒时间可能略晚于目标时间点。

sleep_for

作用

阻塞当前线程至少一段指定的持续时间。用于实现简单的延时或定时等待。

表达式

cpp 复制代码
template< class Rep, class Period >
void sleep_for( const std::chrono::duration<Rep,Period>& sleep_duration );

参数
sleep_duration:一个 std::chrono::duration 对象,表示希望睡眠的时间长度。常用预定义时长包括 std::chrono::secondsstd::chrono::millisecondsstd::chrono::microseconds 等。

返回值: void

  • 注意事项

    • 实际阻塞时间可能因系统调度和计时器精度而略长于指定时间,但不会短于指定时间(除非被其他机制唤醒,但标准未提供唤醒接口)。

    • 如果指定时长为 0 或负数,行为由实现定义(通常立即返回)。

代码示例:

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS
#include <thread>
#include <iostream>
#include <chrono>

using namespace std;


//sleep_for()
void sleepForTest()
{
    std::cout << "Start sleeping..." << std::endl;
    std::this_thread::sleep_for(std::chrono::seconds(2));
    std::cout << "Wake up!" << std::endl;
}
int main() 
{
	sleepForTest();
    return 0;
}

mutex

相关文档:<mutex> - C++ Reference

<mutex> 提供了四种互斥量,它们的主要区别在于锁的语义(是否允许同一线程重复锁定)锁定时的阻塞行为(是否支持超时)

std::mutex ------ 最基础的排他锁

用法:最常用,提供独占的非递归互斥功能。

  • 特点

    • 不支持递归 :如果一个线程在未解锁的情况下再次调用 lock(),会导致未定义行为(通常是无期等待,即死锁)。

    • 不支持超时:线程会一直阻塞直到获取锁。

    • 效率最高:在大多数实现中,它是基于操作系统原语的轻量级封装。

示例代码:

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
#include<mutex>
#include<thread>
using namespace std;
int x = 0;
mutex mtx;
void Print(int n)
{
	mtx.lock();
	for (size_t i = 0; i < n; ++i)
	{
		++x;//++不是原子操作,可能会出现线程安全问题,要加锁保护
	}
	mtx.unlock();
	//加锁后并行变串行,效率会降低,锁的粒度越大,效率越低,所以要尽量缩小锁的粒度

	//不建议这么写,因为临界区过小切换上下文会带来大量的开销
	//for (size_t i = 0; i < n; ++i)
	//{
	//	mtx.lock();
	//	++x;//++不是原子操作,可能会出现线程安全问题,要加锁保护
	//	mtx.unlock();
	//}
	
}

int main()
{
	thread t1(Print, 1000000);
	thread t2(Print, 2000000);
	t1.join();
	t2.join();

	cout << x << endl;
	return 0;
}

如果x和mtx定义在main内,需要用ref取参数。具体原因参考以下文档:

ref - C++ Reference

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
#include<mutex>
#include<thread>
using namespace std;

void Print(int n,int &rx,mutex&rmtx)
{
	rmtx.lock();
	for (size_t i = 0; i < n; ++i)
	{
		++rx;//++不是原子操作,可能会出现线程安全问题,要加锁保护
	}
	rmtx.unlock();
	//加锁后并行变串行,效率会降低,锁的粒度越大,效率越低,所以要尽量缩小锁的粒度

	//不建议这么写,因为临界区过小切换上下文会带来大量的开销
	//for (size_t i = 0; i < n; ++i)
	//{
	//	mtx.lock();
	//	++x;//++不是原子操作,可能会出现线程安全问题,要加锁保护
	//	mtx.unlock();
	//}
	
}

int main()
{	
	int x = 0;
	mutex mtx;
	//这里需要ref才能传递引用,否则会传递值,线程内修改的只是副本,主线程内的x不会改变
	//原因参考这个文档:https://legacy.cplusplus.com/reference/functional/ref/?kw=ref
	thread t1(Print, 1000000, ref(x), ref(mtx));
	thread t2(Print, 2000000, ref(x),ref(mtx));
	t1.join();
	t2.join();

	cout << x << endl;
	return 0;
}

也可以直接用lambda表达式。Lambda 通过捕获列表 [&x, &mtx] 以引用的方式"捕获"了外部变量 xmtx。捕获后的 Lambda 对象内部已经存储了这些变量的引用(或副本)。当创建 std::thread 时,只需传递 Lambda 对象本身和剩余的显式参数(这里是 n),被捕获的变量会自动随 Lambda 一起进入线程内部

cpp 复制代码
#include<iostream>
#include<mutex>
#include<thread>
using namespace std;

//也可以直接用lambda表达式来创建线程函数
int main()
{
	int x = 0;
	mutex mtx;
	auto Print = [&x, &mtx](size_t n) 
		{
			mtx.lock();
			for (size_t i = 0; i < n; ++i)
			{
				++x;//++不是原子操作,可能会出现线程安全问题,要加锁保护
			};
			mtx.unlock();
		};
	thread t1(Print, 1000000);
	thread t2(Print, 2000000);
	t1.join();
	t2.join();
	cout << x << endl;
	return 0;
}

std::recursive_mutex**------ 允许同一线程递归加锁**

用法:当你需要在已经持有锁的同一个线程里,再次调用需要相同锁的函数时使用。

  • 特点

    • 允许递归 :同一个线程可以多次调用 lock(),但必须调用对应次数的 unlock() 才会真正释放锁。

    • 有额外开销:需要记录锁的持有线程和计数。

  • 典型场景:一个公有函数调用了另一个需要同样锁保护的私有函数。

示例代码:

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
#include <mutex>    // for recursive_mutex
#include <thread>
#include <functional> // for std::ref
using namespace std;

// 递归函数,演示 recursive_mutex 特性
void recursive_increment(int n, int& rx, recursive_mutex& rmtx, int depth = 0) 
{
    rmtx.lock();
    // 模拟递归中的操作:这里只是简单递增
    ++rx;
    cout << "Depth " << depth << " locked, rx = " << rx << endl;

    if (depth < n) 
    {
        // 递归调用,再次尝试加锁(同一个线程)
        recursive_increment(n, rx, rmtx, depth + 1);
    }

    rmtx.unlock();
    cout << "Depth " << depth << " unlocked." << endl;
}

int main() 
{
    int x = 0;
    recursive_mutex rmtx;

    // 启动两个线程,每个线程内部递归加锁
    // 用 std::ref(确保已 #include <functional>)
    thread t1(recursive_increment, 3, std::ref(x), std::ref(rmtx));

    // 或用 lambda,避免传参/拷贝问题,更直观
    thread t2([&] { recursive_increment(3, x, rmtx); });

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

    cout << "Final x = " << x << endl;
    return 0;
}

std::timed_mutex**------ 支持超时的非递归锁**

用法:当你不想无限期等待一个锁,或者需要在指定时间内尝试获取锁时使用。

  • 特点

    • 支持超时 :提供 try_lock_for(rel_time)try_lock_until(abs_time) 方法。

    • 非递归 :和 std::mutex 一样,同一线程不能重复锁定。

  • 典型场景:需要避免死锁,或对响应时间有要求的实时系统。

示例代码:

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
#include <mutex>    // for timed_mutex
#include <thread>
#include <chrono>   // for milliseconds
#include <functional>
using namespace std;

void long_task(int& rx, timed_mutex& tmtx) 
{
    tmtx.lock();
    cout << "Long task acquired lock, sleeping for 200ms..." << endl;
    this_thread::sleep_for(chrono::milliseconds(200));
    ++rx;
    tmtx.unlock();
    cout << "Long task released lock." << endl;
}

void try_task(int& rx, timed_mutex& tmtx) 
{
    // 尝试在 100ms 内获取锁
    if (tmtx.try_lock_for(chrono::milliseconds(100))) 
    {
        cout << "Try task acquired lock within timeout." << endl;
        ++rx;
        tmtx.unlock();
    }
    else 
    {
        cout << "Try task could NOT acquire lock within 100ms." << endl;
    }
}

int main() 
{
    int x = 0;
    timed_mutex tmtx;

    thread t1(long_task, ref(x), ref(tmtx));
    // 让 t1 先启动并加锁
    this_thread::sleep_for(chrono::milliseconds(10));
    thread t2(try_task, ref(x), ref(tmtx));

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

    cout << "Final x = " << x << endl;
    return 0;
}

std::recursive_timed_mutex ------ "全能型"但最昂贵

用法:结合了递归和超时两种特性。当你既需要递归加锁,又需要超时控制时使用。

  • 特点

    • 允许递归:同一线程可多次锁定。

    • 支持超时 :拥有 try_lock_for/until 方法。

    • 开销最大:需要同时维护所有权计数和超时机制。

示例代码:

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
#include <mutex>    // for recursive_timed_mutex
#include <thread>
#include <chrono>
#include <functional>
using namespace std;

bool recursive_timed_increment(int n, int& rx, 
                recursive_timed_mutex& rtmtx, int depth = 0) 
{
    // 尝试在 50ms 内获取锁
    if (!rtmtx.try_lock_for(chrono::milliseconds(50))) 
    {
        cout << "Depth " << depth << " failed to acquire lock (timeout)." << endl;
        return false; // 获取锁失败,终止递归
    }

    cout << "Depth " << depth << " acquired lock." << endl;
    ++rx;

    bool ok = true;
    if (depth < n) 
    {
        ok = recursive_timed_increment(n, rx, rtmtx, depth + 1);
    }

    rtmtx.unlock();
    return ok;
}

int main() 
{
    int x = 0;
    recursive_timed_mutex rtmtx;

    // 线程1:深度3,可能因超时而无法完成
    thread t1(recursive_timed_increment, 3, ref(x), ref(rtmtx));
    // 线程2:深度2,也可能受超时影响
    thread t2(recursive_timed_increment, 2, ref(x), ref(rtmtx));

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

    cout << "Final x = " << x << endl;
    return 0;
}
类名 核心成员函数/表达式 参数与行为说明
mutex lock() 无参数。线程调用时,若锁未被占有则获取锁并继续;否则阻塞等待。
try_lock() 无参数 。尝试获取锁,立即返回bool值:true表示成功获取,false表示已被其他线程占有。
unlock() 无参数 。释放锁。必须由成功locktry_lock的同一线程调用
recursive_mutex lock() / try_lock() / unlock() 接口与mutex完全相同 ,但语义不同:同一线程可多次调用lock()/try_lock(),内部有计数器,调用相应次数unlock()后才真正释放。
timed_mutex lock() / try_lock() / unlock() 接口与mutex相同,行为也一致(非递归)。
try_lock_for(rel_time) 参数std::chrono::duration类型的时间长度。在指定时间内尝试获取锁,若超时则返回false
try_lock_until(abs_time) 参数std::chrono::time_point类型的绝对时间点。在该时间点前尝试获取锁,超时返回false
recursive_timed_mutex 上述所有接口 结合了recursive_mutex的递归语义和timed_mutex的超时接口。

lock_guard

相关文档:lock_guard - C++ Reference

lock_guard是C++提供的一种RAIII风格的锁管理类。这样可以更有效的防止因为异常等

原因导致的死锁问题。

  • 特点

    1. 不可复制、不可移动:一旦创建,就唯一地管理着锁。

    2. 锁定策略唯一 :构造时必须且只能 立即锁定互斥量(或接受一个已锁定状态的 std::adopt_lock 标签)。

    3. 无额外接口:除了构造和析构,没有提供任何其他成员函数(如手动解锁、尝试锁定等)。

  • 适用场景绝大多数简单的、作用域明确的临界区。当你确定只需要在某个作用域内持有锁,且不需要提前释放或尝试加锁时,它是最佳选择,性能损耗最小。

示例代码:

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
#include<mutex>
#include<thread>
using namespace std;

int main()
{
	int x = 0;
	mutex mtx;
	auto Print = [&x,&mtx](size_t n) {
			lock_guard<mutex> lock(mtx);
			for (size_t i = 0; i < n; ++i)
			{
				++x;
			}
		};
	thread t1(Print,10000);
	thread t2(Print, 20000);
	t1.join();
	t2.join();
	cout << x << endl;
	return 0;
}

用了lock_guard后,就不需要手动的取lock和unlock。就像前面学习的智能指针一样。

unique_lock

相关的官方文档:unique_lock - C++ Reference

std::unique_lock 是 C++ 标准库中一个非常灵活的 RAII 锁管理类模板。它在 std::lock_guard 的基础上,提供了更多控制互斥量的能力,尤其是在需要延迟锁定、尝试锁定、定时锁定、与条件变量配合 以及转移锁所有权的场景下不可或缺。

std::unique_lock 管理一个互斥量(如 std::mutex)的所有权 。它始终独占式 地管理着它所关联的互斥量(不能像 shared_lock 那样共享)。相比于lock_guard最大的特点是可定制化,非常的灵活,那么它可以用哪些参数呢?

主要有以下三个:std::adopt_lock_t、std::defer_lock_t、std::try_to_lock_t

标签常量 构造函数行为 典型使用场景
std::defer_lock 构造时 锁定互斥量 。互斥量初始为未锁定状态,你需要后续手动调用 lock() 成员函数。 需要延迟加锁 ,例如后续可能根据条件才加锁,或者需要与 std::lock 函数一起一次性锁定多个互斥量以避免死锁。
std::try_to_lock 构造时尝试锁定互斥量 (调用 mutex.try_lock()),但不会阻塞 。你可以通过 owns_lock() 成员函数检查是否成功获得了锁。 需要尝试加锁而不希望阻塞线程,如果获取失败则执行其他路径。
std::adopt_lock 假设当前线程已经持有该互斥量的锁。构造时不再尝试加锁,只负责在析构时自动解锁。 当你已经通过其他方式(如手动 lock()std::lock)获得了锁,但希望用 RAII 对象来管理其生命周期(确保最终解锁)。

unique_lock相关的接口

unique_lock 还提供了以下成员函数,让你可以像操作原始互斥量一样控制锁:

  • lock() :调用关联互斥量的 lock(),若锁已被其他线程持有则阻塞。

  • try_lock() :调用关联互斥量的 try_lock(),尝试加锁并立即返回 bool 值。

  • try_lock_for(rel_time) / try_lock_until(abs_time) :针对支持超时的互斥量(如 std::timed_mutex),在指定时间内尝试获取锁。

  • unlock() :显式解锁互斥量。这在临界区较大、中间有不需加锁的耗时操作时非常有用,可以先解锁以提高并发性,后续再 lock()

  • release()返回指向管理的互斥量的指针 ,并释放所有权 。之后,unique_lock 对象不再与该互斥量关联,互斥量在释放时不会自动解锁!这是一个危险但有时必要的底层操作,需谨慎使用。

  • owns_lock() :返回当前 unique_lock 对象是否拥有一个已锁定的互斥量。

unique_lock与 std::condition_variable 配合

这是 unique_lock 最核心、最不可或缺的应用场景。std::condition_variablewait() 方法必须 接受一个 std::unique_lock<std::mutex> 参数。原因在于 wait 操作的原子性:

  1. 线程调用 wait 时,必须已经持有锁(由 unique_lock 保证)。

  2. wait 内部会原子地 将线程阻塞,并解锁互斥量,允许其他线程修改共享数据并通知。

  3. 当线程被唤醒后,wait 返回前会重新获取锁 (再次通过 unique_lock 加锁),确保线程重新持有锁再继续执行。

示例代码:

默认构造

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
#include <mutex>
#include <thread>
#include <vector>

using namespace std;

//默认构造------相当于lock_guard,立即锁定互斥量,析构时自动解锁
int main() 
{
    int counter = 0;
    mutex mtx;
    vector<thread> threads;

    for (int i = 0; i < 10; ++i) 
    {
        threads.emplace_back([&] {
                // 默认构造:立即锁定 mtx,析构时自动解锁
                unique_lock<mutex> lock(mtx);
                for (int j = 0; j < 100; ++j) 
                {
                    ++counter;
                }
            });
    }

    for (auto& t : threads) t.join();
    cout << "Counter after 10 threads (each ++ 100 times): " << counter << endl;
    return 0;
}

结果为:

defer_lock

cpp 复制代码
//deferlock
int main() 
{
    int counter = 0;
    mutex mtx;

    thread t([&] {
        // 使用 defer_lock,构造时不锁定
        unique_lock<mutex> lock(mtx, defer_lock);

        // 做一些不需要锁的预处理
        cout << "  Thread " << this_thread::get_id() << " is preprocessing...\n";
        this_thread::sleep_for(chrono::milliseconds(10));

        // 手动锁定,进入临界区
        lock.lock();
        for (int i = 0; i < 500; ++i) {
            ++counter;
        }
        // 手动解锁,提前释放锁
        lock.unlock();

        // 后处理,不需要锁
        cout << "  Thread " << this_thread::get_id() << " is postprocessing...\n";
        // 锁已释放,其他线程可进入
        });

    t.join();
    cout << "Counter after thread: " << counter << endl;
    return 0;
}

结果为:

try_to_lock

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
#include <mutex>
#include <thread>
#include <vector>
#include <atomic>
#include <chrono>
using namespace std;
//try_to_lock
int main() 
{
    int counter = 0;
    mutex mtx;
    atomic<bool> stop(false);

    thread t([&] {
        while (!stop) {
            // 使用 try_to_lock,尝试锁定但不阻塞
            unique_lock<mutex> lock(mtx, try_to_lock);
            if (lock.owns_lock()) { // 检查是否成功获得锁
                ++counter;
                // 模拟工作
                this_thread::sleep_for(chrono::milliseconds(1));
                if (counter >= 500) {
                    stop = true;
                }
            }
            else {
                // 未获得锁,让出 CPU 时间片,避免忙等待
                this_thread::yield();
            }
        }
        cout << "  Thread " << this_thread::get_id() << " finished, counter = " << counter << "\n";
        });

    // 主线程也尝试获取锁,增加竞争
    for (int i = 0; i < 10; ++i) {
        this_thread::sleep_for(chrono::milliseconds(2));
        unique_lock<mutex> lock(mtx, try_to_lock);
        if (lock.owns_lock()) {
            ++counter;
            cout << "  Main thread got lock, counter now " << counter << "\n";
        }
    }

    t.join();
    cout << "Final counter: " << counter << endl;
    return 0;
}

结果为:

adopt_lock + std::lock 同时锁定多个互斥量

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
#include <mutex>
#include <thread>
#include <vector>
#include <atomic>
#include <chrono>
using namespace std;
int main() 
{
    int counterA = 0, counterB = 0;
    mutex mtxA, mtxB;

    thread t([&] {
        // 使用 defer_lock,构造时不锁定
        unique_lock<mutex> lockA(mtxA, defer_lock);
        unique_lock<mutex> lockB(mtxB, defer_lock);

        // 一次性安全锁定两个 mutex,避免死锁
        lock(lockA, lockB);

        // 现在两个锁都被持有,操作共享数据
        ++counterA;
        ++counterB;

        // 可以手动提前解锁其中一个
        lockA.unlock(); // 此时仍持有 lockB

        // 做一些只需要 lockB 的操作
        ++counterB;

        // lockB 会在析构时自动解锁
        cout << "  Thread " << this_thread::get_id() << " updated counters: A=" << counterA << ", B=" << counterB << "\n";
        });

    t.join();
    cout << "After thread: counterA=" << counterA << ", counterB=" << counterB << endl;
    return 0;
}

结果为:

与条件变量配合

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
#include <mutex>
#include <thread>
#include <vector>
#include <condition_variable>
#include <atomic>
#include <chrono>
using namespace std;
//与条件变量配合
int main() 
{
    mutex cv_mtx;
    condition_variable cv;
    bool ready = false;
    int data = 0;

    thread producer([&] {
        this_thread::sleep_for(chrono::milliseconds(100));
        {
            lock_guard<mutex> lk(cv_mtx); // 也可以使用 unique_lock,但 lock_guard 足够
            data = 42;
            ready = true;
            cout << "  Producer set data to 42 and notifies.\n";
        }
        cv.notify_one(); // 唤醒等待的线程
        });

    thread consumer([&] {
        // 必须使用 unique_lock,因为 wait 内部需要临时解锁
        unique_lock<mutex> lk(cv_mtx);
        cout << "  Consumer waiting...\n";
        cv.wait(lk, [&] { return ready; }); // 自动解锁并阻塞,被唤醒后重新锁定
        cout << "  Consumer woke up, data = " << data << "\n";
        });

    producer.join();
    consumer.join();
    cout << "Condition variable example finished.\n";
    return 0;
}

结果为:

lock和try_lock

std::lockstd::try_lock 是 C++11 标准库 <mutex> 头文件中提供的两个变参函数模板 ,用于一次性操作多个互斥量 (或任何可锁对象)。它们的主要目的是简化多个锁的获取过程 ,并帮助开发者避免因锁顺序不一致而导致的死锁

lock

cpp 复制代码
template <class Lockable1, class Lockable2, class... LockableN>
void lock(Lockable1& l1, Lockable2& l2, LockableN&... ln);

作用

  • 阻塞当前线程,直到同时获取所有传入的互斥量的锁。

  • 采用一种避免死锁的算法(例如,使用"尝试并回退"策略:尝试锁定所有,若失败则释放已持有的锁并重新尝试,以确保不会出现线程互相等待的死锁局面)。

  • 如果其中任何一个互斥量的 lock() 操作抛出异常,则已锁定的互斥量会被解锁,然后异常继续传播。

示例代码:

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
#include <chrono>
#include <mutex>
#include <thread>
//lock 和 try_lock 是 C++11 中提供的两种锁定多个互斥量的机制,主要用于避免死锁问题。

//lock
struct Account {
    std::mutex mtx;
    int balance = 0;
};

int main() 
{
    Account a, b;
    a.balance = 100;
    b.balance = 50;

    // 线程1:从 a 转 30 到 b
    std::thread t1([&] {
        std::lock(a.mtx, b.mtx);                         // 同时锁定两个互斥量
        std::lock_guard<std::mutex> lock_a(a.mtx, std::adopt_lock);
        std::lock_guard<std::mutex> lock_b(b.mtx, std::adopt_lock);
        a.balance -= 30;
        b.balance += 30;
        });

    // 线程2:从 b 转 20 到 a
    std::thread t2([&] {
        std::lock(b.mtx, a.mtx);                         // 顺序不同但 std::lock 保证安全
        std::lock_guard<std::mutex> lock_b(b.mtx, std::adopt_lock);
        std::lock_guard<std::mutex> lock_a(a.mtx, std::adopt_lock);
        b.balance -= 20;
        a.balance += 20;
        });

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

    std::cout << "Final balances: a=" << a.balance << ", b=" << b.balance << std::endl;
    return 0;
}

结果为:

try_lock

cpp 复制代码
template <class Lockable1, class Lockable2, class... LockableN>
int try_lock(Lockable1& l1, Lockable2& l2, LockableN&... ln);

作用

  • 依次对每个互斥量调用 try_lock()(非阻塞尝试)。

  • 如果全部成功 ,返回 -1,并且所有互斥量都被当前线程锁定。

  • 如果某个 try_lock 失败(返回 false),则立即停止 ,并对之前已锁定的互斥量依次调用 unlock() 释放它们,然后返回失败互斥量的索引(从 0 开始计数)。

  • 若在尝试过程中有异常抛出,则已锁定的互斥量会被解锁,然后异常继续传播。

示例代码:

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
#include <chrono>
#include <mutex>
#include <thread>
std::mutex mtx1, mtx2;

int main() 
{
    // 线程1:lambda 直接嵌入,id 通过参数传递
    std::thread t1([](int id) {
        int idx = std::try_lock(mtx1, mtx2);
        if (idx == -1) {
            std::cout << "Thread " << id << " got both locks, working...\n";
            std::this_thread::sleep_for(std::chrono::milliseconds(100));
            mtx1.unlock();
            mtx2.unlock();
        }
        else {
            std::cout << "Thread " << id << " failed to acquire lock " << idx << ", do something else.\n";
        }
        }, 1);

    // 线程2:同样使用 lambda,id=2
    std::thread t2([](int id) {
        int idx = std::try_lock(mtx1, mtx2);
        if (idx == -1) {
            std::cout << "Thread " << id << " got both locks, working...\n";
            std::this_thread::sleep_for(std::chrono::milliseconds(100));
            mtx1.unlock();
            mtx2.unlock();
        }
        else {
            std::cout << "Thread " << id << " failed to acquire lock " << idx << ", do something else.\n";
        }
        }, 2);

    t1.join();
    t2.join();
    return 0;
}

结果为:

atomic

官方文档:atomic - C++ Reference

在多线程程序中,如果多个线程同时读写同一个变量,可能会产生数据竞争。这里就需要原子操作

原子操作是不可分割的操作,即一旦开始执行,就不会被其他线程中断,从而避免了数据竞争(data race)和未定义行为。在前面我们学习<pthread.h>的时候原子操作是依赖于条件变量和互斥量来实现的,并不能直接的进行原子操作,且这样开销较大。

而在C++11中提供的<atmoic>则提供了一套原子操作 类型和相关函数,用于在多线程环境中对共享数据进行无锁(lock-free) 或**免锁(lock-based but atomic)**的访问。

<atomic> 头文件提供了 std::atomic 类模板,以及针对内置类型的特化别名。

atomic模板特化

1. std::atomic<T> 模板

cpp 复制代码
template< class T >
struct atomic;

可用于任意可平凡复制(trivially copyable)的类型 T。对于整数类型和指针,标准库提供了特化,增加了算术操作(如 fetch_add)。

2. 常用特化别名

为了方便使用,C++ 定义了一组别名:

别名 完整类型
std::atomic_bool std::atomic<bool>
std::atomic_char std::atomic<char>
std::atomic_int std::atomic<int>
std::atomic_uint std::atomic<unsigned int>
std::atomic_long std::atomic<long>
std::atomic_ulong std::atomic<unsigned long>
std::atomic_llong std::atomic<long long>
std::atomic_ullong std::atomic<unsigned long long>
std::atomic_size_t std::atomic<size_t>
std::atomic_ptrdiff_t std::atomic<ptrdiff_t>
std::atomic<T*> 指针类型的特化

3. 原子类型的特性

  • 不可复制、不可移动 :原子对象不能拷贝构造或拷贝赋值,但可以通过 loadstore 读写其值。

  • is_lock_free() :成员函数,返回 true 表示该原子类型的操作是无锁 的(即直接使用硬件指令),否则可能使用内部锁。通常,对于内置类型(如 intvoid*)都是无锁的。

1. std::is_trivially_copyable<T>::value

  • 作用 :判断类型 T 是否为可平凡复制(trivially copyable)

  • 含义 :一个可平凡复制的类型意味着它的对象可以通过直接复制其底层字节(例如用 memcpy)来安全地创建副本,而不需要调用任何构造函数、析构函数或赋值运算符。具体来说,该类型必须满足:

    • 每个拷贝构造函数、移动构造函数、拷贝赋值运算符、移动赋值运算符都是平凡的(即编译器提供的默认实现,且不执行任何非平凡操作)。

    • 有平凡的析构函数。

    • 没有虚函数或虚基类。

  • 用途 :常用于判断能否安全地使用 memcpy 复制对象,或者用于序列化、低延迟通信等场景。例如,在编写序列化库时,可以对可平凡复制的类型直接复制内存,否则需要逐个成员序列化。

2. std::is_copy_constructible<T>::value

  • 作用 :判断类型 T 是否可拷贝构造

  • 含义 :检查是否存在一个可访问的拷贝构造函数(可以是用户定义的或默认的)。对于引用类型、具有删除或私有拷贝构造函数的类型,该特征为 false

  • 用途:用于在模板中要求类型必须支持拷贝构造,或者根据是否可拷贝构造来选择不同的实现。例如,在实现容器时,可能需要元素类型支持拷贝构造才能进行插入操作。

3. std::is_move_constructible<T>::value

  • 作用 :判断类型 T 是否可移动构造

  • 含义:检查是否存在可访问的移动构造函数。类似拷贝构造,但针对移动语义。

  • 用途 :用于优化,如果类型支持移动构造,可以在某些操作(如 std::vector 重新分配)中使用移动而非拷贝,提高性能。也常用于实现完美转发或条件启用移动操作。

4. std::is_copy_assignable<T>::value

  • 作用 :判断类型 T 是否可拷贝赋值

  • 含义 :检查是否存在可访问的拷贝赋值运算符(operator=)。

  • 用途:与拷贝构造类似,用于需要赋值操作的泛型代码中。

5. std::is_move_assignable<T>::value

  • 作用 :判断类型 T 是否可移动赋值

  • 含义:检查是否存在可访问的移动赋值运算符。

  • 用途:用于启用移动赋值相关的优化或约束。

6. std::is_same<T, typename std::remove_cv<T>::type>::value

  • 作用 :判断类型 T 在去除顶层 constvolatile 限定符后是否与自身相同。

  • 拆解

    • std::remove_cv<T>::type:移除 T 的顶层 constvolatile 限定符(如果存在)。

    • std::is_same<A, B>::value:判断 AB 是否为同一类型。

    • 整体表达式检查 T 是否没有 顶层 const/volatile 限定符。

  • 等价于!std::is_const<T>::value && !std::is_volatile<T>::value

  • 用途 :常用于确保类型是非限定(unqualified)的,例如在模板中需要接收非 const 类型,或者在进行某些类型操作前先剥离限定符。

示例代码:

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS
#include <atomic>
#include <iostream>
#include <thread>
#include <vector>

using namespace std;
//原子操作示例2:
struct Date
{
    int _year = 1;
    int _month = 1;
    int _day = 1;
};

template<class T>
void check()
{
    cout << typeid(T).name() << endl;
    cout << std::is_trivially_copyable<T>::value << endl;
    cout << std::is_copy_constructible<T>::value << endl;
    cout << std::is_move_constructible<T>::value << endl;
    cout << std::is_copy_assignable<T>::value << endl;
    cout << std::is_move_assignable<T>::value << endl;
    cout << std::is_same<T, typename std::remove_cv<T>::type>::value << endl << endl;
}

 int main()
 {
     check<int>();
     check<double>();
     check<int*>();
     check<Date>();
     check<Date*>();
     check<string>();
     check<string*>();

     return 0;
 }

结果为:

cpp 复制代码
int
1
1
1
1
1
1

double
1
1
1
1
1
1

int * __ptr64
1
1
1
1
1
1

struct Date
1
1
1
1
1
1

struct Date * __ptr64
1
1
1
1
1
1

class std::basic_string<char,struct std::char_traits<char>,class std::allocator<char> >
0
1
1
1
1
1

class std::basic_string<char,struct std::char_traits<char>,class std::allocator<char> > * __ptr64
1
1
1
1
1
1

原因:

只有 std::string 本身不是平凡可复制的,其他类型(包括内置类型、指针、自定义的平凡结构体)都是平凡可复制的。

所有类型均支持拷贝/移动构造和赋值(因为这里没有不可拷贝的类型)。

所有类型去除 cv 限定后均为自身(无 const/volatile)。

atomic相关接口

1. 构造函数

atomic() noexcept = default;:默认构造,值未初始化(除非是静态或线程局部变量,会进行零初始化)。

constexpr atomic( T desired ) noexcept;:用 desired 初始化。

atomic( const atomic& ) = delete;:禁止拷贝。

2. load ------ 读取值

cpp 复制代码
T load( std::memory_order order = std::memory_order_seq_cst ) const noexcept;

返回原子对象的当前值。order 指定内存序(见后)。

3. store ------ 写入值

cpp 复制代码
void store( T desired, std::memory_order order = std::memory_order_seq_cst ) noexcept;

将值设为 desired

4. exchange ------ 原子地交换

cpp 复制代码
T exchange( T desired, std::memory_order order = std::memory_order_seq_cst ) noexcept;

将值设为 desired 并返回旧值

5. 算术操作(仅整数和指针特化)

对于整数特化和指针特化,提供以下原子算术操作:

  • fetch_add:原子地增加,返回旧值。

  • fetch_sub:原子地减少,返回旧值。

  • ++-- 运算符(前缀和后缀)也支持。

  • fetch_andfetch_orfetch_xor(整数特化):原子位操作。

atomic中的CAS操作

CAS 的全称是 Compare-And-Swap (比较并交换),它是 CPU 提供的一条原子指令。核心作用就是如果当前值等于我期望的值,我就把它改成新值;否则告诉我当前值是多少

相关的接口

cpp 复制代码
bool compare_exchange_weak(T& expected, T desired,
                           std::memory_order success,
                           std::memory_order failure) noexcept;

bool compare_exchange_strong(T& expected, T desired,
                             std::memory_order success,
                             std::memory_order failure) noexcept;

参数:

  • expected :一个引用,既是输入也是输出

    • 输入时,你给它一个你"期望"当前原子变量应该等于的值。

    • 如果 CAS 失败(即当前值不等于期望值),函数会把原子变量的当前值写回 expected,让你知道它现在到底是多少。

    • 所以调用完 CAS 后,你一般要检查 expected 是否被更新了。

  • desired :你想设置的新值。如果当前值等于 expected,就把原子变量改成这个值。

  • success :内存顺序,用于成功 更新时的同步语义。例如 memory_order_release 表示释放语义。

  • failure :内存顺序,用于失败 时的加载语义。例如 memory_order_acquire 表示获取语义。

    注意:failure 不能是 memory_order_releasememory_order_acq_rel,而且不能比 success 更强(具体看标准)。

返回值:

  • true :比较成功,原子变量被更新为 desired

  • false :比较失败,原子变量保持不变,并且 expected 被更新为原子变量的当前值。

两个函数的区别(weak vs strong

  • weak :允许伪失败 (spurious failure)。什么意思?就是即使原子变量当前值等于 expected,在某些平台上(比如某些 RISC 架构)也可能因为指令重试等原因返回 false。因此,weak 通常需要放在循环里,失败就重试。

  • strong :保证不会伪失败。如果当前值等于 expected,一定返回 true;否则返回 false 并更新 expected

那什么时候用 weak

当你已经在循环里重试时,用 weak 可能性能更好(某些平台上它生成的代码更紧凑)。比如无锁栈的 push 操作,通常长这样:

cpp 复制代码
Node* new_node = new Node(value);
new_node->next = head.load();
while (!head.compare_exchange_weak(new_node->next, new_node))
    ; // 失败说明 head 被改了,new_node->next 已经被更新为最新的 head,继续尝试

示例:CAS实现原子加法

cpp 复制代码
#include <atomic>
#include <iostream>

std::atomic<int> counter{0};

void atomic_increment(int delta) 
{
    int old = counter.load();
    while (!counter.compare_exchange_weak(old, old + delta)) 
    {
        // 如果 CAS 失败,old 已经被更新为 counter 的当前值,循环继续尝试
    }
}

int main() {
    atomic_increment(5);
    std::cout << counter.load() << std::endl;  // 输出 5
    return 0;
}

示例:原子计数器和非原子计数器

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS
#include <atomic>
#include <iostream>
#include <thread>
#include <vector>

using namespace std;

atomic<int> acnt;
// atomic_int acnt;   // 另一种写法
int cnt;

void Add1(atomic<int>& cnt)
{
    int old = cnt.load();
    // 如果cnt的值跟old相等,则将cnt的值设置为old+1,并且返回true,这组操作是原子的。
    // 如果在load和compare_exchange_weak操作之间cnt对象被其他线程改了,
    // 则old和cnt不相等,函数会将old的值更新为cnt的当前值,并返回false。
    while (!atomic_compare_exchange_weak(&cnt, &old, old + 1));
    // while (!cnt.compare_exchange_weak(old, old + 1));  // 成员函数版本
}

void f()
{
    for (int n = 0; n < 100000; ++n)
    {
        ++acnt;
        // 用CAS模拟atomic的operator++的原子操作
        // Add1(acnt);
        ++cnt;
    }
}

int main()
{
    std::vector<thread> pool;
    for (int n = 0; n < 4; ++n)
        pool.emplace_back(f);
    for (auto& e : pool)
        e.join();

    cout << "原子计数器为 " << acnt << '\n'
        << "非原子计数器为 " << cnt << '\n';
    return 0;
}

结果为:

示例:全局链表

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS
#include <atomic>
#include <iostream>
#include <thread>
#include <vector>

using namespace std;
// 一个简单的全局链表:
struct Node
{
    int value;
    Node* next;
};

std::atomic<Node*> list_head(nullptr);

void append(int val, int n)
{
    // std::this_thread::sleep_for(std::chrono::seconds(1));
    for (int i = 0; i < n; i++)
    {
        // 向链表追加一个元素
        Node* oldHead = list_head;
        Node* newNode = new Node{ val + i, oldHead };

        // 以下循环等价于:list_head = newNode,但是线程安全的方式:
        while (!list_head.compare_exchange_weak(oldHead, newNode))
            newNode->next = oldHead;
    }
}

int main()
{
    // 生成10个线程来填充链表:
    std::vector<std::thread> threads;
    threads.emplace_back(append, 0, 10);
    threads.emplace_back(append, 20, 10);
    threads.emplace_back(append, 30, 10);
    threads.emplace_back(append, 40, 10);

    for (auto& th : threads)
        th.join();

    // 打印内容:
    for (Node* it = list_head; it != nullptr; it = it->next)
        std::cout << ' ' << it->value;
    std::cout << '\n';

    // 清理:
    Node* it;
    while (it = list_head)
    {
        list_head = it->next;
        delete it;
    }

    return 0;
}

atomic特化成员函数

成员函数 参数 返回值 作用
fetch_add (T arg, memory_order order = memory_order_seq_cst) T(旧值) 原子地将当前值增加 arg,返回旧值
fetch_sub (T arg, memory_order order = memory_order_seq_cst) T(旧值) 原子地将当前值减少 arg,返回旧值
fetch_and (T arg, memory_order order = memory_order_seq_cst) T(旧值) 原子地将当前值与 arg 按位与,返回旧值
fetch_or (T arg, memory_order order = memory_order_seq_cst) T(旧值) 原子地将当前值与 arg 按位或,返回旧值
fetch_xor (T arg, memory_order order = memory_order_seq_cst) T(旧值) 原子地将当前值与 arg 按位异或,返回旧值
operator++ (前缀) () T(新值) 原子地前置递增,返回递增后的值
operator++ (后缀) (int) T(旧值) 原子地后置递增,返回递增前的值
operator-- (前缀) () T(新值) 原子地前置递减,返回递减后的值
operator-- (后缀) (int) T(旧值) 原子地后置递减,返回递减前的值
operator+= (T arg) T(新值) 原子地将当前值增加 arg,返回新值
operator-= (T arg) T(新值) 原子地将当前值减少 arg,返回新值
operator&= (T arg) T(新值) 原子地将当前值与 arg 按位与,返回新值
**`operator =`** (T arg) T(新值)
operator^= (T arg) T(新值) 原子地将当前值与 arg 按位异或,返回新值

补充说明

适用范围 :以上成员函数仅存在于 std::atomic 的整数特化(如 atomic<int>atomic<unsigned long> 等)和指针特化(atomic<T*>)中。对于指针类型,fetch_add/fetch_sub 操作的是 ptrdiff_t,即指针移动的步长(按 sizeof(T) 计算),且没有位操作fetch_and 等仅适用于整数)。

内存序参数 :所有 fetch_xxx 函数均接受可选的 memory_order 参数,用于控制操作的同步语义。复合赋值运算符(如 +=)使用默认的顺序一致内存序,不支持自定义内存序。

atomic内存序

在多线程环境下,一个线程对共享变量的修改,何时能被其他线程看到?看到的顺序是否与代码顺序一致?这就是内存序要保证的内容:可见性与顺序性

C++的<atomic>库默认提供了6种内存序

内存序

memory_order_relaxed**(最弱,无同步)**

  • 语义:仅保证原子操作本身的不可分割性,不提供任何跨线程的同步或顺序保证。编译器与CPU可自由重排该操作前后的内存访问。

  • 可见性 :不同线程间对同一变量的relaxed操作,其修改顺序仅在线程内一致,线程间可能观察到不同的顺序(即"因果一致性")。

  • 适用场景:统计计数器、引用计数等只要求原子性、不要求顺序约束的场景。例如:

    cpp 复制代码
    counter.fetch_add(1, std::memory_order_relaxed);

memory_order_consume(已弃用,目前极少使用)

  • 语义 :旨在处理数据依赖的同步。若A操作consume一个从B操作release的变量,且后续代码依赖该变量的值,则保证依赖链上的内存访问不会被重排到consume之前。

  • 缺陷 :由于实现复杂且编译器难以精确识别依赖,C++17起已被标记为不建议使用,建议用acquire替代。此处仅作了解。

  • 适用场景:理论上适用于读大多数数据结构的指针(如RCU),但实践中避免使用。

memory_order_acquire(用于读操作,如load)

  • 语义 :用在"读-获取"操作(如load)中。保证:本线程中所有后续的读/写操作(包括普通变量)都不会被重排到该acquire操作之前。

  • 可见性 :与同一个原子变量的release操作配对,确保release之前的所有内存写入,对执行acquire的线程可见。

  • 适用场景:实现锁、消息传递(消费者读取生产者发布的数据)。典型模式:

cpp 复制代码
// 线程1(发布)
data = 100;
flag.store(true, std::memory_order_release);

// 线程2(获取)
while (!flag.load(std::memory_order_acquire));
assert(data == 100);  // 保证可见

        memory_order_release(用于写操作,如store)
  • 语义 :用在"写-释放"操作(如store)中。保证:本线程中所有之前的读/写操作(包括普通变量)都不会被重排到该release操作之后。

  • 可见性 :与acquire配对,使release之前的所有内存写入对执行acquire的线程可见。

  • 适用场景:同上,生产者发布数据。

memory_order_acq_rel(用于读-改-写操作,如fetch_add)

  • 语义 :同时具有acquirerelease语义。即:该操作之前的普通内存访问不能被重排到之后,之后的不能被重排到之前;且对同一原子变量的release/acquire序列形成同步。

  • 适用场景 :对同一个原子变量同时进行读写并需要双向同步的操作,例如无锁栈的push/pop中的compare_exchange。

memory_order_seq_cst(最强,默认)

  • 语义 :顺序一致性。除了包含acq_rel的所有保证外,还强制所有线程观察到的所有原子操作(以seq_cst标记的)具有全局一致的顺序(即所有线程看到的修改顺序相同)。这是最直观、最易理解的模型,但开销最大(可能涉及内存屏障或缓存刷新)。

  • 适用场景:默认选项,当不确定或需要强一致性时使用,例如实现互斥锁、单次初始化等。由于性能影响,在性能敏感代码中应考虑降级。

内存序与同步关系总结

  • Relaxed:无跨线程同步。

  • Release/Consume:已弃用,忽略。

  • Release/Acquire:形成"释放-获取"同步,保证数据依赖的可见性。

  • Release/Consume(理论):数据依赖同步,但实践用acquire。

  • Acq_rel:用于RMW操作,兼顾读写同步。

  • Seq_cst:全局顺序一致,最强的同步。

总结表格

内存序 类别 同步方向 保证强度 典型使用场景 性能影响
memory_order_relaxed 无同步 仅原子性,无顺序约束 计数器、统计、引用计数 最低(无屏障)
memory_order_consume 获取(已弃用) 数据依赖同步 仅保证依赖链有序 理论上RCU,实践中避免 依赖于实现
memory_order_acquire 获取 与release配对 禁止后续操作重排到之前 锁、消费者读取数据 较低(读屏障)
memory_order_release 释放 与acquire配对 禁止之前操作重排到之后 锁、生产者发布数据 较低(写屏障)
memory_order_acq_rel 获取+释放 双向同步 结合acquire和release RMW操作(如exchange、CAS) 中等(全屏障)
memory_order_seq_cst 顺序一致 全局一致 所有线程观察顺序相同 默认选项、强同步场景 最高(可能含全屏障)

示例代码

cpp 复制代码
#include <iostream>      // std::cout
#include <atomic>        // std::atomic
#include <thread>        // std::thread
#include <mutex>         // std::mutex
#include <vector>        // std::vector

template<typename T>
struct node
{
    T data;
    node* next;
    node(const T& data) : data(data), next(nullptr) {}
};

namespace lock_free
{
    template<typename T>
    class stack
    {
    public:
        std::atomic<node<T>*> head = nullptr;

    public:
        void push(const T& data)
        {
            node<T>* new_node = new node<T>(data);
            // 将 head 的当前值放到 new_node->next 中
            new_node->next = head.load(std::memory_order_relaxed);
            // 现在令 new_node 为新的 head ,但如果 head 不再是
            // 存储于 new_node->next 的值(某些其他线程必须在刚才插入结点)
            // 那么将新的 head 放到 new_node->next 中并再尝试
            while (!head.compare_exchange_weak(new_node->next, new_node,
                                               std::memory_order_release,
                                               std::memory_order_relaxed))
                ; // 循环体为空
        }
    };
}

namespace lock
{
    template<typename T>
    class stack
    {
    public:
        node<T>* head = nullptr;

        void push(const T& data)
        {
            node<T>* new_node = new node<T>(data);
            new_node->next = head;
            head = new_node;
        }
    };
}

int main()
{
    lock_free::stack<int> st1;
    lock::stack<int> st2;
    std::mutex mtx;
    int n = 1000000;

    auto lock_free_stack = [&st1, n] {
        for (size_t i = 0; i < n; i++)
        {
            st1.push(i);
        }
    };

    auto lock_stack = [&st2, &mtx, n] {
        for (size_t i = 0; i < n; i++)
        {
            std::lock_guard<std::mutex> lock(mtx);
            st2.push(i);
        }
    };

    // 4个线程分别使用无锁方式和有锁方式插入n个数据到栈中进行性能对比
    size_t begin1 = clock();
    std::vector<std::thread> threads1;
    for (size_t i = 0; i < 4; i++)
    {
        threads1.emplace_back(lock_free_stack);
    }
    for (auto& th : threads1)
        th.join();
    size_t end1 = clock();
    std::cout << end1 - begin1 << std::endl;

    size_t begin2 = clock();
    std::vector<std::thread> threads2;
    for (size_t i = 0; i < 4; i++)
    {
        threads2.emplace_back(lock_stack);
    }
    for (auto& th : threads2)
        th.join();
    size_t end2 = clock();
    std::cout << end2 - begin2 << std::endl;

    return 0;
}

condition_variable

基本概念

std::condition_variable :必须与 std::unique_lock<std::mutex> 配合使用,因为 wait 操作需要原子地释放锁并进入等待状态。

std::condition_variable_any :可与任意符合 Lockable 要求的锁配合(如 std::shared_lock),但通常性能略低。

相关接口

wait家族

void wait( std::unique_lock<std::mutex>& lock );

  • 作用

    阻塞当前线程,直到被通知。调用前必须已持有与 lock 关联的互斥锁。该函数会原子地释放锁并阻塞线程。当被唤醒后,它会重新获取锁并返回。

  • 表达式

    cpp 复制代码
    void wait( std::unique_lock<std::mutex>& lock );
  • 参数

    • lock:对 std::unique_lock<std::mutex> 的左值引用,该锁必须已经锁定当前线程的互斥量。
  • 返回值

    无。

  • 注意

    返回时锁已被重新获取,但不保证条件成立,因此必须在循环中重新检查条件。这是为了应对虚假唤醒(spurious wakeup)。

template< class Predicate > void wait( std::unique_lock<std::mutex>& lock, Predicate pred );

  • 作用

    等价于 while (!pred()) wait(lock);。在等待期间,每次被唤醒后都会检查谓词 pred,只有当 pred() 返回 true 时才返回。这是推荐使用的形式,自动处理了虚假唤醒。

  • 表达式

    cpp 复制代码
    template< class Predicate >
    void wait( std::unique_lock<std::mutex>& lock, Predicate pred );
  • 参数

    • lock:同上。

    • pred:可调用对象(如函数、lambda、函数对象),返回 bool 值,用于判断条件是否满足。通常捕获共享状态(如 [&]{ return data_ready; })。

  • 返回值

    无。只有当 pred 返回 true 时才返回。

wait_for

cpp 复制代码
template< class Rep, class Period >
std::cv_status wait_for( std::unique_lock<std::mutex>& lock,
                         const std::chrono::duration<Rep, Period>& rel_time );

template< class Rep, class Period, class Predicate >
bool wait_for( std::unique_lock<std::mutex>& lock,
               const std::chrono::duration<Rep, Period>& rel_time,
               Predicate pred );
  • 作用

    阻塞当前线程,直到被通知或超过指定的相对超时时间。第二个重载(带谓词)相当于循环检查谓词,并处理超时,返回谓词的最终结果。

  • 参数

    • lock:同上。

    • rel_time:相对超时时间,如 std::chrono::seconds(1)

    • pred:谓词(仅第二重载)。

  • 返回值

    • 第一重载(无谓词):返回 std::cv_status::timeout 如果超时未收到通知;否则返回 std::cv_status::no_timeout

    • 第二重载(有谓词):返回 bool,表示谓词最终是否为 true。若超时且谓词为 false,则返回 false;若在超时前谓词变为 true,则返回 true

wait_until

cpp 复制代码
template< class Clock, class Duration >
std::cv_status wait_until( std::unique_lock<std::mutex>& lock,
                           const std::chrono::time_point<Clock, Duration>& abs_time );

template< class Clock, class Duration, class Predicate >
bool wait_until( std::unique_lock<std::mutex>& lock,
                 const std::chrono::time_point<Clock, Duration>& abs_time,
                 Predicate pred );
  • 作用

    wait_for 类似,但使用绝对时间点作为超时界限。

  • 参数

    • lock:同上。

    • abs_time:绝对超时时间点,如 std::chrono::steady_clock::now() + std::chrono::seconds(1)

    • pred:谓词(仅第二重载)。

  • 返回值

    wait_for 类似。

通知家族

void notify_one() noexcept;

  • 作用

    唤醒一个等待在该条件变量上的线程。如果有多个线程等待,选择其中一个(由调度策略决定)。

  • 表达式

    cpp 复制代码
    void notify_one() noexcept;
  • 参数

    无。

  • 返回值

    无。

  • 注意

    通常在修改共享条件后调用,以通知等待线程重新检查条件。可在持有锁或释放锁后调用,但必须保证对条件的修改在线程间可见(通常通过互斥锁保证)。

void notify_all() noexcept;

  • 作用

    唤醒所有等待在该条件变量上的线程。适用于"广播"式条件变化。

  • 表达式

    cpp 复制代码
    void notify_all() noexcept;
  • 参数

    无。

  • 返回值

    无。

总结与建议

接口 作用 核心参数 返回值 注意事项
wait(lock) 阻塞等待通知 unique_lock 必须循环检查条件
wait(lock, pred) 等待直到谓词为真 unique_lock, 谓词 自动处理虚假唤醒,推荐使用
wait_for/wait_until 带超时等待 锁, 超时时间/时间点, 可选谓词 cv_statusbool 配合谓词重载更安全
notify_one() 唤醒一个等待线程 建议在修改条件后尽快调用
notify_all() 唤醒所有等待线程 仅当需要广播时使用

与互斥锁的示例代码

cpp 复制代码
#include <iostream>
#include <queue>
#include <thread>
#include <mutex>
#include <condition_variable>

template<typename T>
class ThreadSafeQueue {
private:
    std::queue<T> queue_;
    std::mutex mtx_;
    std::condition_variable cv_;

public:
    // 生产者:向队列中添加元素
    void push(T value) {
        {
            std::lock_guard<std::mutex> lock(mtx_);  // 1. 锁定互斥量,保护共享队列
            queue_.push(std::move(value));
        } // 2. 锁在此处释放(lock_guard析构)
        cv_.notify_one();  // 3. 唤醒一个等待的消费者(可在锁外调用)
    }

    // 消费者:阻塞等待并取出元素
    T pop() {
        std::unique_lock<std::mutex> lock(mtx_);          // 4. 锁定互斥量
        cv_.wait(lock, [this] { return !queue_.empty(); }); // 5. 等待条件成立
        // 当 wait 返回时,锁已被重新获取,且队列非空
        T value = std::move(queue_.front());
        queue_.pop();
        return value;  // 6. 函数返回时,lock 析构自动释放锁
    }

    // 带超时的尝试弹出
    bool try_pop(T& value, std::chrono::milliseconds timeout) {
        std::unique_lock<std::mutex> lock(mtx_);
        if (!cv_.wait_for(lock, timeout, [this] { return !queue_.empty(); })) {
            return false;  // 超时且队列仍为空
        }
        value = std::move(queue_.front());
        queue_.pop();
        return true;
    }
};

// 使用示例
int main() {
    ThreadSafeQueue<int> tsq;

    std::thread producer([&] {
        for (int i = 0; i < 10; ++i) {
            tsq.push(i);
            std::this_thread::sleep_for(std::chrono::milliseconds(100));
        }
    });

    std::thread consumer([&] {
        for (int i = 0; i < 10; ++i) {
            int val = tsq.pop();
            std::cout << "Consumed: " << val << std::endl;
        }
    });

    producer.join();
    consumer.join();

    return 0;
}

结果为:

关于C++并行开发库的部分内容就先介绍到这里了,后续我们还会介绍它的后续内容喜欢请点个赞谢谢。

封面图如下:

相关推荐
Nightmare0041 小时前
切换conda环境的时候输出zstandard could not be imported. Running without .conda support.
开发语言·python·conda
Trouvaille ~1 小时前
【项目篇】从零手写高并发服务器(一):项目介绍与开发环境搭建
linux·运维·服务器·网络·c++·高并发·muduo库
weixin_395448911 小时前
build_fsd_luyan_from_rm——注释
开发语言·windows·python
lsx2024061 小时前
NumPy 算术函数
开发语言
程序员南飞1 小时前
算法笔试-求一个字符串的所有子串
java·开发语言·数据结构·python·算法·排序算法
秦jh_1 小时前
【C++】哈希扩展
开发语言·c++·哈希算法
开发者小天2 小时前
python中使用jupyter notebook 绘制正态分布直方图 密度图 小提琴图 模仿企鹅喙长分布图
开发语言·python·jupyter
大傻^2 小时前
Python机器学习实战:用机器学习进行情感分析 核心知识点总结
开发语言·python·机器学习
清风徐来QCQ2 小时前
java总结
java·开发语言·数据结构