栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > C/C++/C#

# muduo源码学习 Day02

C/C++/C# 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

# muduo源码学习 Day02

muduo源码学习 Day02

`

文章目录
  • muduo源码学习 Day02
  • 前言
  • 一、初识线程安全(一个创建时候,一个销毁时候)
    • 1. 销毁太难
    • 2. 创建还行
  • 二、初学线程同步
    • 1.线程同步与线程间通信
    • 2.怎么用mutex和条件变量
    • 3.不要使用读写锁和信号量(仅仅当定理使用)
  • 三、封装MutexLock,MutexLockGuard,Condition
  • 总结


前言

因为之前有写过TinyWebsever的项目,所以难免会将muduo和自己写的那个(基于游双大神写的项目作对比), 这一篇就作为muduo开始的前奏吧,把自己想的多线程服务器应该是是什么样的,描述一下-----------------------------------------------------------------

一、初识线程安全(一个创建时候,一个销毁时候)

 回想一下,自己敲这敲那,除了刷了很多题,好像真正敲项目的时间并不多,所以开始的时候真的没有感觉到C++里面那些思想比如面向对象,比如泛型到底应该怎么用,muduo里的封装和RAII是我最开始有认识到自己真的差的很多的地方,所以我想有必要记录一下。
  RAII 就是 Resource Acquisition Is Initialization(资源获取即初始化”),这个即,我认为就是立即的意思,显而易见,构造函数很好的实现了这句话的语义,简单来说,把获取资源放进构造,释放资源放进析构,就可以有效的管理资源,释放资源。这么简单吗?开始的时候我是这么想的,但是到了多线程,一步一个坑。

1. 销毁太难

 想像一下一个场景,怎么保证析构一个对象的时候,没有别的线程正在执行这个对象的成员函数,怎么保证执行这个对象成员函数期间,对象不会在另一个线程被析构,也就是说我在调用函数前,我得知道这个对象还活着没,它是否开始执行了析构函数(析构执行一半,我去调用它的成员函数,这也不行),怎么办----------------------------------------------------------------------------------------自然是加锁,解锁。
 用RAII的手法封装互斥器的创建和销毁(MutexLock),封装临界区的进入和退出(MutexLockGuard),这就解决了?当然不行。。。 封装好的临界区和互斥器,析构的时候会被销毁啊!!!!!!!!!!!!!!!!!!!!!!!!!!!!

代码如下(示例):

Foo::~Foo(){
	MutexLockGuard lock(mutex);
	// to free something
}

void Foo::update(){
	MutexLockGuard lock(mutex);
	// to do something
}

假设,有A,B两个线程都能看到Foo这个对象,A想析构Foo,B想调用Foo的update

// thread A
delete x;
x = NULL;

//thread B
if(x){
	x->update();
}

 我的本意是Foo的析构函数我都加锁了,那么它的析构过程应该是安全的吧,而且我delete之后给对象赋值NULL,利用if (x) 防止访问空指针,防止二次释放。 但是一旦 x死了 ,它的mutex也随之而去了。假设A 开始析构x,同时B刚好通过了if(x)的检测,这时候B阻塞在了mutex上(因为被A占据了),这个时候 A 继续往下走,x死了,mutex也跟着走了,会发生什么?… muduo一书中给出的答案是未知,总之不是好事。
 事实证明,利用mutex想保护好析构过程,不太可能,因为mutex的生命周期最多和对象一样长(它怎么管“后事“呢),另外对于基类,派生类的析构优先于基类,如果我们保护了基类的析构函数,从调用顺序上,派生类析构部分已经结束了,这个过程谁来保护?
 最完美的解决办法就是解决提出问题的人…,设计之初就不让别的线程访问这个对象就好了,好像真的是诶…,现在回过头再读一遍 one loop one thread,这就是贯穿muduo始终的一句话。
 设计终归是设计,总会有直面问题的时刻,考虑这个场景,对象x的指针被其他非静态成员函数所持有,现在x要析构,那个函数还要安全的访问x(别出问题,死了我就不访问了),muduo书上给出的场景是这个对象所属的类还是个基类,更完了,它析构的时候,派生类都析构完了,对象都死了一半了,还有一口气,这个时候谁来保证过程中的线程安全,有是有人能在调用的时候实时告诉我这个对象告诉我他还活着么就好了,实时我不知道难不能做到,但能做到保证你访问的时候,他没死… --------------------------------------------------------------------------------智能指针 yyds
 shared_ptr和weak_ptr是我们的好朋友,这一阶段的学习应该是离不开他们了(我想我会单独写一篇给他们,把项目里的场景拿出来写写)…
 总结起来就是weak指针判断生死,share指针先让你不死,理论上通过智能指针和mutex,可以解决我们面临的问题,但是设计的时候还是要尽量减少使用跨线程对象,至于具体怎么解决的,我想下一篇单独写一下,给它足够的尊重…

2. 创建还行

 对于创建,二段式构造简单粗暴--------------------------------------------------------------------------------------秘诀就是别在构造函数期间泄露this指针
原因有很多,比如构造函数期间对象还没有完全初始化,这时候你把this给出去了,别的线程拿到一个残次品就遭殃了
构造函数+init(),目前我也不会别的了…,还得学

二、初学线程同步 1.线程同步与线程间通信

 可能是背八股背多了,一说到线程同步,我就想线程间通信,条件变量,信号量,互斥锁…
 其实里面东西还是有很多的,还得学,路还长…
 muduo书中介绍了四项原则,总结就是能不通信就别通了,实在不行用系统提供的互斥器和条件变量,别自己瞎写代码,还有尽量别用读写锁(太好了,可以少学一个),别用信号量。

2.怎么用mutex和条件变量

先说mutex
第一   RAII封装,创建 销毁 加锁 解锁四个服务封装到一个类中
第二   只使用不可重入锁(可重入和不可重入就是同一个线程能不能重复加锁)
第三   代码绝对不暴露lock和unlock,加锁解锁一切都交给栈上guard对象
第四   如果跨进程,只使用socket进行通信,禁用跨进程mutex

为什么用不可重入锁呢,可以更好的暴露问题(容易debug),可重入锁虽然可能会减少一些问题的发生,但到了程序真崩溃时候,可能都不知道怎么死的…

不可重入锁可能会带来问题,一个函数可能会在加锁的时候调用,也可能会在未加锁时候调用,写两个版本即可,函数加锁的时候调用加锁版本。

条件变量:注意wait 放在哪 ,if改成while
第一:  必须与mutex一起使用
第二:  先加锁,在调用wait
第三:  wait要放在while循环里
第四:  signal之前要修改布尔表达式的条件(比如下面例子中的queue.empty()),修改的时候要记得加锁

int dequeue(){
	MutexLockGuard lock(mutex);
	while(queue.empty()){
		cond.wait();
	}
	...
}
void enqueue(int x){
{
	MutexLockGuard lock(mutex);
	queue.push_back(x);
}
	cond.notify();//muduo中 Condition类采用notify和noityAll函数名 来使用signal和broadcast
}

muduo中 倒计时 CountDownLatch机制主要就是通过条件变量实现的,后面应该会单独拿出来学习(第一遍没看懂)
倒计时主要用于 主线程等子线程完成初始化再继续执行 或 子线程等主线程完成一些事,再通知子线程启动

3.不要使用读写锁和信号量(仅仅当定理使用)

我的原则是先记着这条,以后遇见并发读写采用muduo中的copy-on-write试着解决,信号量的PV操作,muduo书中说条件变量和mutex可以解决大部分问题了,而且不容易出错。

三、封装MutexLock,MutexLockGuard,Condition

一个好的锁应该可以知道他所在的线程id,应该有很好的断言机制(不是自己的线程访问,立即报错)

class MutexLock :boost::noncopyable{
	public:
	//构造函数即分配资源 持有线程id初始化为0 ,创建一个锁
	MutexLock() :holder_(0){
		pthread_mutex_init(&mutex_,NULL);
	}
	//析构函数即释放资源 断言这个锁用没用
	~MutexLock(){
		assert(holder_ == 0);
		pthread_mutex_destroy(&mutex_);
	}
	//判断持有锁者是不是当前线程(one loop one thread) 这里的当前线程是指他应该在的那个线程(可能有点绕,一个Tcp连接对象的操作应该是在同一个线程中完成)
	bool isLockedBythisThread(){
		return holder_ == CurrentThread::tid();
	}
	void assertLocked(){
		assert(isLockedBythisThread());
	}
	//lock和unlock中的代码顺序不能变,保证tid的改变都在临界区内
	void lock(){
		pthread_mutex_lock(&mutex_);
		holder_ = CurrentThread::tid();
	}
	void unlock(){
		holder_ = 0;
		pthread_mutex_unlock()
	}
	//因为条件变量要使用互斥锁,给它留个接口 
	pthread_mutex_t *getPthreadMutex(){
		return &mutex_;
	}
	private:
	pthread_mutex_t mutex_;
	pid_t holder_;
}; 
class MutexLockGuard:boost::noncopyable{
public:
//构造函数 加锁
explicit MutexLockGuard(MutexLock& mutex):mutex_(mutex){
	mutex_.lock();
}
//析构函数解锁
~MutexLockGuard(){
	mutex_unlock();
}
private:
MutexLock& mutex_;
};
class Condition:boost ::noncopyable{
public:
//构造函数即分配.还记得使用条件变量之前要加锁.
explicit Condition(MutexLock& mutex):mutex_(mutex){
	pthread_cond_init(&pcond_,NULL);
}
//析构函数即销毁,解锁可不在这里!!!!!!!
~Condition(){
	pthread_cond_destroy(&pcond_);
}	
// wait(),signal(),broadcast()
//wait前要加锁,不加锁你参数怎么传????? 
void wait(){
	pthread_cond_wait(&pcond_,mutex.getPthreadMutex());//MutexLock留的接口用上了
}
//signal
void notify(){
	pthread_cond_signal(&pcond_);
}
void notifyAll(){
	pthread_cond_broadcast(&pcond_);
}
private:
MutexLock& mutex_;
pthread_cond_t pcond_;
};

总结

还要学,学习好的解决方案,先记住再研究为什么,真的很幸运,自己学过TinyWebsever后再来学习muduo,有些之前一直不明白为什么代码要这么写的地方,在这找到了答案,下一篇可能还是要继续写木朵的前奏,比如shared,倒计时,还有之前自己用双重检测锁写的单例也要重写一下,还有具体怎么使用智能指针,还要在仔细研磨一下,…路还很长很长,先把基础打牢,加油


2022-05-17

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/892521.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号