C++并发与多线程学习笔记--多线程数据共享问题
- 创建和等待多个线程
- 数据和共享问题分析
- 只读的数据
- 有读有写
- 其他案例
- 共享数据的保护案例代码
创建和等待多个线程
服务端后台开发就需要多个线程执行不同的任务。不同的线程执行不同任务,并返回执行结果。很多个线程都用同一个线程入口:
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。
参考文献