第一节:并发基本概念及实现,进程,线程得基本概念
并发,线程,进程的概念
1. 并发 两个或者更多的任务(独立的活动)同时发生(进行)一个程序同时执行多个任务
以往计算机,单核CPU某一时刻只能执行一个任务:由操作系统调度,每秒钟进行多次所谓的的“任务切换”,这种切换也叫做上下文切换,并且是由时间开销,比如操作系统要保存切换时的各种状态,执行进度等信息
2. 进程,就是一个运行起来的可执行文件了。
3. 线程 每个进程(执行起来的可执行程序),都会由一个主线程,主程序是唯一的,也就是一个进程只有一个主线程
主线程与进程是相依的关系
线程可以理解为代码的执行通路,每创建一个新线程,我就可以在同一个时刻,多干一件事
//线程并不是越多越好,每个线程都需要一个堆栈空间(1M),线程之间的切换要保存很多中间状态,
//切换会耗费本该属于程序运行的时间
并发变成有两种模型
多进程 每个进程只有一个线程,进程间通过文件,管道,消息队列来进行通信。
多线程 一个进程有两个及以上的线程通过 shared memory 来进行通信。
结论:多线程程序可以同时干多个事,所以运行效率高。但是到底有多高,并不是一个很容易评估和量化的东西。比如吃饭和唱歌,共享同一块资源,所以不一定能效率高。
学习心得:
1.c++线程开发需要一定的学习时间
2.c++线程会涉及很多新概念,比如互斥,原子操作等
相对于进程,线程启动速度很快,线程通常是更轻量级。一个进程中的所有线程共享地址空间 ,比如全局变量,指针,引用都可以在线程之间传递,所以使用多线程的开销小于多进程。
但是共享内存也带来问题,数据一致性问题:线程A
多线程比多进程难管理,多线程不能再分布式系统中运行,
第二节:线程启动,结束,创建线程多种方法,join,detach
1.join():加入/汇合,说白了就是阻塞,阻塞主线程,让主线程等待子线程执行完毕,然后子线程和主线程汇合
2.detach
为什么引入detach?我们创建了很多子线程,让主线程的那么多子线程,这种方法不太好,所以引入了detach.
如果主线程执行完毕了,这个子线程相当于被c++运行库接管,当子线程执行完成后,由运行时库清晰该线程相关资源
做法1:使用函数作为线程入口
void function_1() {
std::cout << "hello world " << std::endl;
}
int main(){
std::thread myobj(function_1);//创建了线程,一旦创建,线程就开始执行 myobj.join();//主线程阻塞到这里等待function_1执行完毕,当子线程执行完毕,主线程就继续执行}
做法2:使用类来进行创建: 一定要重载()操作符,使得他变成一个可调用对象,为什么呢?
class TA {
public:
void operator()() {
cout << "hello world" << endl;
}
};
int main() {
TA my;
std::thread myobj(my);
myobj.join();
}
现在我们考虑一个问题,如果这里使用的是detach会不会有问题。
int main() {
TA my;
std::thread myobj(my);
myobj.detach();
}
大家可能由一个疑问,一旦调用detach,那主线程执行结束了,这里的ta对象还在吗?答:TA对象不在了.
但是其实这个对象实际被赋值到了线程中去了,所以执行完主线程后,ta会被销毁,但是被复制的对象(也就是进行了一个拷贝构造函数)仍然存在,我们如何来进行验证呢?
class TA {
public:
TA() {
cout << "TA()构造函数被执行" << endl;
}
//为了证明创建线程得时候对象是被复制进去得,因为是被复制进去,所以还需要调用拷贝构造函数 TA(const TA& ta) //拷贝构造函数怎么写?const {
cout << "TA()拷贝函数被执行" << endl;
}
~TA() {
cout << "TA()析构被执行" << endl;
}
void operator()() {
cout << "hello world" << endl;
}
};
做法3:使用类来进行创建,并进行传参
class TA {
public:
int& m_i;
TA(int &i):m_i(i) {
cout << "TA()构造函数被执行" << endl;
}
//为了证明创建线程得时候对象是被复制进去得,因为是被复制进去,所以还需要调用拷贝构造函数 TA(const TA& ta) :m_i(ta.m_i)//拷贝构造函数怎么写?const {
cout << "TA()拷贝函数被执行" << endl;
}
~TA() {
cout << "TA()析构被执行" << endl;
}
void operator()() {
cout << "hello world" << endl;
}
};
int main(){
int myi = 6;
TA my(myi);
std::thread myobj(my); //创建了线程,线程执行入口时myprint (2)myprint线程开始运行 myobj.join();//主线程阻塞到这里等待myprint()执行完毕,当子线程执行完毕,主线程就继续执行}
通过运行的结果我们可以看到:
std::thread myobj(my); //这一句实际上执行了一次拷贝构造了
但是如果把
thread myobj(std::ref(my)); //这一句实际上执行了一次拷贝构造了
就能够减少一次拷贝构造了
注意:所以只要你的TA类对象里面没有引用,没有指针,那么就不会产生问题!
第三节:线程传参详解,detach()大坑,成员函数做线程函数
传递临时对象作为线程参数
1.1 要避免得陷阱
1.2 要避免的陷阱
1.3 总结
第四节:创建多个线程,数据共享问题分析,案例代码
1.创建多个线程和等待多个线程
//把thread对象放在容器里管理,看起来像个thread数组,对我们一次创建大量的线程管理 vector mythreads;
for (int i = 0; i < 10 ; i++)
{
mythreads.push_back(thread(myprint, i));//创建10个线程,同时这10个线程已经开始执行 }
for (auto iter = mythreads.begin(); iter != mythreads.end(); ++iter)
{
iter->join();//等待10个线程都返回 }
cout << "I love China" << endl;//最后执行这句,整个代码推出
注意:先创建的线程,可能比较慢执行,这个取决于操作系统内部对这些线程的调度机制
2.数据共享问题
关于数据有几种类型:
1.只读数据是安全稳定的,不需要做特殊处理
2.有读有写, 2个线程写,8个线程读,如果代码处理的不好,程序肯定崩溃
数据共享的一个实际例子:假设在高铁售票处,北京-深圳 火车 T123 , 10个售票窗口, 1,2 同时都要订99座
解决方案:最简单的不崩溃处理,读的时候不能写,写得时候不能读.
3.共享数据的保护案例代码
假设我们设计一个网络游戏服务器:两个线程
1.一个线程记录玩家命令(用一个数字代表玩家发来的命令),并把命令数据写到一个队列中。 2.另外一个线程,从队列中取出玩家发送来的命令,解析,然后然后执行玩家需要的动作。
我们使用成员函数作为线程函数的方法来构造线程
#include #include #include#include#include#include
using namespace std;
class A {
public:
//把收到的消息(玩家命令)送到一个队列中 void inMsgRecvQueue() {
for (int i = 0; i < 100000; i++)
{
cout << "inMsgRecvQueue()执行,插入一个元素" << endl;
msgRecvQueue.push_back(i);//假设这个数字就是我收到的命令,我直接弄到消息队列里面去 }
return;
}
//把数据从消息队列中取出线程: void outMsgRecvQueue() {
int command = 0;
for (int i = 0; i < 100000; i++)
{
if (!msgRecvQueue.empty())
{
int command = msgRecvQueue.front();//返回第一个元素, msgRecvQueue.pop_front();//移除第一个元素,但不返回 cout << "OutMsgRecvQueue()执行,插入一个元素" << endl;
}
}
cout << "end" << endl;
}
private:
//这个就是共享数据 std::list msgRecvQueue;//容器,专门用于代表玩家给我们发送来的命令}
int main() {
A myobja;
std::thread myOutMsg(&A::outMsgRecvQueue, &myobja);//第二个参数是 引用,才能保证线程用的是同一个对象这样能够避免多次复制 std::thread myInMsg(&A::inMsgRecvQueue, &myobja);
myOutMsg.join();
myInMsg.join();
}
注意:这种写法百分之百会让程序崩溃,因为这两个线程一个写一个读,所以必须有一种保护共享数据的机制,
操作时,用代码把共享数据锁住,其他想操作共享数据的线程必须等待。因此需要引入互斥量的概念来。
第五节:互斥量概念,用法,死锁演示以及解决详解
(1)互斥量(mutex)的基本概念
互斥量就是一个类对象,理解为一把锁,多个线程尝试用lock()成员函数加锁,只有一个线程能够锁定成功,成功的标志是锁返回了,如果没锁成功,那么流程就卡在那里。
做法1:
注意的一点是:互斥量使用要小心,保护的数据不多也不少。
#include #include #include#include#include#include
using namespace std;
class A {
public:
//把收到的消息(玩家命令)送到一个队列中 void inMsgRecvQueue() {
for (int i = 0; i < 100000; i++)
{
my_mutex.lock();
cout << "inMsgRecvQueue()执行,插入一个元素" << endl;
msgRecvQueue.push_back(i);//假设这个数字就是我收到的命令,我直接弄到消息队列里面去 my_mutex.unlock();
}
return;
}
bool outMgLULProc(int& command) {
my_mutex.lock();
if (!msgRecvQueue.empty())
{
int command = msgRecvQueue.front();//返回第一个元素, msgRecvQueue.pop_front();//移除第一个元素,但不返回 //这里就考虑处理数据 my_mutex.unlock();
return true;
}
//一定要记得两个地方都要unlock my_mutex.unlock();
return false;
}
//把数据从消息队列中取出线程: void outMsgRecvQueue() {
int command = 0;
for (int i = 0; i < 100000; i++)
{
bool result = outMgLULProc(command);
if (result == true)
{
cout << "outMsgRecvQueue()执行,取出一个元素";
}
}
cout << "end" << endl;
}
private:
//这个就是共享数据 std::list msgRecvQueue;//容器,专门用于代表玩家给我们发送来的命令 std::mutex my_mutex;//创建一个my_mutex的锁}
int main() {
A myobja;
std::thread myOutMsg(&A::outMsgRecvQueue, &myobja);//第二个参数是 引用,才能保证线程用的是同一个对象这样能够避免多次复制 std::thread myInMsg(&A::inMsgRecvQueue, &myobja);
myOutMsg.join();
myInMsg.join();
}
做法2:
为了防止大家忘记unlock(),引入一个叫做std::lock_guar的类模板,你忘记unlock不要紧,我替你unlock() std::lock_guard类模板:直接取代lock()和unlock(),
#include #include #include#include#include#include
using namespace std;
class A {
public:
//把收到的消息(玩家命令)送到一个队列中 void inMsgRecvQueue() {
for (int i = 0; i < 100000; i++)
{
std::lock_guard<:mutex> sbguard(my_mutex);
cout << "inMsgRecvQueue()执行,插入一个元素" << endl;
msgRecvQueue.push_back(i);//假设这个数字就是我收到的命令,我直接弄到消息队列里面去
}
return;
}
bool outMgLULProc(int& command) {
std::lock_guard<:mutex> sbguard(my_mutex);
//lock_guard构造函数里面执行了mutex::lock() //lock_guard析构函数里面执行了mutex::unlock() if (!msgRecvQueue.empty())
{
int command = msgRecvQueue.front();//返回第一个元素, msgRecvQueue.pop_front();//移除第一个元素,但不返回 //这里就考虑处理数据
return true;
}
//一定要记得两个地方都要unlock
return false;
}
//把数据从消息队列中取出线程: void outMsgRecvQueue() {
int command = 0;
for (int i = 0; i < 100000; i++)
{
bool result = outMgLULProc(command);
if (result == true)
{
cout << "outMsgRecvQueue()执行,取出一个元素";
}
}
cout << "end" << endl;
}
private:
//这个就是共享数据 std::list msgRecvQueue;//容器,专门用于代表玩家给我们发送来的命令 std::mutex my_mutex;//创建一个my_mutex的锁}
int main() {
A myobja;
std::thread myOutMsg(&A::outMsgRecvQueue, &myobja);//第二个参数是 引用,才能保证线程用的是同一个对象这样能够避免多次复制 std::thread myInMsg(&A::inMsgRecvQueue, &myobja);
myOutMsg.join();
myInMsg.join();
}
(2)死锁
一个生活中的例子:
张三:站在北京,等李四
李四:站在深圳,等张三
在c++中,
如果说我又两把锁(死锁这个问题,是由至少有两个互斥量才能产生)金锁,银锁
1.线程A执行的时候,这个线程先锁金锁,把金锁lock()成功,然后他去lock()银锁。。。
假如这个时候出现了上下文切换
2.线程B执行了,这个线程先锁银锁,因为银锁线程A还没执行,所以B银锁会成功,然后线程B要去Lock金锁。
此时死锁就产生了.
class A {
public:
//把收到的消息(玩家命令)送到一个队列中
void inMsgRecvQueue() {
for (int i = 0; i < 100000; i++)
{
cout << "inMsgRecvQueue()执行,插入一个元素" << endl;
my_mutex1.lock();//实际工程中可能是这两个锁头不一定挨着
//.....................
my_mutex2.lock();
msgRecvQueue.push_back(i);//假设这个数字就是我收到的命令,我直接弄到消息队列里面去
my_mutex2.unlock();
my_mutex1.unlock();
}
return;
}
bool outMgLULProc(int& command) {
my_mutex1.lock();
my_mutex2.lock();
std::lock(my_mutex1, my_mutex2);
if (!msgRecvQueue.empty())
{
int command = msgRecvQueue.front();//返回第一个元素,
msgRecvQueue.pop_front();//移除第一个元素,但不返回
//这里就考虑处理数据
my_mutex1.unlock();
my_mutex2.unlock();
return true;
}
my_mutex1.unlock();
my_mutex2.unlock();
return false;
}
//把数据从消息队列中取出线程:
void outMsgRecvQueue() {
int command = 0;
for (int i = 0; i < 100000; i++)
{
bool result = outMgLULProc(command);
if (result == true)
{
cout << "outMsgRecvQueue()执行,取出一个元素";
}
}
cout << "end" << endl;
}
private:
//这个就是共享数据
std::list msgRecvQueue;//容器,专门用于代表玩家给我们发送来的命令
std::mutex my_mutex1;//创建一个互斥量
std::mutex my_mutex2;//
};
inMsgRecvQueue和outMgLULProc调用my_mutex1,my_mutex2的顺序不一样就会产生错误。
下面这个是一个随便写的:
std::thread t1(function);
t1.detach(); //主程序执行太快,还没等t1执行完毕,主线程就结束了
if (t1.joinable()) {
t1.join();
}
一个线程不仅可以通过函数构造,也可以通过一些可调用的对象进行构造。 我们直接创造一个类来构建
class Fctor {
public:
void operator()() {
for (int i = 0; i > -100; i--)
{
std::cout << "from t1 " << i << std::endl;
}
}
};
int main(){
Fctor fct;
std::thread t1(fct);
t1.join();
}
这里有点奇怪,这种调用方式是什么情况??
class Fctor {
public:
void operator()(std::string msg) {
for (int i = 0; i > -100; i--)
{
std::cout << "from t1 " << msg << std::endl;
}
}
};
std::string s = "I LOVE YOU";
std::thread t1(Fctor(),s);//不能写成std::thread t1(Fctor(s))
//通过引用可以避免很多赋值操作,节省内存的开销
//通过引用可以避免很多赋值操作,节省内存的开销,所以我们对上述代码进行改造,但是我们等下会发现并不是简单的只要再std::string &msg就可以,在19年的编译器会直接报错
class Fctor {
public:
void operator()(std::string &msg) {
std::cout << "from t1 " << msg << std::endl;
msg = "进入值传递";
}
};
int main(){
std::string s = "I LOVE YOU";
std::thread t1(Fctor(),s);//不能写成std::thread t1(Fctor(s))
std::cout << s <<:endl>
//通过引用可以避免很多赋值操作,节省内存的开销
正确的写法如下:
class Fctor {
public:
void operator()(std::string& msg) {
std::cout << "from t1 " << msg << std::endl;
msg = "I love you ";
}
};
int main() {
std::string s = "I LOVE YOU";
std::thread t1(Fctor(), std::ref(s));//不能写成std::thread t1(Fctor(s)) t1.join();
std::cout << s << std::endl;//如果是引用传递的话,这里值应该是 “进入值传递中”
}
将一个转移到另外一个线程上去的做法是使用std::move
class Fctor {
public:
void operator()(std::string& msg) {
std::cout << "from t1 " << msg << std::endl;
msg = "I love you ";
}
};
int main() {
std::string s = "I LOVE YOU";
std::cout << std::this_thread::get_id() << std::endl;
std::thread t1(Fctor(), std::ref(s));//不能写成std::thread t1(Fctor(s))
std::thread t2 = std::move(t1);
std::cout << t2.get_id() << std::endl;//输出线程id
t2.join();
; std::cout << s << std::endl;//如果是引用传递的话,这里值应该是 “进入值传递中”
}
参考来源:https://www.bilibili.com/video/av39171692?from=search&seid=5755539653431937584www.bilibili.com