thread库
概述
std::thread 是 C++11 标准库提供的线程管理类,它为我们提供了一种跨平台、面向对象的线程创建和管理方式。如果你之前接触过 Linux 下的 pthread 库或 Windows 下的线程 API,那么 std::thread 会让你感觉更加优雅和易用。
为什么需要 thread 库?
在 C++11 之前,创建线程需要依赖平台特定的 API:
- Linux : 使用
pthread_create()函数 - Windows : 使用
CreateThread()API
这些 API 存在以下问题:
- 平台依赖性:不同平台需要不同的代码,难以实现跨平台
- 面向过程:使用函数指针,不够灵活
- 参数传递复杂:传递多个参数需要打包成结构体
std::thread 的出现解决了这些问题:
- ✅ 跨平台:底层封装了各系统的线程库(Linux 的 pthread、Windows 的 Thread 等),提供统一的接口
- ✅ 面向对象:采用类的方式管理线程,符合 C++ 的设计理念
- ✅ 现代化特性:充分利用 C++11 的右值引用、移动语义、可变模板参数等特性,使用更加便捷
参考文档
构造函数
std::thread 提供了多种构造函数,让我们来看看它们的具体用法:
1. 默认构造函数
cpp
thread() noexcept;
创建一个不表示任何线程的线程对象。这个对象是"空的",不能执行任何操作,主要用于后续的移动赋值。
cpp
std::thread t; // 创建一个空的线程对象
// t 此时不代表任何线程
2. 初始化构造函数(最常用)
cpp
template <class Fn, class... Args>
explicit thread(Fn&& fn, Args&&... args);
这是日常开发中最常用的构造函数。它接受一个可调用对象(函数、函数指针、lambda 表达式、函数对象/仿函数等)和该可调用对象的参数。
最简单的调用示例:
cpp
#include <iostream>
#include <thread>
// 1. 普通函数
void func(int n)
{
std::cout << "函数: " << n << std::endl;
}
// 2. 函数对象(仿函数)
struct Functor
{
void operator()(int n)
{
std::cout << "仿函数: " << n << std::endl;
}
};
int main()
{
// 方式1:普通函数
std::thread t1(func, 1);
t1.join();
// 方式2:函数指针
void (*fp)(int) = func;
std::thread t2(fp, 2);
t2.join();
// 方式3:lambda 表达式
std::thread t3([](int n)
{
std::cout << "lambda: " << n << std::endl;
}, 3);
t3.join();
// 方式4:函数对象(仿函数)
Functor f;
std::thread t4(f, 4);
t4.join();
// 方式5:临时函数对象
std::thread t5(Functor(), 5);
t5.join();
return 0;
}
术语说明:
- 函数对象(Function Object) 也叫 仿函数(Functor) ,是指重载了
operator()的类对象,可以像函数一样被调用
优势对比:
相比 pthread_create 只能传递函数指针和单个 void* 参数:
c
// pthread 方式:需要打包参数到结构体
struct ThreadArgs
{
int n;
int i;
};
void* Print(void* arg)
{
ThreadArgs* args = (ThreadArgs*)arg;
// ...
}
// 注意:需要确保 args 的生命周期足够长,或者使用动态分配
ThreadArgs args = {10, 0};
pthread_t tid;
pthread_create(&tid, NULL, Print, &args);
pthread_join(tid, NULL); // 必须等待线程完成,否则 args 可能被销毁
std::thread 可以直接传递多个参数,类型安全且简洁:
cpp
// std::thread 方式:直接传递参数
void Print(int n, int i)
{
// ...
}
std::thread t(Print, 10, 0); // 简洁明了!
3. 拷贝构造函数(已删除)
cpp
thread(const thread&) = delete;
thread& operator=(const thread&) = delete;
重要 :std::thread 不支持拷贝构造和拷贝赋值。这是因为线程对象代表一个实际的执行线程,拷贝一个线程对象在语义上是不合理的。
cpp
std::thread t1(Print, 10, 0);
// std::thread t2 = t1; // ❌ 编译错误!不允许拷贝
4. 移动构造函数和移动赋值
cpp
thread(thread&& x) noexcept;
thread& operator=(thread&& rhs) noexcept;
std::thread 支持移动语义,可以将一个线程对象的所有权转移给另一个线程对象。
cpp
std::thread t1(Print, 10, 0);
std::thread t2 = std::move(t1); // ✅ 移动构造,t1 变为空线程对象
// 现在 t2 拥有线程的所有权,t1 不再代表任何线程
使用场景:在容器中存储线程对象时,必须使用移动语义:
cpp
std::vector<std::thread> threads;
threads.emplace_back(Print, 10, 0); // 使用 emplace_back 直接构造
// 或者
std::thread t(Print, 10, 0);
threads.push_back(std::move(t)); // 使用移动语义
核心成员函数
join() - 等待线程完成
cpp
void join();
join() 是线程同步的重要方法。它的作用是:阻塞当前线程(通常是主线程),直到被调用的线程执行完毕。
为什么需要 join?
在多线程程序中,每个 std::thread 对象在销毁前必须调用 join() 或 detach()。如果线程对象在销毁时仍然是一个可加入的线程(joinable),程序会调用 std::terminate() 终止。join() 确保了主线程会等待子线程完成后再继续执行,这是线程同步的标准做法。
什么是"可加入的线程"(joinable thread)?
一个线程对象是可加入的(joinable) ,意味着它代表一个活跃的执行线程 ,并且还没有被 join() 或 detach()。具体来说:
可加入的线程(joinable = true):
- ✅ 通过构造函数创建的线程对象,且线程正在运行或已完成但未 join
- ✅ 可以通过
join()等待线程完成 - ✅ 可以通过
detach()分离线程
不可加入的线程(joinable = false):
- ❌ 默认构造的线程对象(空的线程对象)
- ❌ 已经被
join()的线程对象 - ❌ 已经被
detach()的线程对象 - ❌ 已经被移动的线程对象(移动后变为空线程对象)
示例说明:
cpp
#include <iostream>
#include <thread>
void task()
{
std::cout << "线程执行中..." << std::endl;
}
int main()
{
// 情况1:可加入的线程
std::thread t1(task);
std::cout << "t1 joinable: " << t1.joinable() << std::endl; // 输出: 1 (true)
t1.join();
std::cout << "join后 t1 joinable: " << t1.joinable() << std::endl; // 输出: 0 (false)
// 情况2:不可加入的线程(默认构造)
std::thread t2;
std::cout << "t2 joinable: " << t2.joinable() << std::endl; // 输出: 0 (false)
// 情况3:不可加入的线程(已分离)
std::thread t3(task);
t3.detach();
std::cout << "detach后 t3 joinable: " << t3.joinable() << std::endl; // 输出: 0 (false)
// 情况4:不可加入的线程(已移动)
std::thread t4(task);
std::thread t5 = std::move(t4);
std::cout << "移动后 t4 joinable: " << t4.joinable() << std::endl; // 输出: 0 (false)
std::cout << "t5 joinable: " << t5.joinable() << std::endl; // 输出: 1 (true)
t5.join();
return 0;
}
重要规则:
- 线程对象析构时的检查 :如果线程对象在析构时仍然是 joinable 的,程序会调用
std::terminate()终止 - 必须 join 或 detach :每个 joinable 的线程对象在销毁前必须调用
join()或detach() - 只能 join 一次:一个线程对象只能 join 一次,再次 join 会抛出异常
- detach 后不能 join:detach 后的线程对象不能再 join
cpp
#include <iostream>
#include <thread>
void task()
{
std::cout << "子线程开始执行" << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(2));
std::cout << "子线程执行完毕" << std::endl;
}
int main()
{
std::thread t(task);
std::cout << "主线程等待子线程..." << std::endl;
t.join(); // 主线程在这里阻塞,等待 t 执行完毕
std::cout << "主线程继续执行" << std::endl;
return 0;
}
注意事项:
- 一个线程对象只能调用一次
join() - 如果线程已经 join 过,再次调用会抛出
std::system_error异常 - 如果线程对象是空的(默认构造或已移动),调用
join()也会抛出异常
detach() - 分离线程
cpp
void detach();
与 join() 相反,detach() 将线程对象与底层线程分离,允许线程独立运行。分离后的线程成为"守护线程"(daemon thread),线程对象不再拥有该线程的所有权。
重要提示 :使用 detach() 时,需要确保线程的生命周期不会超出程序的生命周期。当 main() 函数返回时,程序会调用 std::exit(),这会终止所有仍在运行的分离线程。因此,detach() 通常用于不需要等待结果的长时间运行任务,且需要确保这些任务在程序退出前能够完成,或者使用适当的同步机制(如条件变量)来协调线程的退出。
cpp
std::thread t(task);
t.detach(); // 分离线程,主线程不再等待它
// 注意:分离后不能再 join()
使用场景:适合不需要等待结果的长时间运行任务,如日志记录、后台监控等。
joinable() - 检查线程是否可 join
cpp
bool joinable() const;
检查线程对象是否表示一个活跃的线程(可以调用 join() 或 detach())。
cpp
std::thread t(task);
if (t.joinable())
{
t.join(); // 安全地 join
}
线程 ID
thread::id 类型
thread::id 是 std::thread 的嵌套类型(nested type),用于唯一标识一个线程。每个线程都有一个唯一的 ID。从底层角度看,thread::id 封装了各个平台的线程 ID 表示(Linux 的 pthread_t、Windows 的 DWORD 等),提供了统一的抽象接口。
特性:
- 支持比较操作(
==,!=,<,<=,>,>=) - 支持流插入和提取(
<<,>>) - 可以作为
std::unordered_map和std::unordered_set的键(通过特化的hash仿函数)
为什么需要封装?
不同平台的线程 ID 表示方式不同:
- Linux:
pthread_t类型 - Windows:
DWORD类型
thread::id 提供了一个统一的抽象,隐藏了平台差异,使得代码可以跨平台编译和运行。
get_id() - 获取线程 ID
cpp
thread::id get_id() const;
通过线程对象获取线程 ID:
cpp
std::thread t(task);
std::cout << "线程 ID: " << t.get_id() << std::endl;
this_thread::get_id() - 获取当前线程 ID
在线程执行体内,可以通过 std::this_thread::get_id() 获取当前线程的 ID:
cpp
#include <iostream>
#include <thread>
void Print(int n, int i)
{
for (; i < n; i++)
{
std::cout << std::this_thread::get_id() << ":" << i << std::endl;
}
}
int main()
{
std::thread t1(Print, 10, 0);
std::thread t2(Print, 20, 10);
// 获取线程 ID
std::cout << "t1 的线程 ID: " << t1.get_id() << std::endl;
std::cout << "t2 的线程 ID: " << t2.get_id() << std::endl;
t1.join();
t2.join();
// 获取当前运行线程 ID(主线程)
std::cout << "主线程 ID: " << std::this_thread::get_id() << std::endl;
return 0;
}
完整示例
下面是一个综合示例,展示了 std::thread 的基本用法:
cpp
#include <iostream>
#include <thread>
#include <vector>
#include <mutex>
std::mutex coutMutex; // 用于保护 std::cout 的输出
void Print(int n, int i)
{
// 锁放在循环外:保证一个线程完整输出所有内容,避免输出被其他线程打断
std::lock_guard<std::mutex> lock(coutMutex);
for (; i < n; i++)
{
std::cout << std::this_thread::get_id() << ":" << i << std::endl;
}
std::cout << std::endl;
}
int main()
{
// 创建两个线程
std::thread t1(Print, 10, 0);
std::thread t2(Print, 20, 10);
// 获取线程 ID
std::cout << "t1 的线程 ID: " << t1.get_id() << std::endl;
std::cout << "t2 的线程 ID: " << t2.get_id() << std::endl;
// 等待线程完成
t1.join();
t2.join();
// 获取当前运行线程 ID(主线程)
std::cout << "主线程 ID: " << std::this_thread::get_id() << std::endl;
return 0;
}
使用 Lambda 表达式
std::thread 同样支持 lambda 表达式,这让代码更加简洁:
cpp
#include <iostream>
#include <thread>
int main()
{
int n = 10;
// 使用 lambda 表达式创建线程
std::thread t([n]()
{
for (int i = 0; i < n; i++)
{
std::cout << std::this_thread::get_id() << ":" << i << std::endl;
}
});
t.join();
return 0;
}
使用函数对象(仿函数)
函数对象(也叫仿函数)是重载了 operator() 的类对象,可以像函数一样被调用:
cpp
#include <iostream>
#include <thread>
// 函数对象(仿函数)
class Task
{
public:
void operator()(int n)
{
for (int i = 0; i < n; i++)
{
std::cout << "函数对象执行: " << i << std::endl;
}
}
};
int main()
{
Task taskObj; // 创建函数对象
// 直接传递函数对象
std::thread t(taskObj, 10);
t.join();
return 0;
}
注意:也可以使用临时对象,但需要注意语法:
cpp
// ✅ 正确:使用额外的括号避免被解析为函数声明
std::thread t((Task()), 10);
// 或者使用统一初始化语法
std::thread t{Task(), 10};
使用成员函数
如果需要在线程中调用类的成员函数,需要传递对象指针或引用:
cpp
#include <iostream>
#include <thread>
class Worker
{
public:
void doWork(int n)
{
for (int i = 0; i < n; i++)
{
std::cout << "工作线程: " << i << std::endl;
}
}
};
int main()
{
Worker workerObj;
// 传递对象指针和成员函数指针
std::thread t(&Worker::doWork, &workerObj, 10);
t.join();
return 0;
}
传递引用参数
默认情况下,std::thread 会复制所有参数。如果需要传递引用,需要使用 std::ref 或 std::cref:
cpp
#include <iostream>
#include <thread>
#include <functional>
void increment(int& n)
{
n++;
std::cout << "在子线程中: n = " << n << std::endl;
}
int main()
{
int num = 42;
std::cout << "主线程中: num = " << num << std::endl; // 输出 42
// ❌ 错误:会尝试复制引用类型,导致编译错误
// 因为引用类型不能直接复制,必须使用 std::ref 包装
// std::thread t(increment, num); // 编译错误!
// ✅ 正确:使用 std::ref 传递引用
std::thread t(increment, std::ref(num));
t.join();
std::cout << "主线程中: num = " << num << std::endl; // 输出 43
return 0;
}
说明:
std::ref()用于传递非 const 引用std::cref()用于传递 const 引用- 如果不使用
std::ref,参数会被复制,修改不会影响原始变量
常见错误和注意事项
1. 忘记 join 或 detach
每个线程对象在销毁前必须调用 join() 或 detach(),否则当线程对象析构时,如果线程仍然是可加入的(joinable),程序会调用 std::terminate() 终止。
cpp
// ❌ 错误:线程对象销毁前没有 join 或 detach
{
std::thread t(task);
// 作用域结束,t 被销毁,但没有 join 或 detach
} // 程序终止!
// ✅ 正确方式 1:显式 join
{
std::thread t(task);
t.join();
}
// ✅ 正确方式 2:使用 detach
{
std::thread t(task);
t.detach();
}
2. 多次调用 join
一个线程对象只能 join 一次:
cpp
std::thread t(task);
t.join();
t.join(); // ❌ 抛出 std::system_error 异常
3. 参数的生命周期问题
确保传递给线程的参数在线程执行期间保持有效:
cpp
// ❌ 危险:局部变量可能在线程执行前就被销毁
void badExample()
{
int localVar = 42;
std::thread t([&localVar]()
{
std::this_thread::sleep_for(std::chrono::seconds(1));
std::cout << localVar << std::endl; // 可能访问已销毁的变量
});
t.detach(); // 主函数返回,localVar 被销毁
}
// ✅ 正确:按值捕获或确保生命周期
void goodExample()
{
int localVar = 42;
std::thread t([localVar]() // 按值捕获
{
std::this_thread::sleep_for(std::chrono::seconds(1));
std::cout << localVar << std::endl; // 安全
});
t.join(); // 等待线程完成
}
4. 异常安全
如果在线程创建后、join 前发生异常,可能导致线程对象没有正确 join:
cpp
// ❌ 不够安全
void unsafe()
{
std::thread t(task);
someFunction(); // 如果这里抛出异常,t 可能没有 join
t.join();
}
// ✅ 使用 RAII 确保异常安全
class ThreadGuard
{
std::thread t;
public:
explicit ThreadGuard(std::thread&& t_) : t(std::move(t_))
{
}
~ThreadGuard()
{
if (t.joinable())
{
t.join();
}
}
ThreadGuard(const ThreadGuard&) = delete;
ThreadGuard& operator=(const ThreadGuard&) = delete;
};
void safe()
{
std::thread t(task);
ThreadGuard guard(std::move(t)); // 移动线程所有权,确保即使发生异常也会 join
someFunction();
}
与平台 API 的对比
为了更好地理解 std::thread 的优势,我们来看看它与平台特定 API 的对比:
Linux pthread
c
#include <pthread.h>
#include <stdio.h>
void* task(void* arg)
{
int* n = (int*)arg;
printf("线程执行: %d\n", *n);
return NULL;
}
int main()
{
pthread_t tid;
int n = 42;
pthread_create(&tid, NULL, task, &n);
pthread_join(tid, NULL);
return 0;
}
Windows Thread API
cpp
#include <windows.h>
#include <iostream>
DWORD WINAPI task(LPVOID lpParam)
{
int* n = (int*)lpParam;
std::cout << "线程执行: " << *n << std::endl;
return 0;
}
int main()
{
int n = 42;
HANDLE hThread = CreateThread(
NULL, // 安全属性
0, // 栈大小
task, // 线程函数
&n, // 参数
0, // 创建标志
NULL // 线程 ID
);
WaitForSingleObject(hThread, INFINITE);
CloseHandle(hThread);
return 0;
}
C++11 std::thread
cpp
#include <iostream>
#include <thread>
void task(int n)
{
std::cout << "线程执行: " << n << std::endl;
}
int main()
{
int n = 42;
std::thread t(task, n); // 简洁、类型安全、跨平台
t.join();
return 0;
}
对比总结:
| 特性 | pthread | Windows API | std::thread |
|---|---|---|---|
| 跨平台 | ❌ | ❌ | ✅ |
| 类型安全 | ❌ | ❌ | ✅ |
| 参数传递 | 需要 void* | 需要 void* | 直接传递 |
| 代码简洁性 | 中等 | 复杂 | 简洁 |
| 面向对象 | ❌ | ❌ | ✅ |
总结
std::thread 是 C++11 并发编程的基础,它为我们提供了:
- 跨平台的线程管理:一套代码,多平台运行
- 现代化的接口设计:充分利用 C++11 特性,使用更加便捷
- 类型安全:编译时类型检查,减少运行时错误
- 灵活的调用方式:支持函数、函数对象、lambda 表达式、成员函数等
掌握 std::thread 是学习 C++11 并发编程的第一步。在后续章节中,我们将学习如何配合互斥锁、条件变量等同步原语,构建更加复杂和强大的并发程序。
练习建议
- 编写一个程序,创建多个线程,每个线程打印不同的数字序列
- 尝试使用 lambda 表达式创建线程
- 实现一个简单的线程池,使用
std::vector<std::thread>管理多个线程 - 思考:为什么
std::thread不支持拷贝构造?移动语义在这里起到了什么作用?
综合示例:简单的线程池实现
下面是一个简单的线程池实现,展示了如何创建多个线程、使用 lambda 表达式,并确保异常安全:
cpp
#include <iostream>
#include <thread>
#include <vector>
#include <mutex>
#include <stdexcept>
// 简单的线程池类(异常安全)
class SimpleThreadPool
{
public:
// 构造函数:创建指定数量的线程
explicit SimpleThreadPool(size_t threadCount)
: threadCount(threadCount)
{
threads.reserve(threadCount);
// 使用 lambda 表达式创建多个线程,每个线程负责打印特定的数字
for (size_t i = 0; i < threadCount; ++i)
{
threads.emplace_back([this, i, threadCount]()
{
// 每个线程负责打印特定模式的数字序列
for (int num = static_cast<int>(i); num < 20; num += static_cast<int>(threadCount))
{
// 等待轮到自己打印
// 注意:锁必须在循环内部,因为 cv.wait() 会释放锁并等待
// 如果锁在循环外面,一个线程会一直持有锁,其他线程无法执行
std::unique_lock<std::mutex> lock(mtx);
// cv.wait() 会自动释放锁,等待条件满足后重新获取锁
cv.wait(lock, [this, num]() { return currentNum == num; });
// 打印数字(此时持有锁,保证输出和更新的原子性)
std::cout << "线程 " << i
<< " (ID: " << std::this_thread::get_id()
<< ") 打印: " << num << std::endl;
// 更新当前数字,通知下一个线程(在持有锁的情况下更新)
currentNum++;
cv.notify_all();
// 锁在这里自动释放(lock 对象析构)
}
});
}
}
// 析构函数:确保所有线程都被 join(异常安全)
~SimpleThreadPool()
{
// RAII:即使发生异常,也会确保所有线程被 join
for (auto& t : threads)
{
if (t.joinable())
{
t.join();
}
}
}
// 禁止拷贝构造和拷贝赋值(因为 std::thread 不可拷贝)
SimpleThreadPool(const SimpleThreadPool&) = delete;
SimpleThreadPool& operator=(const SimpleThreadPool&) = delete;
// 允许移动构造和移动赋值
SimpleThreadPool(SimpleThreadPool&&) = default;
SimpleThreadPool& operator=(SimpleThreadPool&&) = default;
// 启动所有线程(开始打印)
void start()
{
// 通知所有线程开始工作
std::lock_guard<std::mutex> lock(mtx);
cv.notify_all();
}
// 等待所有线程完成
void waitAll()
{
for (auto& t : threads)
{
if (t.joinable())
{
t.join();
}
}
}
private:
std::vector<std::thread> threads;
std::mutex mtx; // 互斥锁
std::condition_variable cv; // 条件变量
int currentNum = 0; // 当前应该打印的数字
size_t threadCount; // 线程数量
};
int main()
{
// 创建包含4个线程的线程池
SimpleThreadPool pool(4);
// 启动所有线程(开始顺序打印)
pool.start();
// 等待所有线程完成
pool.waitAll();
std::cout << "所有线程执行完毕!数字已按顺序打印:0, 1, 2, 3, 4, 5..." << std::endl;
return 0;
}
代码说明:
-
多个线程顺序打印不同数字序列:
- 使用循环创建多个线程
- 每个线程负责打印特定模式的数字(线程0: 0,4,8...,线程1: 1,5,9...)
- 使用条件变量
std::condition_variable控制打印顺序,确保数字按 0,1,2,3,4... 的顺序打印 - 每个线程等待轮到自己打印的数字,打印后通知下一个线程
-
使用 lambda 表达式:
- 使用
emplace_back直接构造线程,传入 lambda 表达式 - Lambda 捕获
this、线程索引i和线程总数threadCount(用于计算下一个要打印的数字)
- 使用
-
简单的线程池实现:
- 使用
std::vector<std::thread>管理多个线程 - 提供
start()方法启动所有线程 - 提供
waitAll()方法等待所有线程完成
- 使用
-
异常安全(RAII):
- 析构函数中确保所有线程都被
join() - 即使发生异常,析构函数也会被调用,保证资源正确释放
- 禁止拷贝构造和拷贝赋值(因为
std::thread不可拷贝) - 允许移动构造和移动赋值
- 析构函数中确保所有线程都被
-
同步机制:
- 使用
std::mutex和std::condition_variable实现线程同步 - 通过
currentNum变量控制打印顺序 - 每个线程使用
cv.wait()等待轮到自己打印,打印后更新currentNum并通知其他线程 - 锁的位置说明 :锁必须在 for 循环内部,原因如下:
cv.wait()会自动释放锁并进入等待状态,被唤醒后重新获取锁- 如果锁在循环外面,一个线程会一直持有锁,其他线程无法执行,失去并发意义
- 每次循环都需要等待条件变量,所以每次循环都需要创建新的锁对象
- 锁保护的是"检查条件-打印-更新"这个原子操作,而不是整个循环
- 使用
为什么 std::thread 不支持拷贝构造?
- 线程对象代表一个实际的执行线程,拷贝一个线程对象在语义上不合理
- 一个线程只能被一个线程对象管理,拷贝会导致所有权混乱
- 移动语义允许转移线程的所有权,而不需要拷贝
移动语义的作用:
- 允许将线程对象的所有权从一个对象转移到另一个对象
- 在容器中存储线程对象时,必须使用移动语义
- 移动后,原线程对象变为空(不可 joinable),新对象拥有线程的所有权