目录
常见的几个经典错误——1: 关于内存访问的几个经典的错误
解决多线程流对单一数据的写问题
std::mutex
常见的几个经典错误——1: 关于内存访问的几个经典的错误
为了深入的理解一些潜在的错误,笔者设计了一个这样的样例,各位请看:
#include <chrono>
#include <thread>
std::thread th;
void inc_ref(int &ref) {
std::this_thread::sleep_for(std::chrono::seconds(1));
ref++;
}
void worker() {
int value = 1;
th = std::thread(inc_ref, std::ref(value));
}
int main()
{
worker();
th.join();
}
你发现问题了嘛?
答案是,worker是一个同步函数,我们的value的作用域隶属于worker函数的范围内,现在,worker一旦将th线程派发出去,里面的工作函数的引用ref就会非法。现在,我们对一个非法的变量自增,自然就会爆错
➜ make
[ 50%] Building CXX object CMakeFiles/demo.dir/main.cpp.o
[100%] Linking CXX executable demo
[100%] Built target demo
➜ ./demo
Segmentation fault (core dumped)
扩展:
关于C++的时间库,C++的时间库抽象了几个经典的必要的时间操作,因此,使用这个库来表达我们的时间非常的方便。
std::chrono::duration
: 表示时间间隔,例如 5 秒、10 毫秒等。
std::chrono::time_point
: 表示一个时间点,通常是相对于某个纪元(如 1970 年 1 月 1 日)的时间。
std::chrono::system_clock
: 系统范围的实时时钟,表示当前的日历时间。
std::chrono::steady_clock
: 单调时钟,适用于测量时间间隔,不会受到系统时间调整的影响。
std::chrono::high_resolution_clock
: 高精度时钟,通常是steady_clock
或system_clock
的别名。#include <iostream> #include <chrono> int main() { // 定义一个 5 秒的时间间隔 std::chrono::seconds duration(5); // 获取当前时间点 auto start = std::chrono::steady_clock::now(); // 模拟一些操作 std::this_thread::sleep_for(duration); // 获取结束时间点 auto end = std::chrono::steady_clock::now(); // 计算时间间隔 auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(end - start); std::cout << "Elapsed time: " << elapsed.count() << " ms" << std::endl; return 0; }
std::this_thread::sleep_for
std::this_thread::sleep_for
是 C++11 引入的函数,用于使当前线程休眠指定的时间。它接受一个std::chrono::duration
类型的参数,表示休眠的时间长度。这个this_thread表达的是当前的运行线程的线程句柄,也就是说,调用这个函数表达了运行这个函数的当前线程实体休眠若干秒#include <iostream> #include <thread> #include <chrono> int main() { std::cout << "Sleeping for 2 seconds..." << std::endl; // 使当前线程休眠 2 秒 std::this_thread::sleep_for(std::chrono::seconds(2)); std::cout << "Awake!" << std::endl; return 0; }笔者在这里故意延迟,是为了让worker有时间清理他自己的函数栈,让程序必然报错。但是在项目中,很有可能线程在worker清理完之前就执行完成了,这会导致给项目埋雷,引入非常大的不确定性。
这样的例子还有对堆上的内存,这同样管用:
#include <chrono>
#include <thread>
#include <print>
std::thread th;
void inc_ref(int* ref) {
std::this_thread::sleep_for(std::chrono::seconds(1));
(*ref)++;
std::print("value of the ref points to is {}\n", *ref);
}
void worker() {
int* value = new int;
*value = 1;
th = std::thread(inc_ref, value);
delete value; // free immediately
}
int main()
{
worker();
th.join();
}
程序一样会崩溃,这里,我们没有协调内存块的声明周期。一个很好的办法就是使用智能指针,关于智能指针,笔者再最早期写过两篇博客介绍:
C++智能指针1
C++智能指针2
也就是保持变量的引用到最后一刻,不用就释放!
解决多线程流对单一数据的写问题
可变性!并发编程中一个最大的议题就是讨论对贡献数据的写同步性问题。现在,我们来看一个例子
static int shared_value;
void worker_no_promise() {
for (int i = 0; i < 1000000; i++) {
shared_value++;
}
}
int main() {
auto thread1 = std::thread(worker_no_promise);
auto thread2 = std::thread(worker_no_promise);
thread1.join();
thread2.join();
std::print("value of shared is: {}\n", shared_value);
}
你猜到问题了嘛?
➜ make && ./demo [ 50%] Building CXX object CMakeFiles/demo.dir/main.cpp.o [100%] Linking CXX executable demo [100%] Built target demo value of shared is: 1173798
奇怪?为什么不是200000呢?答案是非原子操作!这是因为一个简单的自增需要——将数据从内存中加载,操作,放回内存中——遵循这三个基本的步骤。所以,可能我们的两个线程从内存中加载了同样的数,自增放回去了也就会是相同的数,两个线程实际上只加了一次。甚至,可能没有增加!(这个你可以自己画图求解,笔者不在这里花费时间了)
如何处理,我们下面引出的是锁这个概念。
std::mutex
mutex就是锁的意思。它的意思很明白——那就是对共享的数据部分进行“上锁”,当一个线程持有了这个锁后,其他的线程想要请求这个锁就必须门外竖着——阻塞等待。
static int shared_value;
std::mutex mtx;
void worker() {
for (int i = 0; i < 1000000; i++) {
mtx.lock();
shared_value++;
mtx.unlock();
}
}
int main() {
auto thread1 = std::thread(worker);
auto thread2 = std::thread(worker);
thread1.join();
thread2.join();
std::print("value of shared is: {}\n", shared_value);
}
现在我们的结果相当好!
➜ make && ./demo [ 50%] Building CXX object CMakeFiles/demo.dir/main.cpp.o [100%] Linking CXX executable demo [100%] Built target demo value of shared is: 2000000
事实上,只要不超过INT64_MAX的值,相加的结果都应该是正确的!