C++ 语言层面上的多线程

C++ 语言层面上的多线程

C++11 多线程 | 行码棋

1. C++11 的线程模型 vs pthread 的对应关系

pthread APIC++11 对应写法说明
pthread_create()std::thread(func, args...)创建并启动线程
pthread_self()std::this_thread::get_id()获取当前线程 ID
pthread_join()t.join()等待线程结束
pthread_detach()t.detach()分离线程
pthread_exit()return 即可函数返回自动结束线程
pthread_cancel()无直接等价(需自定义退出标志)C++ 不建议 强制终止线程
pthread_mutex_*std::mutex / std::lock_guard自动 RAII 锁管理
pthread_cond_*std::condition_variable条件变量

2. 线程的使用

1. 语法

1
2
std::thread t(func, arg1, arg2, ...);
// std:: thread 变量名(要执行的函数, 参数...)

可以传 任意个参数(0 个也行),只要函数能接收即可,默认是 值传递(参数 自动复制一份 传入线程中),如果要让线程内修改外部变量,使用 std::ref(变量)(头文件 <functional> 包一层表示传引用,比如:std::ref(var),线程启动后立即执行传入函数。如果要创建一个 无参的线程对象(不执行任何函数,这种情况很少见),有两种正确写法:

  1. 省略括号: std::thread t1;,此时会默认构造一个 std::thread 对象(不关联任何线程,joinable() 返回 false)。
  2. 使用统一初始化语法(C++11 起): std::thread t1{};,显式调用默认构造函数,效果与 std::thread t1; 相同。

2. 代码示例

1
2
3
4
5
void func(int& x) { x = 100; }
int val = 0;
thread t(func, ref(val)); // 传引用
t.join();
cout << val; // 输出 100

1. 传普通函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <iostream>
#include <thread>
#include <windows.h>
using namespace std;

void func(int n)
{
cout << "我拿到了一个值:" << n << endl;
}

int main()
{
thread t1(func, 100);
// 注意:每个 std:: thread 对象,在销毁前必须被 join() 或 detach(),否则会造成资源泄漏
// 分别测试下面 3 中情况观察现象:全注释掉或者使用其中任意一个

// 1. 等待线程结束,确保安全
//t1.join();

// 2. 分离线程,让它独立运行。注意:主线程可能在子线程打印之前就结束了,所以输出可能看不到!用 Sleep 等它一等
t1.detach();
Sleep(1); // 等待线程结束

return 0;
}

C++ 标准库要求:线程对象销毁前,必须显式处理线程的执行状态(要么 join 等待其完成,要么 detach 让其后台运行)。如果不处理,线程对象销毁时会触发异常,导致程序 abort(“Debug Error! abort () has been called”)。

这是 C++ 标准委员会为了 防止资源泄漏和未定义行为 而做的安全设计:std::thread 是 RAII 对象,它的析构应该清理资源。如果不强制 joindetach,程序员很容易忘记,导致线程悬空、资源泄露、甚至访问非法内存。相比于 pthread 的“自由放任”,std::thread 更偏向“安全第一”。

2. 传引用参数(想修改主线程变量时)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <iostream>
#include <thread>
#include <functional>
using namespace std;

void add(int &x)
{
x += 10;
}

int main()
{
int value = 5;
thread t(add, ref(value)); // 用 std:: ref 表示按引用传参
t.join();
cout << "value = " << value << endl; // 输出 15
return 0;
}

不加 ref(),参数会被复制(传值),加 ref(),可修改主线程变量(传引用)。

3. 传 lambda(常用)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <iostream>
#include <thread>
using namespace std;

int main()
{
int n = 0;
thread t([&n]() { // 捕获外部变量 n 的引用
for (int i = 0; i < 5; ++i) n++;
});

t.join();
cout << "n = " << n << endl;
return 0;
}

4. 传成员函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <iostream>
#include <thread>
using namespace std;

class Task
{
public:
void run(int times)
{
for (int i = 0; i < times; ++i)
cout << "Task 工作中..." << endl;
}
};

int main()
{
Task obj;
thread t(&Task::run, &obj, 3); // 第一个参数:成员函数指针;第二个参数:对象指针;后续参数:传给函数的参数
t.join();
return 0;
}

3. join 和 detach

  • join():主线程等待子线程执行完毕,再继续执行。适用于需要同步的场景。
  • detach():子线程与主线程 “分离”,在后台独立运行(主线程退出后,子线程可能继续执行,由系统回收资源)。适用于后台任务,不关心何时结束。

join 其实非常好理解和上手使用,但是 detach 有一个注意事项:detach() 使线程后台运行,不再与主线程同步,若主线程提前退出,资源可能已被销毁,线程访问将非法。所以:

  1. join 的使用场景: 需要等待线程执行完成的情况,需要获取线程执行结果的情况,主线程退出前必须确保子线程完成的情况。
  2. detach 的使用场景: 线程需要长时间运行(如后台服务),主线程不需要等待线程完成,线程不需要返回结果给主线程。注意事项:detach 后无法再对线程进行 join,要确保 detach 的线程不会访问已经被释放的资源,一旦主线程从 main() 返回或程序调用 exit(),整个进程将终止,此时所有后台线程(包括已 detach 的)都会被操作系统强制销毁,无论其是否完成。

4. 线程结束与终止机制

C++ 中线程终止的推荐方式与 Linux 的 pthread 有所不同:C++ 标准没有提供 pthread_cancel 那样的 外部强制终止线程,因为这种行为可能在 C++ 对象析构时造成资源泄露(破坏 RAII)。

方式说明推荐程度
return / 函数执行完正常退出(推荐)⭐⭐⭐⭐⭐
标志位控制用于特殊场景的控制⭐⭐⭐
抛出异常(未捕获则线程终止)用于错误情况⭐⭐⭐
调用 std::terminate() / exit() / _exit()会终止整个进程,而非仅线程十分不推荐

代码示例:

1
2
3
4
5
void thread_function() 		// 执行任务
{
// ……
return; // 线程函数自然 return 返回,正常结束(推荐)
}

正常退出的优点:自动调用线程函数中所有对象的析构函数、正确释放线程堆栈内存、设置正确的退出代码、递减线程内核对象使用计数。

1
2
3
4
5
6
7
8
9
10
11
12
13
// 标志位控制线程退出:
std::atomic<bool> stop_flag(false);
void thread_function()
{
while (!stop_flag)
{
// 执行循环任务
}
// 清理工作
}

// 在主线程中设置标志位
stop_flag = true;

使用 原子标志位 控制线程逻辑:不通过强制“杀死线程”,而是让线程 自己检测退出信号。 这种方式资源安全、可控,是现代 C++ 推荐的线程终止模式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <iostream>
#include <thread>
#include <atomic>
#include <chrono>
using namespace std;

atomic<bool> stopFlag(false);

void worker()
{
while (!stopFlag)
{
cout << "工作中..." << endl;
this_thread::sleep_for(chrono::milliseconds(500));
}
cout << "线程安全退出" << endl;
}

int main()
{
thread t(worker);
this_thread::sleep_for(chrono::seconds(2));

stopFlag = true; // 通知线程退出
t.join();
}

5. 线程休眠

在 C++ 层面,不再使用 sleep()usleep(),推荐下面 2 种,用于替代 Windows 的 Sleep() 和 Linux 的 usleep(),可跨平台。

函数头文件作用
std::this_thread::sleep_for(duration)<thread> + <chrono>休眠指定时间
std::this_thread::sleep_until(time_point)<thread> + <chrono>休眠到指定时间
  • using namespace std::chrono;:作用是引入 std::chrono 命名空间下的 时间类型和时钟类型,例如:
  • 时长类型:std::chrono::secondsstd::chrono::milliseconds 等。
  • 时钟类型:std::chrono::system_clockstd::chrono::steady_clock 等。
  • 时间点类型:std::chrono::time_point 等。
  • using namespace std::chrono_literals;:作用是引入 C++14 新增的时间单位字面量(如 smsusnsminh),可以用更简洁的方式表示时长,例如:
  • 1s 等价于 std::chrono::seconds(1)
  • 500ms 等价于 std::chrono::milliseconds(500)
  • 2min 等价于 std::chrono::minutes(2)
  • 1h 等价于 std::chrono::hours(1)

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <thread>
#include <chrono> // 提供时间单位和时长类型
using namespace std::chrono_literals; // 启用时间字面量(s, ms, us 等)

int main()
{
// 休眠 1 秒(1s 等价于 std::chrono:: seconds(1))
std::this_thread::sleep_for(1s);

// 休眠 500 毫秒(0.5 秒)
std::this_thread::sleep_for(500ms);

// 休眠 200 微秒(0.0002 秒)
std::this_thread::sleep_for(200us);

// 休眠 1000 纳秒(0.000001 秒)
std::this_thread::sleep_for(1000ns);
return 0;
}

理解方式:std::this_thread::sleep_for(duration) 让当前线程休眠一段时间(“睡多久”),即延迟执行后面的代码。“sleep for X” = 从现在起暂停 X 时间再继续执行,假如当前时间是 10:00:00,sleep_for(3s) → 线程在 10:00:03 再醒。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <thread>
#include <chrono>
#include <iostream>
using namespace std::chrono;

int main()
{
// 获取当前系统时间(以系统时钟为准,包含日期时间)
auto now = system_clock::now();

// 计算目标时间点:当前时间 + 2 秒(即 2 秒后)
auto target_time = now + 2s;

// 休眠到目标时间点
std::this_thread::sleep_until(target_time);

// 到达目标时间点后执行
std::cout << "已到达目标时间点!" << std::endl;
return 0;
}

这里 nowsystem_clock::time_point 类型。

理解方式:std::this_thread::sleep_until(time_point) 让当前线程休眠到指定的时刻,即“睡到什么时候”。“sleep until T” = 睡到某个时间点 T,假设现在是 10:00:00,指定 10:00:03 → 线程睡到那时醒,若调用时系统时间已超过目标时间,函数立即返回。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <thread>
#include <chrono>
// 通常会同时引入 chrono 命名空间和时间字面量命名空间
using namespace std::chrono;
using namespace std::chrono_literals;

int main()
{
// 使用字面量表示时长
std::this_thread::sleep_for(1s); // 1 秒
std::this_thread::sleep_for(500ms); // 500 毫秒
std::this_thread::sleep_for(2min); // 2 分钟
std::this_thread::sleep_for(1h); // 1 小时

// 使用 chrono 类型和时钟
auto now = system_clock::now();
auto target = now + 100ms; // 结合字面量和 chrono 类型
std::this_thread::sleep_until(target);
return 0;
}

6. 锁:std::mutex

C++ 互斥锁的三种常见用法

C++语言层面提供了三种常用锁,都在头文件 <mutex> 中:

锁类型是否自动解锁功能特点推荐使用场景
std::mutex❌ 否最基础的互斥锁,需手动 lock/unlock灵活控制锁时机
std::lock_guard<std::mutex>✅ 是自动加锁、自动释放,作用域结束自动解锁简单作用域加锁
std::unique_lock<std::mutex>✅ 是功能更强(可延迟加锁、可解锁再加锁)条件变量、复杂锁逻辑

1. 手动加解锁(像 pthread)

1
2
3
4
5
6
7
mutex mtx;
void func()
{
mtx.lock();
// 临界区
mtx.unlock();
}

注意:不要忘记 unlock(),异常时无法释放锁!若函数中有异常或提前 return,可能导致未解锁,不推荐在复杂逻辑中使用。如果临界区代码抛出异常,锁将无法释放,可能会导致死锁!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <iostream>
#include <thread>
#include <mutex>
using namespace std;

mutex mtx; // 全局互斥锁

void print(int id)
{
mtx.lock(); // 手动加锁
cout << "线程 " << id << " 正在执行" << endl;
mtx.unlock(); // 手动解锁
}

int main()
{
thread t1(print, 1);
thread t2(print, 2);

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

2. RAII 自动锁:std::lock_guard

std::lock_guard最简单、最轻量、不能折腾的锁

只能:

  • 构造 → 自动 lock()
  • 析构 → 自动 unlock()

不能:

  • 中途 unlock
  • 不能 lock
  • 不能移动
  • 不能延迟加锁
1
2
3
4
5
6
mutex mtx;
void func()
{
lock_guard<mutex> lock(mtx); // 构造时加锁,析构时自动解锁
// 临界区
} // 作用域结束自动 unlock

比较常用、安全的写法。lock_guard<mutex> 是一个模板类,<mutex> 指模板参数的类型,即“锁的类型”,必须是 mutex,不能写成 intstring(mtx) 是构造参数,表示传入要保护的那把锁,离开作用域(函数返回、异常抛出等)时会 自动调用 unlock(),非常安全。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <iostream>
#include <thread>
#include <mutex>
using namespace std;

mutex mtx;

void print(int id)
{
lock_guard<mutex> lock(mtx); // 自动加锁,作用域结束自动解锁
cout << "线程 " << id << " 正在执行" << endl;
}

int main()
{
thread t1(print, 1);
thread t2(print, 2);

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

3. 可灵活控制的锁:std::unique_lock

std::unique_lock功能全面版的锁,为了处理复杂情况设计的,它允许:

  • 中途 unlock()
  • 中途 lock()
  • 可以使用 try_lock
  • 可以延迟加锁(defer_lock)
  • 可以转移所有权(移动语义)
  • 可以和 condition_variable 协作(这是重点)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
mutex mtx;
void func()
{
unique_lock<mutex> lock(mtx); // 可随时 lock/unlock
// 临界区
lock.unlock(); // 手动解锁
// ...执行其他代码
lock.lock(); // 再次加锁
}


{
std::unique_lock<std::mutex> lock(_mutex); // 自动 lock()
// ……
} // 自动 unlock()

unique_lock<mutex> 也接受 mutex 类型作为模板参数,可以选择何时加锁、解锁,更灵活,常用于 condition_variable(条件变量)场景。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <iostream>
#include <thread>
#include <mutex>
using namespace std;

mutex mtx;

void work(int id)
{
unique_lock<mutex> lock(mtx); // 自动加锁
cout << "线程 " << id << " 获取锁" << endl;

lock.unlock(); // 手动释放锁
cout << "线程 " << id << " 解锁后执行其他操作" << endl;

lock.lock(); // 再次加锁
cout << "线程 " << id << " 重新加锁执行" << endl;
}

int main()
{
thread t1(work, 1);
thread t2(work, 2);

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

平时开发中,常用的差不多就这些,更多高深的较少用到,需要时简单查阅一下就行,扩展:了解一下递归锁,大概就是在递归中的用的锁,但是实际开发中递归也比较少用,所以这里不作为重点。


7. 原子操作:std::atomic

1. std::mutex vs std::atomic:两种完全不同的思路

对比点std::mutex(互斥锁)std::atomic(原子变量)
保护范围一整段代码(临界区)一个变量本身
操作粒度粗(可包住多行逻辑)细(只保护单个读写)
原理多线程竞争锁 → 内核或自旋控制CPU 提供原子指令实现
阻塞会阻塞(等待锁)不阻塞或轻量阻塞(硬件原子指令)
使用场景复杂临界区,修改多个共享变量或复杂逻辑(多步骤操作、多变量共享)简单操作:计数、状态标志、单变量累加等
开销较大(系统级锁,涉及内核态切换)极小(硬件原语,用户态操作,接近普通变量)
可替代关系原子操作不能代替锁保护复杂逻辑锁可以保护一切,但性能低
  • mutex:锁的是“一段代码”(临界区)。
  • atomic:锁的是“一个变量的单个读写”。

2. 为什么需要 atomic

atomic 的自增操作底层是硬件级原子指令(如 LOCK XADD),不会被中断打断,也不会出现竞争。

因为有时候我们只是做一个 简单的共享数值操作(例如计数器),用互斥锁反而太重,std::atomic 直接让这些操作变为 无锁且线程安全

mutex 版:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <iostream>
#include <thread>
#include <mutex>
using namespace std;

mutex mtx;
int counter = 0;

void work()
{
for (int i = 0; i < 10000; ++i)
{
lock_guard<mutex> lock(mtx);
++counter; // 被锁保护
}
}

int main()
{
thread t1(work), t2(work);
t1.join(); t2.join();
cout << counter << endl;
}

atomic 版:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <iostream>
#include <thread>
#include <atomic>
using namespace std;

atomic<int> counter(0);

void work()
{
for (int i = 0; i < 10000; ++i)
++counter; // 原子自增,无需锁
}

int main()
{
thread t1(work), t2(work);
t1.join(); t2.join();
cout << counter.load() << endl;
}

3. atomic 的使用

1. 定义

头文件:

1
2
#include <atomic>
using namespace std;

定义方式:

1
2
3
4
atomic<int> cnt(0);       // 定义原子整型变量并初始化成 0
atomic<bool> ready(false); // 原子布尔变量,初始化成 flase
atomic<long long> total(0); // 原子长整型并初始化成 0
std::atomic<int*> atomic_ptr(nullptr); // 原子指针初始化成空

2. 常用操作

操作含义示例
load()读取当前值int x = cnt.load();
store(val)写入值cnt.store(10);
fetch_add(n)加法并返回旧值cnt.fetch_add(1);
fetch_sub(n)减法并返回旧值cnt.fetch_sub(1);
++--原子自增、自减++cnt;--cnt;
exchange(val)原子地替换值cnt.exchange(100);
compare_exchange_strong(expected, desired)CAS 操作如果当前值 == expected,则更新为 desired

3. 代码示例

示例一:多线程计数器(替代 mutex)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <iostream>
#include <thread>
#include <atomic>
using namespace std;

atomic<int> cnt(0); // 原子计数器, 避免使用 count 命名,可能与库中的冲突
//int cnt = 0; // 非原子计数器

void work()
{
for (int i = 0; i < 1000000; ++i)
++cnt; // 原子自增,无需加锁(等价于 cnt.fetch_add(1);,线程安全)
}

int main()
{
thread t1(work);
thread t2(work);

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

cout << "最终计数结果: " << cnt.load() << endl;
//cout << "最终计数结果: " << cnt << endl;

return 0;
}

示例二:线程通信(退出标志)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <iostream>
#include <thread>
#include <atomic>
#include <chrono>
using namespace std;
using namespace chrono_literals;

atomic<bool> flag(false); // 线程共享标志

void worker()
{
// while (! flag)// 也可以,更简洁,且等价
while (!flag.load()) // 检查标志
{
cout << "工作中..." << endl;
this_thread::sleep_for(500ms);
}
cout << "收到停止信号,退出线程" << endl;
}

int main()
{
thread t(worker);

this_thread::sleep_for(2s);
flag.store(true); // 改变标志,通知线程退出

t.join();
return 0;
}

示例三:compare_exchange_strong(CAS),CAS(Compare And Swap)是底层原子操作的基础。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
std::atomic<int> val(10);
int expected = 10;

// 尝试将 val 从 10 改为 20
bool success = val.compare_exchange_weak(expected, 20);
// 若 val 原本是 10:success = true,val 变为 20
// 若 val 被其他线程修改为 30:success = false,expected 变为 30

// 通常配合循环使用(处理 weak 版本的伪失败)
expected = 10;
while (!val.compare_exchange_weak(expected, 20)) // 失败时,expected 已被更新为当前值,需重置为目标预期值
{
expected = 10;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <iostream>
#include <atomic>
using namespace std;

int main()
{
atomic<int> value(10);
int expected = 10; // 期望值
int desired = 20; // 想要更新的值

if (value.compare_exchange_strong(expected, desired))
cout << "更新成功,新值=" << value.load() << endl;
else
cout << "更新失败,当前值=" << value.load() << endl;

return 0;
}

4. 注意事项

  1. 不支持复杂操作: 原子类型仅保证 单个操作 的原子性,若要执行多步操作(如 a += bb 也是共享变量),仍需用 mutex 保护。
  2. 避免过度使用: 原子类型的性能虽高,但比普通变量仍有开销。若变量无需多线程共享,用普通类型即可。
  3. 类型限制: 并非所有类型都能作为 std::atomic 的模板参数,仅支持 “可平凡复制” 的类型(如基本类型、指针、简单结构体)。复杂类型需用 mutex 保护。
  4. 内存序(Memory Order): 上述示例省略了内存序参数(默认 std::memory_order_seq_cst,最严格),复杂场景可通过指定内存序(如 std::memory_order_relaxed)优化性能,目前用不到,需要时查一下即可。扩展:实际开发中,理解内存序对于编写正确的并发代码比较有帮助。