类对象跨线程处理的是十分棘手的,要想清楚一切可能发生死锁的情况,本篇博客中的例子都是来自muduo网络库陈硕的这本书,这本书前两章读了很多遍才彻底读懂,书中的例子十分具有特点,我十分喜欢,故拿书中举的例子记录在博客之中
加锁的顺序一致
加锁的顺序一定要一致,不要出现线程加锁顺序不一致的情况,因为会有死锁的可能发生。A和B都继承Base,加锁的顺序不一样,可能导致死锁现象的产生。解决办法是通过比较锁地址大小来进行加锁,就是加个ifelse,始终保持先加大的还是先加小的。
伪共享
伪共享:多根线程在一个缓存行操作,一个CPU的缓存的缓存行还要和其他的CPU缓存的缓存行交互,这就是一种伪共享参考的链接
熟练使用gdb bt把每个线程的栈都会打出来的,如果发生了死锁也可以轻而易举的看出来。
以前我不懂为什么C++容易产生内存泄露,深入学习多线程之后,尤其一个类的对象是跨线程的,对象什么时候要delete分析出来是十分复杂且麻烦的事情。
#include <iostream>
#include <mutex>
#include <thread>
#include <vector>
#include <unistd.h>
#include <deque>
#include <atomic>
using namespace std;
using VecI = deque<int>;
shared_ptr<VecI> g_ptr = make_shared<VecI>();// 初始化一下
mutex g_mutex;
void read(){
unique_lock<mutex> l(g_mutex);
for(int i = 0;i < g_ptr->size();++i){
//doSomething;
}
}
void write(int val){
unique_lock<mutex> l(g_mutex);
g_ptr->emplace_back(val);
}
在上述代码中,看样子是正确的,我在没看muduo网络库这本书之前,也觉得这么写挺对的,但实际呢,当doSomething间接的调用了write,那么就产生了死锁。可以写一个不加锁的write版本,但又存在新的问题,误用不加锁版本的write怎么办?介绍一个牛逼的做法,copy on write
copy on write
copy on write坦白讲可以理解成多个线程一起读啦,一起读肯定不会有什么线程安全问题的啦。但是要修改某个变量的时候,就需要分道扬镳了,各自维护一个不同缓冲区。或者最后在合并。COW(copy on write的简称)好处如果只是读的话没必要大费周章的去拷贝,拷贝会耗时,一起读就好了。当需要改变这个副本的时候再进行深拷贝即可。举个例子,像fork函数的子进程拥有父进程的副本,也是使用了COW技术,当子进程修改副本的时候再进行拷贝。他们的文件表是共享的,也就是当前文件偏移量都可以知道了,父进程写完子进程写是不会有问题的。参考APUE184页,附上自己写的小demo
#include <sys/types.h>
#include <unistd.h>
#include<iostream>
#include<sys/wait.h>
#include <signal.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
using namespace std;
int main(){
int fd = ::open("./hello.txt",O_RDWR);
if(fd < 0){
cout << "打开失败" << endl;
return -1;
}
if(fork() == 0){
char* buff = "I am son process\n";
::write(fd,buff,strlen(buff));
exit(0);
}
//sleep(2); // 加上让子进程先写。
char* buff = "I am father process\n";
::write(fd,buff,strlen(buff));
return 0;
}
read中稍微改动一下,也减少了临界区,后来加入的再晚点调用。
void read(){
shared_ptr<VecI> tmp;
{
unique_lock<mutex> l(g_mutex);
tmp = g_ptr;
}
for(int i = 0;i < tmp->size();++i){
//doSomething
}
write函数中也需要改一下,如果没有再读,就直接插入数据,因为这是一个共享的智能指针,如果有再读的线程,那就重新new一下,old地址还没有delete,等read线程读完了,old地址智能指针就会释放掉了。copy on write设计是真的很精妙,书上给出了3种错误的copy on write的写法,书中留作的是思考题,再博客中记录下自己所思考的东西,想了差不多一个下午的时间。
void write(int val){
unique_lock<mutex> l(g_mutex);
if(!g_ptr.unique()){
g_ptr.reset(new VecI(*g_ptr));
}
g_ptr->emplace_back(val);
}
以下的代码都是错误的!!
// 错误1
/*
* 错误原因:同时操作了一个shared_ptr
* 但经过测试,数据没少,但是同时操作操作了shared_ptr
*/
void write(){
++num;
unique_lock<mutex> l(g_mutex);
g_ptr->emplace_back(2);
}
// 错误2,会出现core dump,原因如下
/*
* 假设有两根子线程,线程1在加锁的时候,重新给g_ptr赋值,
* 老的g_ptr被释放掉了,但是,此时很不幸,线程2拿到的是老的g_ptr的值,
* 线程2用*nullptr就出现段错误了(core dump)
*/
void write(){
++num;
shared_ptr<VecI> newPtr = make_shared<VecI>(*g_ptr);
newPtr->emplace_back(2);
unique_lock<mutex> l(g_mutex);
g_ptr = newPtr;
}
// 错误三
/*
* 数据有损失了但不会core dump
* 同时存在多个newPtr,以最后赋值的newPtr赋值,当然会出现数据丢失现象
*/
void write(){
++num;
shared_ptr<VecI> oldPtr;
{
unique_lock<mutex> l(g_mutex);
oldPtr = g_ptr;
}
shared_ptr<VecI> newPtr = make_shared<VecI>(*oldPtr);
newPtr->emplace_back(2);
unique_lock<mutex> l(g_mutex);
g_ptr = newPtr;
}