C++线程(thread)
C++11引入多线程支持,开发者通过<thread>
库实现并行处理。
线程是操作系统执行调度的最小单位,也是程序执行的最小单元。
1、thread库概述
<thread>
提供管理线程,保护共享数据、线程间同步操作、原子操作的基本功能。
1.1 thread的成员函数
join()和detach()
join 会阻塞当前线程,当在一个线程中调用另一个线程对象的 join()
方法时,当前线程会暂停执行,等待被调用的线程完成其任务。
detach 会将线程对象与执行的线程分离,被分离的线程会在后台独立运行,不再受原线程对象的控制。调用 detach()
后,该线程对象不再关联任何线程。
C++
// 模拟一个耗时的任务
void longTask() {
std::cout << "子线程开始执行耗时任务..." << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(5));
std::cout << "子线程完成耗时任务。" << std::endl;
}
int main() {
std::cout << "主线程开始。" << std::endl;
// 使用 join 的情况
std::cout << "\n使用 join 的情况:" << std::endl;
std::thread joinThread(longTask);
std::cout << "主线程等待 join 线程完成..." << std::endl;
joinThread.join();
std::cout << "join 线程已完成,主线程继续。" << std::endl;
// 使用 detach 的情况
std::cout << "\n使用 detach 的情况:" << std::endl;
std::thread detachThread(longTask);
std::cout << "主线程分离 detach 线程并继续执行..." << std::endl;
detachThread.detach();
std::this_thread::sleep_for(std::chrono::seconds(1));
std::cout << "主线程不等待 detach 线程,继续执行其他任务。" << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(1));
std::cout << "主线程完成其他任务,准备退出。" << std::endl;
return 0;
}
代码中如果修改detach段的第二个sleep_for
的等待时间,则会得到两个不一样的结果。究其原因就是 detach
分离线程导致的,如果子线程的耗时任务大于主线的退出时间,则子线程不能完成。
不论是
join
还是detach
,运行后joinable
固定返回 false
joinable()
joinable()
用于检测线程是否可以join
或detach
,其本质是判断thread
对象是否与活跃线程相关联。
1.2 thread的构造函数
c++11为thread类提供了无参构造 和含可变参数构造两种主要初始化构造方式。删除了拷贝构造(毕竟线程可以拷贝不合理),提供了移动构造来配合无参构造使用。
C++
#include<iostream>
#include<thread>
using namespace std;
void func(int n)
{
int x = 0;
for (int i = 0; i < n; i++)
{
++x;
}
cout << x << endl;
}
int main()
{
thread td1(func,100);
thread td2;
td2 = thread(func, 200);
td1.join();
td2.join();
return 0;
}
无参构造一般用于被数据结构创建并管理的情景:
C++
#include<iostream>
#include<vector>
#include<thread>
#include<mutex>
using namespace std;
int main()
{
vector<thread> thd;
thd.resize(5);
mutex mtx;
int x = 0;
auto func = [&](int n) {
mtx.lock();
for (int i = 0; i < n; i++)
{
++x;
}
mtx.unlock();
};
for (auto& nthd : thd)
{
nthd = thread(func, 100);//移动构造
}
for (auto& nthd : thd)
{
nthd.join();
}
return 0;
}
2、线程锁
线程之间的锁的类型有很多,按功能和使用场景有如互斥锁、条件锁、自旋锁、读写锁、递归锁。其主要目的是用于控制多个线程对共享资源的访问,避免数据竞争和不一致的问题。
2.1 互斥锁(Mutex)
功能:提供独占式资源 访问,保证同一时间只有同一个线程访问资源。一个线程获取互斥锁后,其他线程获锁尝试将被阻塞。
使用:互斥锁用于共享资源保护 ,确保在多个线程 访问同一个共享资源时,保证线程安全。
C++
mutex mtx;
int i = 0;
void fun(int n) {
while (i < n) {
i++;
cout << i << " ";
}
}
int main() {
thread t1(fun, 100);
thread t2(fun, 120);
t1.join();
t2.join();
return 1;
}
如果没有加锁,两个线程同时访问共享数据会引发混乱。
我们可以通过互斥锁的方式保证资源只被同一线程使用。
C++
void fun(int n) {
mtx.lock();
while (i < n) {
i++;
cout << i << " ";
}
mtx.unlock();
}
在线程访问数据时上锁,保证其他线程不会同时访问数据,直到循环完成,下一个线程才可以继续访问数据。
2.2 条件锁(Condition Variable)
功能:作用线程 而非共享资源,主要是用于线程间的同步 。某个线程因为条件变量 而阻塞,当其他线程改变条件使其满足时,发出信号量,该线程继续执行。
使用:线程等待条件成立。比如:生产------消费者模型。
示例:生产------消费者模型
C++
mutex mtx;
condition_variable cv;
queue<string> taskQuene;
const int MAX_SIZE = 20;
string product() {
int num1 = rand() % 10;
int num2 = rand() % 10;
return to_string(num1) + "+" + to_string(num2) + "=";
}
void solve(string exp) {
vector<int> v;
for (char c : exp) {
if (isdigit(c)) {
string str(1, c);
v.push_back(stoi(str));
}
}
int num1 = v.back();
v.pop_back();
int num2 = v.back();
int res = num1 + num2;
cout << exp << res << endl;
}
void producer() {
for (int i = 0; i < 10; i++) {
unique_lock<mutex> lock(mtx);
cv.wait(lock, [] {return taskQuene.size() < MAX_SIZE; });
string expression = product();
taskQuene.push(expression);
cout << "Produced: " << expression << endl;
lock.unlock();
cv.notify_one();
this_thread::sleep_for(chrono::milliseconds(200));
}
}
void consumer() {
while (true) {
unique_lock<mutex> lock(mtx);
cv.wait(lock, [] {return !taskQuene.empty(); });
string task = taskQuene.front();
taskQuene.pop();
solve(task);
lock.unlock();
cv.notify_one();
this_thread::sleep_for(chrono::milliseconds(200));
}
}
int main()
{
thread producerThread1(producer);
thread consumerThread1(consumer);
producerThread1.join();
consumerThread1.detach();
}
2.3 递归锁(Recurise Mutex)
功能:特殊的互斥锁 ,能被同一线程多次获取。只有所有获取操作被释放,其他线程才能获得锁。
使用:在递归函数或者嵌套调用中,同一线程多次进入临界区。比如:状态机模型,嵌套调用的同步模型。
电梯状态机:
C++
enum class ElevatorState {
IDLE,
MOVING_UP,
MOVING_DOWN
};
class ElevatorStateMachine {
private:
ElevatorState currentState;
recursive_mutex mtx;
void transitionTo(ElevatorState newState) {
lock_guard<recursive_mutex> lock(mtx);
currentState = newState;
switch (newState)
{
case ElevatorState::IDLE:
cout << "状态为停止" << endl;
break;
case ElevatorState::MOVING_UP:
cout << "状态为上升" << endl;
break;
case ElevatorState::MOVING_DOWN:
cout << "状态为下降" << endl;
break;
default:
break;
}
}
public:
ElevatorStateMachine() : currentState(ElevatorState::IDLE) {}
// 处理上升指令
void goUp() {
std::lock_guard<std::recursive_mutex> lock(mtx);
if (currentState != ElevatorState::MOVING_UP) {
transitionTo(ElevatorState::MOVING_UP);
}
}
// 处理下降指令
void goDown() {
std::lock_guard<std::recursive_mutex> lock(mtx);
if (currentState != ElevatorState::MOVING_DOWN) {
transitionTo(ElevatorState::MOVING_DOWN);
}
}
// 处理停止指令
void stop() {
std::lock_guard<std::recursive_mutex> lock(mtx);
if (currentState != ElevatorState::IDLE) {
transitionTo(ElevatorState::IDLE);
}
}
};
// 线程函数,模拟不同的操作
void operateElevator(ElevatorStateMachine& elevator, int operations) {
for (int i = 0; i < operations; ++i) {
int randomOp = rand() % 3;
switch (randomOp) {
case 0:
elevator.goUp();
break;
case 1:
elevator.goDown();
break;
case 2:
elevator.stop();
break;
}
}
}
int main()
{
//srand(time(nullptr));
ElevatorStateMachine elevator;
vector<thread> threads;
// 创建多个线程
for (int i = 0; i < 3; ++i) {
threads.emplace_back(operateElevator, std::ref(elevator), 5);
}
// 等待所有线程完成
for (auto& thread : threads) {
thread.join();
}
return 0;
}
2.4 自旋锁(Spin Lock)
功能:线程申请失败时,不会进入阻塞 ,状态线程会不断的申请 锁直到成功申请。类似于while
循环。减少了线程上下文切换的开销。
使用:性能要求极高,锁持有时间极短的场景。例如:实时系统的任务同步。
2.5 读写锁(Read - Write Lock)
功能:读者---写者模型,共享资源访问存在两种操作。允许多线程同时执行一种操作 ,进行另一操作 时则仅运行单线程。
场景:读者---写者模型。
C++
std::mutex mtx;
std::condition_variable cvReaders, cvWriters;
int readers = 0;
int writers = 0;
bool writing = false;
// 读者函数
void reader(int id) {
std::unique_lock<std::mutex> lock(mtx);
// 等待没有写者正在写且没有写者在等待
cvReaders.wait(lock, [] { return!writing && writers == 0; });
readers++;
lock.unlock();
std::cout << "读者 " << id << " 在读. 共有读者: " << readers << std::endl;
// 模拟读取操作
std::this_thread::sleep_for(std::chrono::milliseconds(100));
lock.lock();
readers--;
// 如果当前没有读者了,唤醒可能等待的写者
if (readers == 0) {
cvWriters.notify_one();
}
lock.unlock();
std::cout << "读者 " << id << " 完成读入." << std::endl;
}
// 写者函数
void writer(int id) {
std::unique_lock<std::mutex> lock(mtx);
writers++;
// 等待没有读者正在读且没有写者正在写
cvWriters.wait(lock, [] { return readers == 0 && !writing; });
writing = true;
writers--;
lock.unlock();
std::cout << "写者 " << id << " 写入." << std::endl;
// 模拟写操作
std::this_thread::sleep_for(std::chrono::milliseconds(200));
lock.lock();
writing = false;
// 如果有写者在等待,优先唤醒写者,否则唤醒所有读者
if (writers > 0) {
cvWriters.notify_one();
}
else {
cvReaders.notify_all();
}
lock.unlock();
std::cout << "写者 " << id << " 写完." << std::endl;
}
int main() {
std::vector<std::thread> threads;
// 创建多个读者和写者线程
for (int i = 0; i < 3; ++i) {
threads.emplace_back(reader, i);
}
for (int i = 0; i < 2; ++i) {
threads.emplace_back(writer, i);
}
// 等待所有线程完成
for (auto& thread : threads) {
thread.join();
}
return 0;
}
3、锁守卫
在 C++ 多线程编程中,锁守卫(Lock Guard)是一种利用 RAII技术来管理锁的工具。对象构造时获取锁 ,析构时释放锁。
RAII(Resource Acquisition Is Initialization)即资源获取即初始化,是一种将资源的生命周期与对象生命周期绑定的技术。
3.1 lock_guard
.lock_guard
在构造时锁定关联的互斥锁,在析构时解锁该互斥锁,不支持手动解锁和重新锁定操作。
C++
mutex mtx;
int val = 0;
void func() {
lock_guard<mutex> lock(mtx);
int res = val;
while (val < res + 10000) {
val++;
}
cout << val << endl;
}
int main()
{
thread t1(func);
thread t2(func);
t1.join();
t2.join();
return 1;
}
取消锁守卫可以参看互斥锁的例子,lock_guard
本质上就是一个自助管理锁的对象。
3.2 unique_lock
unique_lock
提供比lock_guard
的管理功能。支持延迟锁定、手动解锁、重新锁定以及与条件变量配合使用的操作。
4、原子操作
C++的 atomic
类是 C++11 标准引入的,支持原子操作,主要用于多线程编程中 。原子操作是一种不可分割 的最小且不可并行操作,在多线程环境中只存在完成和未完成两种形式。
原子操作的特点就是在执行过程中不可被中断的操作。
C++
std::atomic<int> counter(0);
// 线程函数,对计数器进行递增操作
void increment() {
for (int i = 0; i < 100000; ++i) {
++counter;
}
}
int main() {
std::vector<std::thread> threads;
// 创建多个线程
for (int i = 0; i < 10; ++i) {
threads.emplace_back(increment);
}
// 等待所有线程完成
for (auto& t : threads) {
t.join();
}
std::cout << "Final counter value: " << counter << std::endl;
return 0;
}
从多线程编程的角度考虑,原子操作同样能使多个线程安全共享数据。
4.1 构造函数
atomic
类似于 STL 的容器,实例化对象时要指明实例化对象的类型。 atomic
支持实例化整型类型、bool、字符类型(某些编译可能不支持)、指针类型等类型,同时也支持满足特定条件的自定义类型。
C++
#include <atomic>
std::atomic<int> n;
std::atomic<char> ch;
std::atomic<bool> is;
4.2 成员函数
fetch_add() 和 fetch_sub()
这两个函数分别用于进行自增和自减操作,它能保证这个操作的过程是原子的。对于这两个函数的 sync
参数,它它决定了当前线程和其他线程之间对内存的访问顺序,默认缺省使用 std::memory_order_seq_cst
它提供最强的顺序一致性保证,确保所有线程的操作按顺序进行,类似于全局的同步操作。它保证所有线程中所有的原子操作遵循单一的顺序。 在绝大多数情况下,使用默认的 sync
即可。
实例:
C++
#include <iostream>
#include <atomic>
#include <thread>
std::atomic<int> for_add(0);
std::atomic<int> for_sub(0);
void increment() {
for (int i = 0; i < 500; ++i) {
for_add.fetch_add(1); // 原子加1
}
}
void decrement() {
for (int i = 0; i < 500; ++i) {
for_sub.fetch_sub(1); // 原子减1
}
}
int main() {
std::thread a1(increment);
std::thread a2(increment);
std::thread s1(decrement);
std::thread s2(decrement);
a1.join();
a2.join();
s1.join();
s2.join();
std::cout << "for_add: " << for_add << std::endl; // 输出 1000
std::cout << "for_sub: " << for_sub << std::endl; // 输出 -1000
return 0;
}
store()
原子地将一个值存储到原子变量中。
C++
std::atomic<int> atomicVar(0);
void writer() {
atomicVar.store(100);
}
4.3 自定义类型
atomic
类也可以用于自定义类型的变量,但自定义类型必须满足:
-
满足标准布局(Standard Layout) 要求。
标准布局即没有虚拟继承、成员变量必须按顺序排列、没有特殊的构造函数和析构函数。
-
满足可复制 和可赋值的要求。
需要有默认构造和拷贝构造和赋值。
-
必须支持合适的内存模型。
-
必须具有适当的对齐要求。
实际上, atomic
实例化自定义类型的要求就是自定义类型必须足够简单,在实际中很难把握,所以推荐自己加锁保证原子性和线程安全即可。
CSDN 文章作者同名