C++并发与多线程学习笔记--多线程数据共享问题

时间:2023-04-21 22:02:02
  • 创建和等待多个线程
  • 数据和共享问题分析
    • 只读的数据
    • 有读有写
    • 其他案例
  • 共享数据的保护案例代码

创建和等待多个线程

服务端后台开发就需要多个线程执行不同的任务。不同的线程执行不同任务,并返回执行结果。很多个线程都用同一个线程入口:

void myprint(int num)
{
cout << "线程开始执行了: " << num << endl;
cout << "My print id: "<<this_thread::get_id() << endl;
cout << "线程结束执行了" << endl;
return;
} int main()
{
vector<thread> workers;
//创建10个线程,线程的入口统一使用 void myprint(int num)
for (int i = 0; i < 10; i++) {
workers.push_back(thread(myprint, i)); //创建并开始执行线程
}
for (auto iter = workers.begin(); iter != workers.end(); ++iter) {
iter->join();
} cout << "Main Thread End!!!" << endl; return 0;
}

 小结:

1)多个线程的执行顺序是乱的,跟操作系统内部的运行机制有关。

2)主线程等待所有子线程运行结束,最后主线程才结束。

3)用join写出来的程序才比较稳定,更容易写出稳定的程序。

4)专门用迭代器创建多个线程的写法,创建大量的线程进行管理,很方便。==>线程池的思路。

数据和共享问题分析

只读的数据

共享数据

vector<int> g_v = { 1,2,3 }; //共享数据

 修改打印函数

void myprint(int num)
{ cout << "线程的ID为 " << this_thread::get_id() <<
"打印 g_v的值为" << g_v[0] << g_v[1] << g_v[2] << endl;
return;
}

 虽然顺序不定,但是每次都成功读出了数组中的值。==>只读数据是安全稳定的,直接可以读。

有读有写

有读有些的线程,一旦代码写不好的时候,容易出问题,崩溃or报错。

两个线程往容器里面写,八个线程往容器里读,如果没有特别的处理,程序肯定崩溃。最简单的不崩溃处理,读的时候不能写,写的时候不能读。==>互斥锁的思路??

1)任务切换有各种诡异的事情发生,如程序崩溃。

std::mutex locker;

  

void myprint(int num)
{
locker.lock();
cout << "线程的ID为 " << this_thread::get_id() <<
"打印 g_v的值为" << g_v[0] << g_v[1] << g_v[2] << endl;
locker.unlock();
return;
}

  

其他案例

例1:假设定火车票,10个售票窗口。

1,2窗口同时卖票,如果座位已经有人订了,那么直接返回,告诉顾客已经有人坐了,否则订票。

1号窗口和2号窗口共享这些数据。

共享数据的保护案例代码

实际工作中的范例:网络游戏服务器开发,网络游戏服务器,这个服务器,最简单的有两个自己创建的线程:(实际中可以用线程池来做):

1) 用来收玩家的命令并把命令数据写到一个队列中,这个线程专门负责通过网络收数据。

2) 线程重队列中取出玩家发送的命令,解析,执行玩家的动作---抽卡!!!

3) 用数字表示玩家的动作

4) vector, list, list和vector的内部实现手段不一样,在底层虽然都是push_back(),但是在插入元素的时候,vector需要复制内存到新的空间,并且有[]操作符的重载,而list是数据结构中的双向链表,因此没有[],在频繁插入和删除的时候使用List,容器对于随机的插入和删除效率高。

 使用List的时候

#include<list>

 用成员函数作为线程函数的方法写线程。实际中,一般都把变量写在类中,符合面向对象的程序设计思想。

class ProcessRequest {
public:
//把命令加入到一个队列
void inMsgRecvQueue() {
for (int i = 0; i < 100000; ++i) {
cout << "插入一个元素" << endl;
m_msgRecvQueue.push_back(i); //假设这个队列表示玩家的命令 } //占用时间片
}
//把命令移出一个队列
void outMsgRecvQueue() { for (int i = 0; i < 100000; ++i) {
if (!m_msgRecvQueue.empty()) {
//消息不为空
int command = m_msgRecvQueue.front();
//尝试返回第一个元素,取出元素
m_msgRecvQueue.pop_front();
}
else
{
cout << "outMsgRecvQueue() 还执行,但是消息队列为空"<<i<< endl;
//消息队列为空
} } //占用时间片
cout << "end!!!!!" << endl;
} private:
std::list<int> m_msgRecvQueue; //容器,用于表示玩家的发送过来命令 };

 主函数

	ProcessRequest obj;
std::thread outWorker(&ProcessRequest::outMsgRecvQueue, &obj);
//第二参数是引用才是同一个对象,不能用detach(),否则不稳定
std::thread inWorker(&ProcessRequest::inMsgRecvQueue, &obj);
//两个线程创建完成之后,要保证对象是有意义的,用join
outWorker.join();
inWorker.join();

 数据共享的理论,有读有写,不断地读和写,共享的消息队列,如果完全不控制,一定会出错。共享数据与锁,某个线程在操作的时候,其他线程需要等待。写的时候锁住,读的时候锁住。

1)互斥量。多线程一定会有互斥量,下回分解。。。

其他知识

vector和built-in数组类似,它拥有一段连续的内存空间,并且起始地址不变,因此它能非常好的支持随即存取,即[]操作符,但由于它的内存空间是连续的,所以在中间进行插入和删除会造成内存块的拷贝,另外,当该数组后的内存空间不够时,需要重新申请一块足够大的内存并进行内存的拷贝。这些都大大影响了vector的效率。

list就是数据结构中的双向链表(根据sgi stl源代码),因此它的内存空间可以是不连续的,通过指针来进行数据的访问,这个特点使得它的随即存取变的非常没有效率,因此它没有提供[]操作符的重载。但由于链表的特点,它可以以很好的效率支持任意地方的删除和插入。

deque是一个double-ended queue,它的具体实现不太清楚,但知道它具有以下两个特点:
它支持[]操作符,也就是支持随即存取,并且和vector的效率相差无几,它支持在两端的操作:push_back,push_front,pop_back,pop_front等,并且在两端操作上与list的效率也差不多。

因此在实际使用时,如何选择这三个容器中哪一个,应根据你的需要而定,一般应遵循下面
的原则:
  1、如果你需要高效的随即存取,而不在乎插入和删除的效率,使用vector
  2、如果你需要大量的插入和删除,而不关心随即存取,则应使用list
  3、如果你需要随即存取,而且关心两端数据的插入和删除,则应使用deque。

参考文献

https://study.163.com/course/courseLearn.htm?courseId=1006067356#/learn/video?lessonId=1053470368&courseId=1006067356

https://blog.csdn.net/bmjhappy/article/details/82228080