常用接口:
| 函数名 | 功能 |
|---|---|
| thread() | 构造一个线程对象,没有关联任何线程函数,即没有启动任何线程 |
| thread(fn, args1, args2, …) | 构造一个线程对象,并关联线程函数fn,args1,args2,…为线程函数的 参数 |
| get_id() | 获取线程id |
| jionable() | 判断线程是否还在执行 |
| jion() | 该函数调用后会阻塞主线程,当该线程结束后,主线程继续执行 |
| detach() | 分离线程 |
| swap(thread& x) | 交换两个线程 |
线程是操作系统中的一个概念,是进程中的一个执行分支,线程对象可以关联一个线程,用来控制线程以及获取线程的状态
当创建一个线程对象后,没有提供线程函数,该对象实际没有对应任何线程
#include <thread>
int main(){
std::thread t1;//空线程
return 0;
}
当创建一个线程对象后,并且给线程关联线程函数,该线程就被启动,与主线程一起运行
线程函数就是可调用对象,可以有以下形式:
当创建一个空线程时,thread是防拷贝的,不允许拷贝构造以及赋值,但是可以移动构造和移动赋值,即将一个线程对象关联线程的状态转移给其他线程对象,转移期间不影响线程的执行
#include <iostream>
#include <thread>
using namespace std;
void ThreadFunc(int a) {
cout << "Thread1" <<" " << a << endl;
}
void func(double i){
cout << i / 2 << endl;
}
class TOB {
public:
void operator()(){
cout << "Thread3" << endl;
}
};
int main(){
//函数指针移动赋值给t1
thread t1;
t1 = thread(ThreadFunc, 998);
//线程函数为lambda表达式
thread t2([]() {
cout << "Thread2" << endl;
});
//线程函数为仿函数对象
TOB tb;
thread t3(tb);
//线程函数为包装器
function<void(double)> f1 = func;
thread t4(f1, 999);
t1.join();
t2.join();
t3.join();
t4.join();
return 0;
}

可以通过jionable()函数判断线程是否是有效的,如果是以下任意情况,则线程无效
- 采用无参构造函数构造的线程对象
- 线程对象的状态已经转移给其他线程对象
- 线程已经调用jion或者detach结束
线程函数的参数是以值拷贝的方式拷贝到线程栈空间中的,因此:即使线程参数为引用类型,在线程中修改后也不能修改外部实参,因为其实际引用的是线程栈中的拷贝,而不是外部实参
#include <iostream>
#include <thread>
using namespace std;
void Func1(int& x){
x += 10;
return;
}
void Func2(int* x){
*x += 10;
return;
}
int main(){
int a = 10;
// 在线程函数中对a修改,不会影响外部实参,因为:线程函数参数虽然是引用方式,但其实际引用的是线程栈中的拷贝
// 如果线程函数的接收是引用接收,vs2022会报错
/*thread t1(Func1, a);
t1.join();
cout << a << endl;*/
// 如果想要通过形参改变外部实参时,必须借助std::ref()函数
thread t2(Func1, ref(a));
t2.join();
cout << a << endl;
// 也可以拷贝地址再解引用,实现对a的更改
thread t3(Func2, &a);
t3.join();
cout << a << endl;
return 0;
}
如果是类成员函数作为线程参数时,必须将this指针作为线程函数参数
#include <iostream>
#include <thread>
using namespace std;
class A{
public:
void Func1(int x){
cout << x << endl;
}
static void Func2(int x){
cout << x << endl;
}
};
int main(){
A* a = new A;
//非静态函数需要传入类的实例或者指针
thread t1(&A::Func1, a, 10);
//thread t1(&A::Func1, *a, 10);
t1.join();
delete a;
//静态成员函数只需传入函数指针和参数
thread t2(&A::Func2, 10);
t2.join();
return 0;
}
多线程的程序有多个执行流,那么如果一份资源可以被多个线程访问到,那么这个资源就是临界资源,如果多个线程同时对临界资源进行修改,就会出现线程安全问题
比如用两个线程同时对一个全局变量进行++操作
int main() {
vector<thread> vthreads;
vthreads.resize(2);
int N = 100000;
int x = 0;
for (auto& td : vthreads) {
td = thread([&N, &x] {
for (int i = 0; i < N; ++i)
{
++x;
}
});
}
for (auto& td : vthreads) {
td.join();
}
cout << x << endl;
return 0;
}
按道理来说两个线程都对x进行++,++了100000次,那么x的最终值会是2

但是实际却少了几万次,是因为**++操作不是原子操作**,当一个线程对x进行++时,另一个线程也在执行++操作,就会导致两次++操作只有一次有效,就有了线程安全问题
针对这个问题,要对临界区进行保护,那么可以在临界区加入互斥锁,保证线程安全
int main() {
vector<thread> vthreads;
vthreads.resize(2);
mutex mtx;
int N = 100000;
int x = 0;
for (auto& td : vthreads) {
td = thread([&mtx, &N, &x] {
mtx.lock();
for (int i = 0; i < N; ++i)
{
++x;
}
mtx.unlock();
});
}
for (auto& td : vthreads) {
td.join();
}
cout << x << endl;
return 0;
}

虽然加锁可以解决,但是加锁有一个缺陷就是:只要一个线程在对sum++时,其他线程就会被阻塞,会影响程序运行的效率,而且锁如果控制不好,还容易造成
死锁
因此C++11中引入了原子操作。所谓原子操作:即不可被中断的一个或一系列操作,C++11引入的原子操作类型,使得线程间数据的同步变得非常高效

int main() {
vector<thread> vthreads;
vthreads.resize(2);
int N = 100000;
//atomic<int> x = 0;
atomic_int x = 0;
for (auto& td : vthreads) {
td = thread([&N, &x]{
for (int i = 0; i < N; ++i)
{
//此时对x的++就是原子操作
++x;
}
});
}
for (auto& td : vthreads) {
td.join();
}
cout << x << endl;
return 0;
}

在C++11中,程序员不需要对原子类型变量进行加锁解锁操作,线程能够对原子类型变量互斥的访问,更为普遍的,程序员可以使用atomic类模板,定义出需要的任意原子类型
atmoic<T> t; // 声明一个类型为T的原子类型变量t
注意:原子类型通常属于"资源型"数据,多个线程只能访问单个原子类型的拷贝,因此在C++11中,原子类型只能从其模板参数中进行构造,不允许原子类型进行拷贝构造、移动构造以及operator=等,为了防止意外,标准库已经将atmoic模板类中的拷贝构造、移动构造、赋值运算符重载默认删除掉了。
C++11提供的最基本的互斥量,该类的对象之间不能拷贝,也不能进行移动
| 函数名 | 函数功能 |
|---|---|
| lock() | 上锁:锁住互斥量 |
| unlock() | 解锁:释放对互斥量的所有权 |
| try_lock() | 尝试锁住互斥量,如果互斥量被其他线程占有,则当前线程也不会被阻塞 |
线程函数调用lock()时可能会发生以下三种情况:
- 如果该互斥量当前没有被锁住,则调用线程将该互斥量锁住,直到调用 unlock之前,该线程一直拥有该锁
- 如果当前互斥量被其他线程锁住,则当前的调用线程被阻塞住
- 如果当前互斥量被当前调用线程锁住,则会产生死锁(deadlock)
线程函数调用try_lock()时可能会发生以下三种情况:
- 如果当前互斥量没有被其他线程占有,则该线程锁住互斥量,直到该线程调用 unlock 释放互斥量
- 如果当前互斥量被其他线程锁住,则当前调用线程返回 false,而并不会被阻塞掉
- 如果当前互斥量被当前调用线程锁住,则会产生死锁(deadlock)
允许递归程序加锁
- 比 std::mutex 多了两个成员函数,try_lock_for(),try_lock_until() , try_lock_for()
- 接受一个时间范围,表示在这一段时间范围之内线程如果没有获得锁则被阻塞住(与std::mutex 的 try_lock() 不同,try_lock 如果被调用时没有获得锁则直接返回false),如果在此期间其他线程释放了锁,则该线程可以获得对互斥量的锁,如果超时(即在指定时间内还是没有获得锁),则返回 false
- try_lock_until()接受一个时间点作为参数,在指定时间点未到来之前线程如果没有获得锁则被阻塞住,如果在此期间其他线程释放了锁,则该线程可以获得对互斥量的锁,如果超时(即在指定时间内还是没有获得锁),则返回 false
recursive_mutex和timed_mutex的结合
锁控制不好时,可能会造成死锁,最常见的比如在锁中间代码返回,或者在锁的范围内抛异常。因此:C++11采用RAII的方式对锁进行了封装,即lock_guard和unique_lock
std::lock_gurad 是 C++11 中定义的模板类
源码:
template <class _Mutex>
class _NODISCARD lock_guard { // class with destructor that unlocks a mutex
public:
using mutex_type = _Mutex;
//引用的方式传入互斥量,在析构函数中加锁
explicit lock_guard(_Mutex& _Mtx) : _MyMutex(_Mtx) { // construct and lock
_MyMutex.lock();
}
lock_guard(_Mutex& _Mtx, adopt_lock_t) : _MyMutex(_Mtx) {} // construct but don't lock
//析构函数中解锁
~lock_guard() noexcept {
_MyMutex.unlock();
}
lock_guard(const lock_guard&) = delete;
lock_guard& operator=(const lock_guard&) = delete;
private:
//接收外面锁的引用
_Mutex& _MyMutex;
};
使用:
int main() {
vector<thread> vthreads;
vthreads.resize(2);
mutex mtx;
int N = 100000;
int x = 0;
for (auto& td : vthreads) {
td = thread([&mtx, &N, &x] {
//在需要加锁的地方声明
//如果想要控制加锁的范围,可以使用局部域
//可以使用局部域控制对象的生命周期,实现对加锁范围的控制
{
lock_guard<mutex> lg(mtx);
for (int i = 0; i < N; ++i)
{
++x;
}
}
///
});
//出了作用域,lg对象销毁,就会调用lock_guard的析构函数,解锁mtx
}
for (auto& td : vthreads) {
td.join();
}
cout << x << endl;
return 0;
}
运用了RAII的思想,用对象的生命周期来控制加锁和解锁,防止lock和unlock的时候发生异常安全问题
但是lock_guard并不支持在对象作用域范围内进行手动的解锁和上锁,所以就有了unique_lock
与lock_gard类似,unique_lock类模板也是采用RAII的方式对锁进行了封装,并且也是以独占所有权的方式管理mutex对象的上锁和解锁操作
与lock_guard不同的是,unique_lock更加的灵活,提供了更多的成员函数:
lock、try_lock、try_lock_for、try_lock_until和unlockowns_lock(返回当前对象是否上了锁)、operator bool()(与owns_lock()的功能相同)、mutex(返回当前unique_lock所管理的互斥量的指针)。可以使用条件变量实现这个小程序

int main() {
mutex mtx;
condition_variable cv;
int n = 100;
bool flag = true;
//打印奇数
thread t1([&] {
int i = 1;
for (; i < n;) {
unique_lock<mutex> lock(mtx);
cv.wait(lock, [&flag] {return flag; });
cout << i << endl;
i += 2;
flag = false;
cv.notify_one();
}
});
//打印偶数
thread t2([&] {
int j = 2;
for (; j < n;) {
unique_lock<mutex> lock(mtx);
cv.wait(lock, [&flag] {return !flag; });
cout << j << endl;
j += 2;
flag = true;
cv.notify_one();
}
});
t1.join();
t2.join();
return 0;
}
可以保证两个线程不会连续打印,保证了线程的同步