实战篇 | 21 C++ 多线程与资源竞争问题
小知识:C++指针与引用的区别
对C++当中的指针和引用,用时还是分不清,C++里面为什么要有这两种概念呢,分为能解决什么样的问题呢,类比来看呢,Java或C#这样的高级编程语言里面,为什么没有出现指针这个东西呢,只有引用这个概念?
我们来看一个例子
Java当中传递是引用,指向的都是同一个对象,所以,o.getName()得到的是Samuel。
在C++中,因为参数传递的不是引用,而是拷贝,所以o.getName()的结果是空。
如果我们希望也像Java当中,拿到我们修改的东西,我们需要在参数前面加一个取地址符号&,变成引用,这样本质上指向的是原本对象,而不是拷贝出来的对象。这种情况下我们用到了引用。
那引用和指针的区别又在哪里呢?
前面我们把引用当作内存当中的首地址,它是一个别名。
但是指针,我们要把Obj 看成是一个完整的一个整体。我们要把指针看成一个类型。所以这里,这个o是Obj 类型,它是指针类型。我们在前面讲C++的时候讲到过一个比较重要的概念,就是那几种类型转换:dynamic_cast(RTTI),static_cast,强制转换(reinterpret_cast),cost_cast.
我们现在看reinterpret_cast,它可以把C++任何一种类型转换成我想要的类型。不管转换出来的东西有没有用,反正都能转。所以大家要注意,指针本质上是一个长度为4字节的内存区域。它里面存的是一串数字,如:0x0000FDE1,那这串数字又是什么,是不是地址,存储的内存地址。我们通常讲的int32_t,也是4个字节,我们现在用reinterpret_cast把这个.o转换成int32_t.我们发现它本质上就是一串数字。那这串数字是什么东西呢,它指向一块特定内存区域,那块内存区域的首地址,它指向的是内存当中的另外一个地方。
所以,大家可以看到引用和指针的区别在哪里?
int i = 100;
引用的写法:&i,拿到的是100的地址;
而我们想把这个地址存起来,我们用:
int* iPtr = &i;
这里我们可以看到,引用是一个值,而指针是一个变量
。
我们为什么要在C++当中去区分指针和引用?
关键,归根结底我们希望灵活。我们希望通过指针和引用来实现不同的功能或者场景。 大家考虑一下我们刚刚前面的例子,我在参数前面写了一个&,这个场景是非常管用的。我只是用别名 .o,我没有创建指针,我直接去操纵别名的。那如果我创建指针会有什么用呢。
假如,机器学习中,我们需要导入大量图片去做训练,那这些图片放在内存里面我怎么取管理它呢,每一张图片需要一块内存去存储它,前面文章谈卷积神经网络的时候,我们是不是把位图比作一个矩阵,通过数字来定义的这样一个矩阵,每个图片image是一个矩阵,所以会有无数个矩阵,我们放在C++当中,大家考虑一下C++当中是不是自己管理内存,它很*,需要开发人员去管理它的内存。那我们是不是要考虑用一种方式去管理它,这是第一点,那第二点,在前面的文章中提到一个概念stack(栈)和heap(堆)。那对于一个程序的进程空间来说,stack是非常有限的,非常小的,但是heap堆会非常的大,前者是连续的,后者是不连续的。我们这么多图片很显然不能存到栈上,所以我们要考虑把这么多图片的内存信息存到heap上,那如果我们要存在堆上,我们是不是要有一种办法去管理内存。现在我们谈论的是管理内存的问题。如果我不做内存管理这件事情会怎么样,内存泄漏。因为你永远找不到这个内存到底跑哪里去了。我们使用标准库的当中的向量去存储指针pointer/address,我们希望大家尽量使用标准库当中的向量std::vector去当数组,尽量不要使用裸的数组。裸的数组容易越界了,为什么,它不是向量,比如你定义好了int[10][10],它就定死了,我不能在里面存10*11个数据的。
所以,为了程序安全,我们尽可能的去使用vector,使用向量去存储我们的数据,使用vector它是一个模板,我们要指定一个类型。
我们这边给大家讲一个技巧,对C++使用比较多的朋友对这个很熟,但是你使用C++不是特别熟,比较少的话,不一定会这样去用。
std::vector<void *> arr;
我这里写了一个void,我们经常在写Java或C#程序的时候,返回值类型是void对吗,void就是空的意思,没有的意思。
在C++当中,void 可以做很多事情,它是一个通用“符号”,这个符号不是C++当中的symbol,它是象征意义。
为什么写void * ?
我只是希望这个数组里面存储一些地址。存储一些指针,我不需要知道他的类型。
我的图片存在heap堆中,堆大,内存不连续,中间可能有碎片,stack当中我存了一个arr,它里面是数组,是一个在有限内存中无限扩充的向量。这里面的指针能够帮我找到对应的内存区域的image,我不需要知道它是什么类型。我们前面提到C++是一个强类型语言,我们一定得知道他是什么类型。这里有一个技巧,我不需要知道他是什么类型,我只需要知道第一张图片的内存在什么位置,第二张又在什么位置,第三张又在什么位置就足够了。所以我不需要知道类型,我用void *
去存就OK了。
咱们把C++当中关键的内存管理、引用、指针的概念又梳理了一遍。然后讲了一个实用技巧,std::vector
Thread
创建线程
新建main.cpp,编写创建线程代码
#include <iostream>
#include <thread>
#include <functional> // C++ lambdba expression
main() {
std::thread t1([&]() -> void {
std::cout << "Thread 1 is running" << std::endl;
});
t1.join();
return 0;
}
以上我们使用C++当中的lambda
表达式来创建多线程。
[&]:在这里,&既不表示取地址符,也不表示逻辑关系与,在lamdba表达式当中的意思是继承上下文,如下,继承了上文的a = 100,所以a++后,假设等待线程t1执行结束,输出a为101。
[=]:如果改成等号,a输出100,lambda表示式里面等号表示把外面的上下文复制进来。相当于lambda表达式里面的a是个拷贝,在里面a++对外面不受影响。
编译好后,直接运行即可,输出如下:
Thread 1 is running
Makefile 前面谈过,这里不多谈。注意这里需要引入pthread
,它是Linux或Unix当中线程的标准库,如果我们在程序当中使用了pthread。
pthread是Linux当中的,不是C++标准里面的东西,如果我们使用了pthread ,我们需要引用pthread。很多老的程序里面会使用pthread,为了确保编译通过,我们通常会在Makefile里面习惯性的加上一个链接 -pthread。
模拟线程阻塞
模拟线程阻塞,线程一直停在这里。
按Control + C
退出阻塞线程。
C++ 14 下,如何实现多线程
模拟无阻塞线程,线程t1执行期间执行线程t2,不需要等待线程t1执行完毕。
资源竞争问题
如上,我创建类两个线程t1,t2.我不知道谁先执行结束,我如何保证number是100,而不是0呢,这里我们引入C++ 11当中的技术叫做条件量condition_variable
,当然我们还需要其他工具mutex
,来保证资源的互斥性。所以我们引入这两个库文件。
这个条件量很强大,以前没有的,后来C++11新引入的,以后也会经常用到。
这个condition_variable(简称cv)是基于lock,所以,在main.cpp
文件修改我们的程序,添加锁和条件量。
#include <iostream>
#include <thread>
#include <functional> // C++ lambda expression
#include <chrono>
#include <condition_variable> //cv
#include <mutex> //lock
// usage of cv
// cv depends on [lock, lock] depends on mutex.
std::mutex mtx;
std::condition_variable cv;
bool t1Finished = false;
int32_t number = 0;
int main()
{
// 1. create thread
// 1.1 lambda expression
// 1.2 sleep and chrono
std::thread t1([&]() -> void { // lambda expression, C++11 introduced in.
std::unique_lock<std::mutex> lock(mtx);
number = 100; // [write]
std::cout << "thread 1 is running" << std::endl;
// notify litener of cv
cv.notify_one();
// update t1Finished
t1Finished = true;
});
//2. thread java, join() & detach()
//t1.join(); // blocking process?
t1.detach(); // non-blocking, let the thread manage life of it.
// do something else
std::thread t2([&]() -> void {
// create a lock using mtx
std::unique_lock<std::mutex> lock(mtx);
if (!t1Finished)
// wait using cv based on lock
cv.wait(lock); // blocking
std::cout << "thread 2 is running, number = " << number /*read*/ << std::endl;
});
t2.detach();
// thread synchronization
// let main thread sleep for a very long time.
std::this_thread::sleep_for(std::chrono::milliseconds(100000));
return 0;
}
编译运行,不管我运行多少次,线程2都是100.
我们通过condition_variable这项新技术在线程执行体内部由内而外的去控制整个程序的执行流程。
大家看到,在C++2.0时代(现代C++),在程序的执行流程控制上,包括如何充分利用硬件性能,CPU多核的性能之上,现在C++提供了力度极细的控制源域方法,所以咱们今天基本全部覆盖了高线程编程线程这块的所有内容,大家可以看到我们通过这几项技术,其实就这么多,去灵活的组合的时候,我们发现我们可以随心所欲的控制我们的程序。