目录
一、基本概念
二、线程创建以及 join() detach() 的区别
三、mutex\lock_guard\unique_lock\...
1. mutex
2. recursive_mutex、time_mutex
3. lock_guard
4. unique_lock
四、条件变量 condition_variable
1. wait 类函数
2. notify 类函数
3. 虚假唤醒问题
五、死锁
六、async 和 future
网上说的不清不楚的,自己写一篇。
并发,就是几个任务同时进行。单核 CPU 的“并发”是假的,只是任务快速切换给人感觉像并行,实际是串行。多核 CPU 才会真正并行。当然,任务数 > CPU 核数还是会有切换的情况,比如 windows 上的任务(线程)都是上千个。
一个可执行程序执行起来就是一个进程,一个进程只能有一个主线程。线程也不是越多越好,每个线程需要独立的堆栈空间,线程切换也要保存很多中间状态。
如何实现并发?多进程或多线程。但优先使用多线程,因为一个进程中所有线程共享地址空间,还有诸如全局变量、指针、引用都可以在线程间传递,开销远小于多进程。
线程创建直接使用 thread 库:
thread thread1(fun1);
这样会创建名字叫 thread1 的线程,调用函数 fun1(),注意,立即执行。 不像 java 里还要 start() 哦!
join() 和 detach() 的区别见下面代码注释,不赘述。
#include
#include
using namespace std;
void fun1()
{
cout << "开始执行fun1()" << endl;
}
void fun2(int x)
{
cout << "开始执行fun2(), " << "x=" << x << endl;
}
int main() {
cout << "主线程已经开始" << endl;
// 创建线程,会立即执行
thread thread1(fun1);
thread thread2(fun2, 666);
thread1.join(); // join() 代表汇合,阻塞的作用,主线程在这里等待子线程执行完毕
thread2.join();
// 如果使用detach(),则主线程与子线程分离,主线程不会等待子线程
// 使用detach()主线程执行完了return 0, 程序终止子线程也没了,因此join()阻塞的方式更常用
// thread1.detach();
cout << "主线程马上结束" << endl;
return 0;
}
上述代码执行结果:
主线程已经开始
开始执行fun1()
开始执行fun2(), x=666
主线程马上结束
thread1 是否一定比 thread2 先执行完毕,不一定。thread1 是比 thread2 先启动,但两者是并行执行的,理解这个就理解了多线程核心。
detach() 的方式可能会出现局部变量失效导致线程被内存非法访问的问题,因为主线程执行完了退出,子线程还没执行完,这时一些传入参数的地址在子线程中访问就出问题了。
还有一个 joinable() 函数也有一定作用,可以判断你是否调用过 join() 或者 detach()。
一个线程一个id,查看方式
std::this_thread::get_id()
多线程访问同一全局变量时,比如同时读写,可能出现时序问题,因此多线程一般都要加锁。此小节包含互斥量 mutex 普通上锁解锁,以及常用的 lock_guard、unique_lock 等用法举例。
std::mutex 是 C++11 中最基本的互斥量。
#include // std::cout
#include // std::thread
#include // std::mutex, std::lock_guard
using namespace std;
volatile int cnt(0); // non-atomic counter
mutex mtx;
void ten_thousand_increase() {
for (int i = 0; i < 10000; ++i) {
mtx.lock();
++cnt;
mtx.unlock();
}
}
int main(int argc, const char *argv[]) {
thread threads[10];
for (auto &th: threads)
th = thread(ten_thousand_increase);
for (auto &th: threads)
th.join();
cout << "cnt=" << cnt << endl;
return 0;
}
程序运行结果:
cnt=100000
该程序创建了10个 thread,每个都会运行 ten_thousand_increase() 函数,如果不加锁,那最后 cnt 可能不是10000,因为可能发现同时写的情况,比如现在 cnt 是100,thread1 和 thread2 同时执行 cnt++,那么 cnt 变成了 101 而不是 102 。加锁就可以保证只能一个线程在写,其他线程在 mtx.lock() 位置等待。
不加锁运行,cnt 结果不一定:
cnt=96925
如果换成 try_lock():
#include // std::cout
#include // std::thread
#include // std::mutex, std::lock_guard
using namespace std;
volatile int cnt(0); // non-atomic counter
mutex mtx;
void ten_thousand_increase() {
for (int i = 0; i < 10000; ++i) {
if (mtx.try_lock()) { // 仅在其他线程未加锁时执行
++cnt;
mtx.unlock();
}
}
}
int main(int argc, const char *argv[]) {
thread threads[10];
for (auto &th: threads)
th = thread(ten_thousand_increase);
for (auto &th: threads)
th.join();
cout << "cnt=" << cnt << endl;
return 0;
}
运行结果:
cnt=16347
可见,cnt 远小于 10w。
recursive_mutex 允许同一线程对互斥量递归上锁(多次上锁),来获取多层控制权。lock() 和 unlock() 次数要相同。
time_mutex 一段时间 try_lock_for() 或截止时间 try_lock_until() 加锁,也就是多了时间控制。
lock_guard 在实际开发中非常常用。在 lock_guard 类模板的构造函数里,调用了 mutex 的 lock() 成员函数,而在析构函数里,调用了mutex 的 unlock() 成员函数,仅此而已。因为 lock_guard 又是局部变量,出了作用域立马析构,所以经常被使用防止开发者忘记写 unlock()。
#include // std::cout
#include // std::thread
#include // std::mutex, std::lock_guard
using namespace std;
volatile int cnt(0); // non-atomic counter
mutex mtx;
void ten_thousand_increase() {
for (int i = 0; i < 10000; ++i) {
lock_guard lck(mtx);
++cnt;
}
}
int main(int argc, const char *argv[]) {
thread threads[10];
for (auto &th: threads)
th = thread(ten_thousand_increase);
for (auto &th: threads)
th.join();
cout << "cnt=" << cnt << endl;
return 0;
}
运行结果:
cnt=100000
相比 lock_guard,unique_lock 更灵活,但效率差一点,内存占用多一点。
首先,它可完全替代 lock_guard。
第二,它可携带第二个参数 std::unique_lock
第三,它有精细控制的成员函数:
比如说,unique_lock 可以提前 unlock,不是必须等到析构。这也是和 lock_guard 的一个重要区别。
条件变量不是锁,往往跟互斥量同时出现。条件变量是利用线程间共享的全局变量进行同步的一种机制。
condition_variable 对象的某个 wait 类函数被调用时,当前线程阻塞,进入 blocking thread 队列。直到其他线程在 同一个 condition_variable 对象上调用 notify 类函数去唤醒它。
#include // std::thread
#include // std::mutex, std::lock_guard
#include // std::conditional_variable
#include
using namespace std;
mutex mtx; // 全局互斥量
condition_variable cv; // 全局条件变量
bool ready = false; // 全局标志位
void printId(int id) {
unique_lock lck(mtx);
printf("enter thread: %d\n", id);
// 暂停2秒,方便看规律
this_thread::sleep_for(chrono::milliseconds(2000));
while (!ready) {
cv.wait(lck);
}
// 线程被唤醒
printf("done thread: %d\n", id);
}
void go() {
unique_lock lck(mtx);
printf("go()\n");
ready = true; // 改变全局标志位
cv.notify_all(); // 唤醒所有线程
}
int main(int argc, const char *argv[]) {
thread threads[10];
for (int i = 0; i < 10; ++i)
threads[i] = thread(printId, i);
printf("create done.\n");
go();
for (auto &th: threads)
th.join();
printf("process done.\n");
return 0;
}
运行结果:
enter thread: 0
create done.
enter thread: 3
enter thread: 4
enter thread: 2
enter thread: 5
enter thread: 6
enter thread: 1
enter thread: 7
enter thread: 8
enter thread: 9
go()
done thread: 9
done thread: 8
done thread: 7
done thread: 4
done thread: 2
done thread: 5
done thread: 3
done thread: 0
done thread: 1
done thread: 6
process done.
从运行结果可以看出,10个线程都阻塞在 wait 处,当调用 notify_all() 时,所有线程被唤醒。
普通 wait() 函数第一个参数为 unique_lock
while (!pred()) wait(lck);
同样它有 wait_for 和 wait_until。
ontify_one 随机唤醒一个等待线程。
ontify_all 唤醒所有等待线程。
如果没有等待线程,什么也不做。
网上一堆误人子弟的说法,实际上 wait 设计的目的是防止一直 while 判断条件变量效率太低,并不会保证一定是 notify 才会唤醒,因此会有虚假唤醒的可能。解决方式就是上面例子中那样:
while (!ready) {
cv.wait(lck);
}
这样即使虚假唤醒,ready 状态没变还是会重新 wait。
举个例子:
#include
#include
using namespace std;
mutex mtx;
void B() {
mtx.lock();
printf("process B");
mtx.unlock();
}
void A() {
mtx.lock();
printf("process A");
B(); //这里调用B方法
mtx.unlock();
}
int main(int argc, const char *argv[]) {
thread threadA(A);
thread threadB(B);
threadA.join();
threadB.join();
printf("process done.\n");
return 0;
}
执行结果:
process A
A 函数里调用 B 函数,但是 mtx 在A函数里加了锁,还没解锁 B 卡在 mtx.lock()。B 等待着 A 解锁,A 也等待 B 解锁才会往下执行。
另外一种就是很常见的忘记 unlock。这种情况当然哪个线程都会卡在 lock 的地方。
死锁的排查方式后面有时间专门写。
上面的例子线程入口函数返回值都是 void,也就是线程不会返回结果。那如果入口函数返回值是 int,我们需要线程返回结果该怎么做?可能你会想到不必返回,用一个全局变量。那有没有其他更好的方式?那就要用到函数模板 async 和类模板 future。
async 创建并执行异步任务,future 封装返回值。举例:
#include
#include
#include
using namespace std;
int fun(int input) {
cout << "fun thread id=" << this_thread::get_id() << endl;
int output = input + 1;
return output;
}
int main(int argc, const char *argv[]) {
cout << "main thread id=" << this_thread::get_id() << endl;
int input = 666;
future result = async(fun, input);
int output = result.get();
cout << "process done. result=" << output << endl;
return 0;
}
执行结果:
main thread id=1
fun thread id=2
process done. result=667
更复杂的用法不展开叙述,只要你有个概念这个 future 是干啥的就可以。