c++ 全局对象 多线程共用_c++多线程的学习(1)

这篇博客介绍了并发、进程和线程的基础概念,强调了多线程在提高程序效率上的优势。讨论了线程的启动、结束和管理,包括`join()`和`detach()`的使用。通过示例代码说明了如何创建和管理线程,以及在多线程中处理数据共享和同步问题,特别是通过互斥量避免死锁。此外,还提到了使用类和成员函数创建线程时需要注意的问题,如拷贝构造函数和数据一致性。
摘要由CSDN通过智能技术生成

第一节:并发基本概念及实现,进程,线程得基本概念

并发,线程,进程的概念

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=5755539653431937584​www.bilibili.com

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值