C++11 ---- 线程库
目录
前言
一、thread库使用
1.1、构造
1.2、移动构造,移动赋值
1.3、线程函数的参数
二、mutex的种类
2.1、std::mutex
2.2、std::recursive_mutex
2.3、timed_mutex
三、RAII封装的锁
3.1、std::lock_guard
3.2、unique_lock
四、条件变量
4.1、经典问题
五、原子操作库
前言
为了实现windows 和 Linux 跨平台创建线程问题,C++11开始,搞了个线程库。把std::thread、std::mutex、std::automic 等组件收进标准库,语法与普通 C++ 代码无异,不再依赖平台 API 或外部库。
一份 std::thread 源码在 Windows、Linux、macOS 上都能直接编译运行,标准库负责把细节映射到各平台的底层线程实现。
一、thread库使用
1.1、构造
头文件:<thead>
template <class Fn, class... Args>
explicit thread(Fn&& fn, Args&&... args);
作用:启动新线程,并执行可调用对象 fn ,通过可变模板参数,传递参数 args...
作用:获取线程id,返回的id是一个成员类型,thread::id
与std::this_thread::get_id() 的区别
- std::thread::get_id() 是成员函数,需通过线程对象调用
- std::this_thread::get_id() 是命名空间函数,直接获取当前线程的ID
跑一个线程:
- 参数也支持传不同类型的参数
1.2、移动构造,移动赋值
C++11 的线程库支持移动构造和移动赋值,但不支持拷贝构造和赋值
#include <iostream>
#include <thread>
#include <vector>using namespace std;void Print(int val)
{std::cout << "线程id: " << std::this_thread::get_id()<< " " << val << std::endl;
}int main()
{int n = 10;vector<thread> threads(n);// 创建n个线程执行Printfor (auto& td : threads){// 这里是移动赋值td = thread(Print, 10);}for (auto& td : threads){td.join();}return 0;
}
输出结果:
移动构造同理:
int main()
{thread t1(Print, 10);//thread t2(t1); // 报错thread t2(move(t1)); // 正常执行t2.join();return 0;
}
为什么需要移动构造和移动赋值意义?
- 确保一个线程只能被一个对象管理,避免资源冲突。
- 可以利用vector缓存一批线程,再通过移动赋值将新线程高效加入池中
- thread也可执行其他可调用对象,lambda,仿函数,包装器
1.3、线程函数的参数
线程函数的参数是以值拷贝的方式拷贝到线程栈空间中的,因此:即使线程参数为引用类型,在 线程中修改后也不能修改外部实参,因为其实际引用的是线程栈中的拷贝,而不是外部实参。
如果想要通过形参改变外部实参时,必须借助std::ref()函数
#include <iostream>
#include <thread>
#include <mutex>using namespace std;void ThreadFunc1(int& x)
{x += 20;
}
void ThreadFunc2(int* x)
{*x += 20;
}
int main()
{int a = 10;// 在线程函数中对a修改,不会影响外部实参,因为:线程函数参数虽然是引用方式,// 但其实际引用的是线程栈中的拷贝thread t1(ThreadFunc1, a);t1.join();cout << a << endl;// 如果想要通过形参改变外部实参时,必须借助std::ref()函数thread t2(ThreadFunc1, std::ref(a));t2.join();cout << a << endl;// 地址的拷贝thread t3(ThreadFunc2, &a);t3.join();cout << a << endl;return 0;
}
结论:
- 当线程执行的函数的参数是左值引用时,传参的时候要加 ref
- 但const 引用是可以不需要 ref 的
二、mutex的种类
头文件:<mutex>
2.1、std::mutex
C++11提供的互斥量,该类的对象之间不能拷贝,也不能进行移动。mutex常用函数:
函数名 | 功能 |
lock() | 加锁 |
unlock() | 解锁 |
try_lock() | 尝试加锁,如果互斥量被其他线程占有,则当前线程不会被阻塞 |
示例如下:
#include <iostream>
#include <thread>
#include <mutex>using namespace std;int main()
{int x = 0;int n = 10000;mutex mtx; // 互斥锁thread t1([&x, &mtx, n]() {for (int i = 0;i < n;i++){mtx.lock();++x;mtx.unlock();}});thread t2([&x, &mtx, n]() {for (int i = 0;i < n;i++){mtx.lock();++x;mtx.unlock();}});t1.join();t2.join();cout << x << endl; // 输出20000return 0;
}
注:锁不支持传值
2.2、std::recursive_mutex
递归互斥锁,允许同一个线程对互斥量多次上锁(即递归上锁),来获得对互斥量对象的多层所有权, 释放互斥量时需要调用与该锁层次深度相同次数的 unlock()
#include <iostream>
#include <mutex>// 递归函数:计算阶乘
int func(int n, std::recursive_mutex& mtx)
{if (n <= 1) return 1;mtx.lock(); // 递归获取锁int result = n * func(n - 1, mtx);mtx.unlock();return result;
}int main()
{std::recursive_mutex rmtx;// 同一个线程递归获取锁int result = func(5, rmtx);std::cout << "5! = " << result << std::endl;return 0;
}
2.3、timed_mutex
带超时功能的互斥锁,比mutex多两个成员函数
成员函数 | 功能 |
try_lock_for() | 接受一个时间范围,表示在这一段时间范围之内线程如果没有获得锁则被阻塞住,如果在此期间其他线程释放了锁,则该线程可以获得对互斥量的锁,如果超时(即在指定时间内还是没有获得锁),则返回 false。 |
try_lock_until() | 接受一个时间点作为参数,在指定时间点未到来之前线程如果没有获得锁则被阻塞住, 如果在此期间其他线程释放了锁,则该线程可以获得对互斥量的锁,如果超时(即在指定时间内还是没有获得锁),则返回 false。 |
#include <iostream>
#include <thread>
#include <mutex>
#include <chrono>using namespace std;std::timed_mutex tmtx; // 带超时功能的互斥锁void worker(int id)
{// 尝试在100毫秒内获取锁if (tmtx.try_lock_for(std::chrono::milliseconds(100))) {std::cout << "线程 " << id << " 成功获取锁\n";// 模拟工作std::this_thread::sleep_for(std::chrono::milliseconds(50));std::cout << "线程 " << id << " 释放锁\n";tmtx.unlock();}else {std::cout << "线程 " << id << " 获取锁超时\n";}
}int main()
{std::thread t1(worker, 1);std::thread t2(worker, 2);// 让主线程稍等,确保两个工作线程同时运行std::this_thread::sleep_for(std::chrono::milliseconds(10));t1.join();t2.join();return 0;
}
其中:
- <chrono> 是C++11引入的标准库头文件,提供了一套时间处理工具
- std::this_thread::sleep_for,这是 <thread> 头文件中定义的函数,用于阻塞当前线程一段指定的时间。
三、RAII封装的锁
用RAII封装锁的目的,是为了防止在加锁后,解锁之前抛了异常或者其他情况,而导致的死锁问题
#include <iostream>
#include <thread>
#include <mutex>
#include <stdexcept>std::mutex _mtx;void threadA_func()
{_mtx.lock();int x = 1;int y = 0;int z = x / y; // 除零抛异常, 导致未能释放锁_mtx.unlock();
}int main()
{std::thread t1(threadA_func);t1.join();return 0;
}
3.1、std::lock_guard
std::lock_gurad 是 C++11 中定义的模板类。底层类似下面:
template<class _Mutex>
class lock_guard
{
public:// 在构造lock_gard时,_Mtx还没有被上锁explicit lock_guard(_Mutex& _Mtx): _MyMutex(_Mtx){_MyMutex.lock();}// 在构造lock_gard时,_Mtx已经被上锁,此处不需要再上锁lock_guard(_Mutex& _Mtx, adopt_lock_t): _MyMutex(_Mtx){}~lock_guard(){_MyMutex.unlock();}lock_guard(const lock_guard&) = delete;lock_guard& operator=(const lock_guard&) = delete;
private:_Mutex& _MyMutex;
};
注意:成员变量是引用,因为锁不支持拷贝
lock_guard类模板主要是通过RAII的方式,对其管理的互斥量进行了封装,在需要加锁的地方,只需要用上述介绍的任意互斥体实例化一个lock_guard,调用构造函数成功上锁,出作用域前,lock_guard对象要被销毁,调用析构函数自动解锁,可以有效避免死锁问题。
3.2、unique_lock
与lock_gard类似,unique_lock类模板也是采用RAII的方式对锁进行了封装,并且也是以独占所有权的方式管理mutex对象的上锁和解锁操作,即其对象之间不能发生拷贝。
与lock_guard不同的是,unique_lock更加的灵活,提供了更多的成员函数:
- 上锁/解锁操作:lock、try_lock、try_lock_for、try_lock_until和unlock
- 修改操作:移动赋值、交换(swap:与另一个unique_lock对象互换所管理的互斥量所有 权)、释放(release:返回它所管理的互斥量对象的指针,并释放所有权)
- 获取属性:owns_lock(返回当前对象是否上了锁)、operator bool()(与owns_lock()的功能相同)、mutex(返回当前unique_lock所管理的互斥量的指针)。
四、条件变量
条件变量的本质就是操作系统内核里的一个“等待队列 + 原子化的睡眠/唤醒”——它本身不保存任何业务状态,只做三件事:
-
把线程放进队列
-
把关联的互斥量原子地解锁并让线程睡眠
-
当别的线程调用 notify 时,把队列里的线程唤醒并重新加锁
常用函数:
函数名 | 函数功能 |
wait | 阻塞当前线程(把当前线程放入等待队列),直到被其他线程通过 notify_one() 或 notify_all() 唤醒 |
wait_for | 阻塞当前线程,直到被唤醒或超过指定的时间长度 |
wait_until | 阻塞当前线程,直到被唤醒或到达指定的时间点(例如:等待到 2023-12-31 23:59) |
notify_one | 唤醒一个正在等待此条件变量的线程(如果有多个等待线程,唤醒顺序不确定) |
notify_all | 唤醒所有正在等待此条件变量的线程 |
注:wait等待时会自动解锁,被唤醒时会重新加锁
4.1、经典问题
实现支持两个线程交替打印,一个打印奇数,一个打印偶数
#include <iostream>
#include <thread>
#include <mutex>
#include <chrono>using namespace std;int main()
{mutex mtx;condition_variable cond;int x = 1;bool flag = false;thread t1([&]() {for (int i = 0;i < 10000;i++){unique_lock<mutex> lock(mtx);while (flag)cond.wait(lock);cout << this_thread::get_id() << " : " << x++ << endl;flag = true;lock.unlock();cond.notify_one();}});thread t2([&]() {for (int i = 0;i < 10000;i++){unique_lock<mutex> lock(mtx);while(!flag)cond.wait(lock);cout << this_thread::get_id() << " : " << x++ << endl;flag = false;lock.unlock();cond.notify_one();}});t1.join();t2.join();return 0;
}
分析:上面 t1 打印奇数,t2打印偶数
五、原子操作库
automic是C++封装的一个模板类,使得不用加锁解锁,也可以保证变量的原子操作
使用要加头文件:<automic>
#include <iostream>
#include <atomic>
#include <thread>std::atomic<int> x = 0; // 原子整型void add()
{for (int i = 0; i < 100000; ++i)++x;
}int main()
{std::thread t1(add);std::thread t2(add);t1.join();t2.join();std::cout << x << '\n'; // 输出 200000return 0;
}
其他成员函数:
成员函数 | 函数功能 |
automic::store(val) | 原子地将值写入原子对象,保证写入操作是原子的 |
automic::load() | 原子的读,保证读取操作看到完整的值(不会读取到部分写入的数据) |
#include <iostream>
#include <atomic>
#include <thread>std::atomic<int> x{0}; // 原子变量
int y = 0; // 普通变量void writer()
{x.store(42); // 原子写y = 42; // 普通写
}void reader()
{std::this_thread::sleep_for(std::chrono::milliseconds(100)); // 等写线程先跑std::cout << "x = " << x.load() << '\n'; // 原子读,永远拿到 42std::cout << "y = " << y << '\n'; // 普通读,可能拿到 0 或 42
}int main()
{std::thread t1(writer);std::thread t2(reader);t1.join();t2.join();return 0;
}