注:本章节内容基于Linux学习多线程之后,默认已经有了进程线程的基础,所以重点讲解库的使用,不会讲解进程线程相关的概念以及基础知识。
1、thread库
thread库底层是对各个系统的线程库的封装,比如pthread(Linux)和thread(Windows),所以C++11的thread库的第一个特点是可以跨平台 ,第二个特点就是面向对象,并且融合了一些C++11的语言特点(比如右值引用、可变模版参数等),用起来会更加方便。
线程创建有以下四种构造函数:

最常用的是第二个,它支持传一个可调用对象和参数即可。相比于pthread_create而言,这里不再局限于只传函数指针,并且参数传递也会更加方便。
cpp
#include <iostream>
#include <thread>
using namespace std;
void Print(int n, int i)
{
for (; i < n; i++)
{
cout << this_thread::get_id() << ":" << i << endl;
}
cout << endl;
}
int main()
{
thread t1(Print, 10, 0);
thread t2(Print, 20, 10);
cout << t1.get_id() << endl;
cout << t2.get_id() << endl;
t1.join();
t2.join();
return 0;
}
join是主线程结束前,还需要阻塞等待从线程,否则主线程结束了,也就意味着进程结束,从线程可能还在运行时就被终止了。
class thread::id是一个thread的内部类表示线程id。因为各个平台的线程id表示类型不同,所以只能用一个类进行封装。线程对象可以用get_id获取线程id,在执行体内可以通过this_thread::get_id()来获取。
2、this_thread
this_thread是一个命名空间,主要封装了线程相关的4个全局接口函数:
get_id:当前执行线程的线程id;
yield:主动让出当前线程的执行权,让其他线程先执行;
sleep_for:阻塞当前线程执行,至少经过指定的sleep_duration;
sleep_until:阻塞当前线程执行,直至抵达指定的sleep_time。
cpp
#include <iostream>
#include <thread>
#include <chrono>
using namespace std;
int main()
{
for (int i = 10; i >= 0; i--)
{
std::cout << i << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(1));
}
return 0;
}
3、mutex
mutex是封装互斥锁的类,用于保护临界区的共享资源。
cpp
#include <iostream>
#include <thread>
#include <mutex>
using namespace std;
void Print(int n, int& rx, mutex& rmtx)
{
rmtx.lock();
for (int i = 0; i < n; i++)
{
rx++;
}
rmtx.unlock();
}
int main()
{
int x = 0;
mutex mtx;
thread t1(Print, 1000, ref(x), ref(mtx));
thread t2(Print, 2000, ref(x), ref(mtx));
t1.join();
t2.join();
cout << x << endl;
return 0;
}
这里要注意,传参时必须要用ref()传参,线程中拿到的才是x和mtx的引用!
thread 内部机制:构造 thread 对象时,所有传入的实参都会被拷贝到线程内部的存储空间;启动线程时,再把内部拷贝出来的值转发给目标函数 Print;默认转发是值语义,就算你写&x,它也会复制一份 x 的副本,并把副本传给 Print。
而std::ref(x) 会生成一个 reference_wrapper<T> 包装对象。包装器本身可以被自由拷贝;内部存储原始变量的地址;thread 在转发参数时,如果遇到 reference_wrapper,会自动解包,转为左值引用传给函数。
所以像
mutex这种禁止拷贝的类型,不传 ref 必然编译失败。
所以为了尽量避免上面这种"坑",我们还可以用下面这种写法(lambda表达式):
cpp
#include <iostream>
#include <thread>
#include <mutex>
using namespace std;
int main()
{
int x = 0;
mutex mtx;
auto Print = [&x, &mtx](int n) {
mtx.lock();
for (int i = 0; i < n; i++)
{
x++;
}
mtx.unlock();
};
thread t1(Print, 1000);
thread t2(Print, 2000);
t1.join();
t2.join();
cout << x << endl;
return 0;
}
思考一下:如果我把锁放在循环里面,程序运行结果仍然正确吗?性能会怎样?
cpp
void Print(int n, int& rx, mutex& rmtx)
{
for (int i = 0; i < n; i++)
{
rmtx.lock();
rx++;
rmtx.unlock();
}
}
先说结论:两种写法结果都正确,但是锁的粒度不同,开销差距明显。
原始代码是粗粒度锁,线程一旦拿到锁,一次性执行全部 n 次自增,全程持有锁。循环内部上锁就变成了细粒度锁,每执行一次 rx++,就要 lock 并 unlock 一次。这就会涉及操作系统内核调用(用户态 ↔ 内核态切换),内部包含原子指令、缓存同步、线程阻塞唤醒机制,频繁争抢锁会造成 CPU 缓存颠簸。大量重复的加解锁带来巨大额外开销,CPU 花费大量时间处理同步,而不是业务计算rx++。所以思考版本代码运行更慢!
4、lock_guard
lock_guard是C++11提供的支持RAII方式管理互斥锁资源的类,这样可以有效防止因为异常等原因导致的死锁问题。
cpp
#include <iostream>
#include <thread>
#include <mutex>
using namespace std;
int main()
{
int x = 0;
mutex mtx;
auto Print = [&x, &mtx](int n) {
lock_guard<mutex> lock(mtx);
for (int i = 0; i < n; i++)
{
x++;
}
};
thread t1(Print, 1000);
thread t2(Print, 2000);
t1.join();
t2.join();
cout << x << endl;
return 0;
}
5、condition_variable
condition_variable需要配合互斥锁进行使用,主要提供wait和notify系统接口。
wait需要传递一个unique_lock<mutex>类型的互斥锁,wait会阻塞当前线程,直到被notify。在进入阻塞的一瞬间,会解开互斥锁,便于其他线程获取锁并访问条件变量。
notify_one会唤醒当前条件变量上等待的其中一个线程,使用时也需要互斥锁保护,如果没有阻塞等待的线程,那么该系统接口什么事也不做。
unique_lock也是C++11提供的支持RAII方式的互斥锁资源的类,相比于lock_guard,它的功能支持更复杂。
下面演示一个经典问题:两个线程交替打印奇数和偶数
cpp
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
using namespace std;
int main()
{
std::mutex mtx;
condition_variable c;
int n = 100;
bool flag = true;
thread t1([&]() {
int i = 0;
while (i < n)
{
unique_lock<mutex> lock(mtx);
while (!flag)
{
c.wait(lock);
}
cout << this_thread::get_id() << ":" << i << endl;
flag = false;
i += 2;
c.notify_one();
}
});
thread t2([&]() {
int j = 1;
while (j < n)
{
unique_lock<mutex> lock(mtx);
while (flag)
{
c.wait(lock);
}
cout << this_thread::get_id() << ":" << j << endl;
flag = true;
j += 2;
c.notify_one();
}
});
t1.join();
t2.join();
return 0;
}