1. thread

std::thread 是 C++11 标准引入的跨平台线程库组件,定义在 <thread> 头文件中,用于封装操作系统底层线程,简化多线程编程(替代传统的 pthread(Linux)、CreateThread(Windows)等平台相关 API)。

其核心目标是让开发者以统一的接口创建、管理线程,无需关注底层系统差异。

实际上,thread类的接口与使用方法与Linux下的pthread库十分相似:Linux笔记---线程控制

1.1 创建线程对象(启动线程)

std::thread 的构造函数接收一个可调用对象(函数、lambda、函数对象、成员函数)作为线程入口,后续参数作为该函数的参数。

template <class Fn, class... Args>
explicit thread (Fn&& fn, Args&&... args);

例如,创建两个线程分别执行 countDown函数:

#include <iostream>
#include <thread>
#include <chrono>
using namespace std;

void countDown(int n, int fin)
{
	for (int i = n; i >= fin; i--)
	{
		cout << this_thread::get_id() << ": " << i << endl;
		this_thread::sleep_for(chrono::seconds(1));
	}
}

int main()
{
	thread t1(countDown, 10, 1);
	thread t2(countDown, 20, 11);

	cout << "t1: " << t1.get_id() << endl;
	cout << "t2: " << t2.get_id() << endl;

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

	return 0;
}

1.2 线程管理核心接口

std::thread 提供三个关键接口管理线程生命周期:join()、detach()、joinable()(可能是考虑安全原因,并没有提供曲线线程的 cancel() 接口)。

  • join():阻塞等待子线程完成
    • 调用后,当前线程(如主线程)会阻塞,直到子线程执行完毕。
    • 子线程结束后,会回收其资源(避免 “僵尸线程”)。
    • 只能调用一次,重复调用会导致未定义行为(程序崩溃)。
  • detach():分离线程,后台运行
    • 调用后,子线程与当前线程 “分离”,成为后台线程(守护线程)。
    • 当前线程无需等待子线程,继续执行;子线程结束后,系统自动回收资源。
    • 分离后,std::thread 对象不再关联任何线程(joinable() 变为 false),不可再调用 join()。
    • 注意:若主线程先退出,分离的子线程可能被系统强制终止(取决于操作系统),需确保子线程不依赖主线程的局部变量。
  • joinable():判断线程是否可管理
    • 返回 true:线程处于可 join() 或 detach() 状态(未调用过 join()/detach(),且不是空线程)。
    • 返回 false:空线程(默认构造的 std::thread)、已 join()/detach() 的线程。
    • 用途:避免重复 join()/detach() 导致的崩溃,或在析构前判断是否需要处理线程。

1.3 按引用传参

std::thread 构造时,如果需要引用传参,必须用 std::ref() 或 std::cref() 包装参数:

#include <thread>
#include <iostream>
#include <functional> // std::ref

void modify_val(int& x) {
    x = 999; // 修改主线程的变量
}

int main() {
    int a = 10;
    std::thread t(modify_val, std::ref(a));

    t.join();
    std::cout << a << std::endl; // 输出 999
    return 0;
}

由于各个平台系统调用的规范的线程入口不同,std::thread 构造时,会先将用户传入的实参(Args&&...)通过 完美转发(std::forward<Args>)打包成 tuple,然后再通过特定的方式传递给该平台规范的线程入口。

这个tuple是在构造函数当中创建的临时对象,在向底层传递时,会使用移动语义来减少开销。

并且最终在对应平台的规范线程入口函数中,使用如下方式调用用户希望的线程入口函数:

// 简化的底层逻辑:子线程中展开 tuple 并调用目标函数
template <typename F, typename Tuple>
void thread_entry_point(F&& f, Tuple&& tup) {
    // std::apply 展开 tuple,传递的是右值(std::move(tup))
    std::apply(std::forward<F>(f), std::move(tup)); 
}

这就导致传入的参数本质上都是右值,若采用非const引用的方式接收参数会直接报错,而即便采用const引用的方式接收参数,也是引用到一个临时对象。

而std::ref会返回一个 std::reference_wrapper<T> 类型的引用包装器(本质是一个轻量级对象,内部存储指向原始对象的指针)。

当 std::thread 在子线程中调用目标函数时,std::reference_wrapper<T> 会被 隐式转换为 T&(通过其 operator T&() 重载),最终目标函数拿到的是对 原始对象的引用,而非副本。

1.4 std::this_thread 命名空间

std::this_thread 提供操作当前线程的工具函数,常用接口:

接口功能描述
get_id()获取当前线程的唯一 ID(std::thread::id 类型)
sleep_for(duration)让当前线程休眠指定时间(如 std::chrono::seconds(1))
sleep_until(time_point)让当前线程休眠到指定时间点
yield()让出 CPU 时间片,允许其他线程运行(不阻塞)

注:

sleep_for() 和 sleep_until() 两个接口的参数涉及到C++11 引入的 标准时间库 std::chrono。

有兴趣的小伙伴可以自行了解,在这两个接口中,我们最常用的还是sleep_for()。

sleep_for() 的参数 std::chrono::duration 是模板类,用于表示「一段时间间隔」,定义如下:

template <typename Rep, typename Period = std::ratio<1>>
class duration;
  • Rep:计数类型(如 int、long long、double),表示「Period 的倍数个数」。
  • Period:周期(std::ratio 类型),表示每个计数单位对应的「秒数」,默认是 std::ratio<1>(1 秒),std::ratio<1, 10>表示1/10秒。

为了使用方便,标准库提供了常用时长的别名(直接用即可):

类型含义Period
nanoseconds纳秒std::ratio<1, 1e9>
microseconds微秒std::ratio<1, 1e6>
milliseconds毫秒std::ratio<1, 1e3>
secondsstd::ratio<1>
minutes分钟std::ratio<60>
hours小时std::ratio<3600>
days(C++20)std::ratio<86400>
weeks(C++20)std::ratio<604800>

时长的常用操作:

  • 创建时长:直接用预定义类型初始化,或用 std::chrono::duration_cast 转换。
  • 算术运算:支持 +、-、*、/(如 1s + 500ms = 1500ms)。
  • 获取计数:用 count() 方法获取时长的计数值(如 1s.count() = 1,500ms.count() = 500)。
#include <chrono>
#include <iostream>

using namespace std::chrono; // 简化命名空间

int main() {
    // 1. 创建时长
    seconds s(1);          // 1 秒
    milliseconds ms(500);  // 500 毫秒
    microseconds us = 1000us; // C++14 字面量(需启用 -std=c++14)

    // 2. 算术运算
    auto total = s + ms;   // 1500 毫秒(类型为 duration<long long, ratio<1,1000>>)
    std::cout << "total ms: " << total.count() << std::endl; // 输出 1500

    // 3. 时长转换(不同周期需显式转换,用 duration_cast)
    auto s2 = duration_cast<seconds>(total); // 1 秒(500ms 被截断)
    std::cout << "total s: " << s2.count() << std::endl; // 输出 1

    // 4. 浮点数时长(支持小数间隔)
    duration<double, seconds::period> s3 = 1.5s; // 1.5 秒
    std::cout << "1.5s in ms: " << duration_cast<milliseconds>(s3).count() << std::endl; // 1500

    return 0;
}

2. mutex

std::mutex 是 C++11 标准库引入的基础互斥量(mutual exclusion,互斥),定义在 <mutex> 头文件中,核心作用是保护共享资源,防止多个线程同时访问导致的数据竞争(data race),确保同一时间只有一个线程能进入临界区(访问共享资源的代码段)。

同样地,接口上的设计与 pthread 库十分相似:Linux笔记---线程同步与互斥

2.1 创建mutex对象

mutex只有一个默认的构造函数,在使用时直接实例化即可:

在 pthread 库中,我们可以将锁设置为递归锁或超时锁,mutex可不可以呢?

答案是不可以,mutex就是一个最基础的互斥量。在C++11中,专门提供了这些特殊锁类型:

互斥量类型特点
std::mutex非递归、无超时、基础功能
std::recursive_mutex可递归(同一线程多次加锁)
std::timed_mutex非递归、支持超时加锁(try_lock_for)
std::recursive_timed_mutex可递归、支持超时加锁

2.2 常用接口

接口功能描述
lock()阻塞式加锁:若锁未被持有,当前线程获取锁;若已被持有,当前线程阻塞至锁释放。
try_lock()非阻塞式加锁:尝试获取锁,成功返回 true,失败返回 false(不阻塞)。
unlock()释放锁:必须由持有锁的线程调用,否则行为未定义(UB)。
native_handle()返回原生互斥量句柄(如 POSIX 下的 pthread_mutex_t),用于平台相关操作(慎用)。

除了上面的接口,含 timed 前缀的锁类型还支持以下两个接口:

接口功能描述
try_lock_for(duration)至多阻塞等待锁duration时间,若在该时间段内未获取到锁,则放弃。
try_lock_until(time_point)至多阻塞等待锁到time_point时间点,若在到达该时间点前未获取到锁,则放弃。

2.3. RAII 包装器

和内存的申请释放同理,锁的申请释放也推荐使用RAII的思想来管理。

C++ 提供了 std::lock_guardstd::unique_lock 等 RAII 工具类(定义于 <mutex> 头文件),自动管理加锁 / 解锁(构造时加锁,析构时解锁),避免手动操作的遗漏,是实际开发的首选。

它们的核心差异在于 灵活性和开销:std::lock_guard 是 “轻量简单款”,std::unique_lock 是 “灵活全能款”。

2.3.1 lock_guard

std::lock_guard 的构造函数仅支持 “直接加锁” “接管已加锁的互斥量”,参数需传递互斥量的引用(不可传值,否则会拷贝互斥量,而 std::mutex 不可拷贝)。

构造函数原型功能描述注意事项
explicit lock_guard(mutex_type& m)默认构造:传入互斥量 m,构造时自动调用 m.lock() 加锁。最常用形式,互斥量需是外部定义的(如全局、类成员、智能指针管理的)。
lock_guard(mutex_type& m, std::adopt_lock_t tag)接管已加锁的互斥量:假设调用者已手动调用 m.lock(),构造时不重复加锁,仅接管解锁权。必须确保 m 已被当前线程加锁,否则析构时调用 unlock() 会导致 UB(未定义行为)。
std::mutex mtx;

void func1() {
    std::lock_guard<std::mutex> lock(mtx);  
    shared_val *= 2;  // 临界区
}

void func2() {
    mtx.lock();
    // 接管已加锁的mtx,构造时不重复加锁,析构时自动解锁
    std::lock_guard<std::mutex> lock(mtx, std::adopt_lock);  
    shared_val *= 2;  // 临界区
}
2.3.2 unique_lock

std::unique_lock 是功能最丰富的 RAII 锁包装器,支持手动控制锁状态延迟加锁超时加锁所有权转移等高级特性,代价是比 std::lock_guard 多一点内存开销和执行开销。

它的核心设计是 “独占所有权”(同一时间仅一个 unique_lock 对象管理某个互斥量),但支持移动语义(可将锁的所有权转移给其他 unique_lock 对象)。

构造函数原型(简化)锁策略功能描述
explicit unique_lock(mutex_type& m)默认加锁构造时调用 m.lock(),阻塞直到获取锁。
unique_lock(mutex_type& m, std::defer_lock_t)延迟加锁构造时不加锁,仅关联互斥量,后续手动调用 lock()。
unique_lock(mutex_type& m, std::adopt_lock_t)接管已加锁构造时不加锁,接管当前线程已持有的锁,析构时解锁。
unique_lock(mutex_type& m, std::try_to_lock_t)尝试加锁构造时调用 m.try_lock(),成功返回持有锁,失败返回不持有(不阻塞)。
unique_lock(mutex_type& m, const chrono::duration& d)超时加锁构造时调用 m.try_lock_for(d),超时未获取则不持有锁(需互斥量支持超时)。

相比于 std::lock_guard,std::unique_lock 为了进行更加灵活的控制,提供了丰富的成员函数。

其中包括对底层锁主要接口的封装,可直接使用相同的接口对底层的锁进行控制:

除此之外,还提供了如下的常用接口:

成员函数功能描述
owns_lock()返回 bool:当前对象是否持有锁(避免重复加锁 / 解锁)。
mutex()返回指向底层互斥量的引用(慎用,避免手动操作互斥量)。
release()释放对互斥量的管理:返回底层互斥量指针,后续 unique_lock 析构时不解锁。
operator bool()同owns_lock(),可以直接将unique_lock对象作为条件表达式。

2.4 lock和try_lock

为什么要有“接管已加锁的互斥量”“延迟加锁”这样的设计呢?这样设计主要是为了和std::lock以及std::try_lock配合,以避免死锁:

这两个函数的作用是同时对参数列表中的所有锁变量进行加锁操作。

std::lock内部通过特定算法(如 “按互斥量地址排序后加锁”),确保多个线程以相同顺序获取锁,从根源避免死锁。

std::try_lock尝试获取所有传入的互斥量,不会阻塞—— 若所有互斥量都可获取,则成功,返回 -1;若任意一个不可获取,则立即放弃,释放已获取的锁,返回第一个获取失败的互斥量的索引(从 0 开始)。

例如cplusplus.com中给出的这个示例:

// unique_lock constructor example
#include <iostream>       // std::cout
#include <thread>         // std::thread
#include <mutex>          // std::mutex, std::lock, std::unique_lock
                          // std::adopt_lock, std::defer_lock
std::mutex foo,bar;

void task_a () {
  std::lock (foo,bar);         // simultaneous lock (prevents deadlock)
  std::unique_lock<std::mutex> lck1 (foo,std::adopt_lock);
  std::unique_lock<std::mutex> lck2 (bar,std::adopt_lock);
  std::cout << "task a\n";
  // (unlocked automatically on destruction of lck1 and lck2)
}

void task_b () {
  // foo.lock(); bar.lock(); // replaced by:
  std::unique_lock<std::mutex> lck1, lck2;
  lck1 = std::unique_lock<std::mutex>(bar,std::defer_lock);
  lck2 = std::unique_lock<std::mutex>(foo,std::defer_lock);
  std::lock (lck1,lck2);       // simultaneous lock (prevents deadlock)
  std::cout << "task b\n";
  // (unlocked automatically on destruction of lck1 and lck2)
}


int main ()
{
  std::thread th1 (task_a);
  std::thread th2 (task_b);

  th1.join();
  th2.join();

  return 0;
}

3. condition_variable

std::condition_variable 是 C++11 引入的同步原语(条件变量),定义在 <condition_variable> 头文件中,用于线程间的条件等待:让一个 / 多个线程等待某个条件满足,直到其他线程通知其条件成立。

与pthread库中的条件变量有所不同的是,condition_variable的成员函数并不直接以锁为参数,而是接收一个std::unique_lock对象。

除此之外,用法几乎与pthread库中的条件变量相同,下面是常用的成员函数:

接口说明
wait(unique_lock&)解锁互斥锁,阻塞等待;被唤醒后重新加锁,需循环检查条件。
wait(lock, pred)带谓词的等待(推荐),等价于 while(!pred()) { wait(lock); }
wait_for(lock, dur, pred)等待指定时长,超时或条件满足时返回,返回值为 bool(是否满足条件)。
wait_until(lock, time, pred)等待到指定时间点,逻辑同 wait_for。
notify_one()唤醒一个等待的线程(若有)。
notify_all()唤醒所有等待的线程(若有)。

示例代码(两个线程合作按序打印1~100,一个线程打印奇数,一个线程打印偶数):

#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
using namespace std;

int main()
{
	// true打印偶数,false打印奇数
	bool flag = true;
	mutex mtx;
	condition_variable cond;

    // 线程1打印偶数
	thread t1([&]() {
		int i = 0;
		while (i <= 100)
		{
			unique_lock<mutex> lck(mtx);
			while (!flag)
				cond.wait(lck);
			cout << this_thread::get_id() << ": " << i << endl;
			i += 2;
			flag = false;
			cond.notify_one();
		}
	});

    // 线程2打印奇数
	thread t2([&]() {
		int i = 1;
		while (i <= 100)
		{
			unique_lock<mutex> lck(mtx);
			while (flag)
				cond.wait(lck);
			cout << this_thread::get_id() << ": " << i << endl;
			i += 2;
			flag = true;
			cond.notify_one();
		}
	});

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

	return 0;
}

除了condition_variable,标准库中还提供了condition_variable_any。相比于前者,后者的成员函数可以接收任意类型的锁对象(只要提供了lock和unlock成员函数)。

4. call_once

std::call_once 是 C++11 标准库提供的 线程安全的一次性初始化工具(定义于 <mutex> 头文件),核心作用是保证某个函数在多线程环境下仅执行一次,常用于全局资源初始化、单例模式实现等场景。

它与 std::once_flag 配合使用,底层通过互斥量和原子操作实现,比手动用 std::mutex + bool 标记 更简洁、安全(避免内存可见性问题和竞态条件)。

// call_once example
#include <iostream>       // std::cout
#include <thread>         // std::thread, std::this_thread::sleep_for
#include <chrono>         // std::chrono::milliseconds
#include <mutex>          // std::call_once, std::once_flag

int winner;
void set_winner (int x) { winner = x; }
std::once_flag winner_flag;

void wait_1000ms (int id) {
  // count to 1000, waiting 1ms between increments:
  for (int i=0; i<1000; ++i)
    std::this_thread::sleep_for(std::chrono::milliseconds(1));
  // claim to be the winner (only the first such call is executed):
  std::call_once (winner_flag,set_winner,id);
}

int main ()
{
  std::thread threads[10];
  // spawn 10 threads:
  for (int i=0; i<10; ++i)
    threads[i] = std::thread(wait_1000ms,i+1);

  std::cout << "waiting for the first among 10 threads to count 1000 ms...\n";

  for (auto& th : threads) th.join();
  std::cout << "winner thread: " << winner << '\n';

  return 0;
}

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐