Muduo网络库的实现runInLoop(六)

源码下载以及安装点击链接https://blog.csdn.net/YoungSusie/article/details/90021742

分类 Muduo网络库编程 学习笔记

1、runInLoop

Event Loop 有一个非常有用的功能:在它的IO线程内执行某个用户任务的回调, 即EventLoop::runInLoop(const Functor & cb), 其中 functor是 boost::function如果用户在当前IO线程调用这个函数,回调会同步进行;如果用户在其他线程调用runInLoop(),cb会被加入队列,IO线程会被唤醒来执行回调。
有了这个功能,我们可以轻易地在线程间进行任务调配,这样可以在不用加锁的情况下保证线程的安全性。

由于IO线程平时阻塞在事件循环EventLoop 的 poll函数中 ,为了让其他线程能立刻执行用户回调,需要立刻唤醒它。传统的方法是使用pipe(),IO 线程始终监视此管道的readable 事件,在需要唤醒的时候,其他线程往管道里写一个字节,这样IO线程就从IO multiplexing 阻塞调用中返回。现在有了Linux的eventfd ,可以高效地唤醒,因为其不必管理缓冲区。

eventfd

  • 概念

    是一个用来通知事件的文件描述符,就像前一节所述的timerfd 是定时器的文件描述符,二者都是内核向用户空间的应用发送通知的机制,可以有效地被用来实现用户空间的事件/通知驱动的应用程序。

  • 使用
    使用eventfd() 系统调用,需要包含头文件 #include ,在pipe 仅用于发出事件信号的所有情况下都可以换为eventfd。
    与pipe(用于事件通知的时候)区别主要在于两点:首先,pipe 需要两个文件描述符,fd[0] 为读而打开,fd[1] 为写而打开;eventfd 只需要一个文件描述符。其次pipe 需要管理不定长的缓冲区,eventfd缓冲区为定长8字节。

int eventfd(unsigned int initval, int flags);

创建一个eventfd 对象,该对象是一个内核维护的无符号的64位整型计数器。

  • 支持的操作
    read: 如果计数值counter 的值不为0,读取成功,获得该值。如果计数值 counter 的值为0 ,并且创建eventfd设置为非阻塞,则直接返回,设置错误为 EAGAIN;如果
    创建eventfd设置为阻塞,则等待counter 为非0 。
    write: 会增加8字节的整数在就计数器counter 上,如果 counter 的值达到0xfffffffffffffffe时,就会阻塞,直到counter 的值被read。
    close

Linux Programmer’s Manual - EVENTFD

代码

void EventLoop::runInLoop(const Functor & cb)
{
	if (isInLoopThread())  //如果在当前IO 线程,直接回调
        {
		std::cout << "In Thread" <<  std::endl;
		cb();
	}
	else{
		std::cout << "Not in Thread " <<  std::endl;
		queueInLoop(cb);//如果不在当前线程,将回调加入队列
	}
}
void EventLoop::queueInLoop(const Functor & cb)
{
	{
		MutexLockGuard  lock(mutex_);
		pendingFunctors_.push_back(cb);
	}
	if (!isInLoopThread() || callingPendingFunctors_)
	{ //如果不在IO线程或者此时正在pending functor ,则wakeup
		wakeup();
	}
}

思考:
为什么只有在IO线程的事件回调中调用queueInLoop () 才无须wakeup()?
换句话说:为什么在pending functor 的时候仍然需要wakeup?

因为由于doPendingFunctor() 中调用的Functor()可能再调用queueInLoop(),这是如果不 wakeup,可能或导致新加入的cb 回调不及时。

void EventLoop::doPendingFunctors()
{
	std::vector<Functor> functors;
	callingPendingFunctors_ = true;

	{
		MutexLockGuard lock(mutex_);
		functors.swap(pendingFunctors_);
	}

	for(size_t i = 0; i < functors.size();++i)
	{
		functors[i]();
	}
	callingPendingFunctors_ = false;
}
void EventLoop::wakeup()
{
	uint64_t one = 1;
	ssize_t n = ::write(wakeupFd_,&one,sizeof one);
	if( n != sizeof one)
	{ 
		std::cout << "EventLoop::wakeup error";
		_exit(-1);
	}
}

2、提高TimerQueue 的线性安全性

前面TimerQueue class 对外开放的接口 addTimer() 只能在IO线程调用,因此 EventLoop::runAfter() 系列的函数不是线程安全的。因为 EventLoop::runAfter() 会调用addTimer() ,如果是在其他的线程调用EventLoop::runAfter() ,也就是在其他的线程调用addTimer(),则不是安全的。
新增一个TimerQueue class 的private 成员函数 addTimerInLoop,在addTimer 中调用这个函数,将实际的工作转移到IO线程中。
将addertime拆分为两部分,拆分后的addtimer只负责转发,addTimerInLoop负责完成修改定时器列表的工作。线程安全的。


TimerId TimerQueue::addTimer(const TimerCallback & cb,Timestamp when,double interval)
{
	Timer* timer = new Timer(cb,when,interval);
	loop_->runInLoop(boost::bind(&TimerQueue::addTimerInLoop,this,timer));
	return TimerId(timer);

}

void TimerQueue::addTimerInLoop(Timer* timer)
{
	
	loop_->assertInLoopThread();
	bool earliestChanged = insert(timer); ///已经失效的定时器
	if(earliestChanged)
	{ resetTimerfd(timerfd_,timer->expiration());}

	
}

3、EventLoopThread class

IO线程不一定是主线程, 我们可以在任何一个线程创建并运行eventloop, 一个程序也不止一个IO线程,可以按照优先级不同将不同的socket 分给不同的IO线程,避免优先级反转。
EventLoopThread class会启动自己的线程,并在自己的线程中启动loop()。这个class中关键的是startLoop() , 这个函数会返回新线程中EventLoop对象的地址,因此用条件变量来等待线程的创建与运行。

条件变量

如果需要等待某个条件成立,应该使用条件变量,这个变量是一个或者多个线程等待某个bool 表达式为真,即等待别的线程唤醒它。条件变量的正确使用方法:
对于wait 端,对于等待条件端:

  • 必须与mutex一起使用,该bool 表达式的读写需要受到mutex 保护
  • 在mutex 已上锁的时候才可以调用wait() .
  • 把判断bool条件和wait() 放在while循环中

伪代码可以表示为

MutexLock mutex;
Condition cond;
std::deque<int> queue

int dequeue()
{
	MutexLockGuard lock(mutex);
	while(queue.empty())
	{
		cond.wait();
	}
	int top = queue.front();
	queue.pop_front();
	return top;
}

必须用while 来等待 条件变量,不能用if,否则会引起虚假唤醒
在多核处理器中,pthread_cond_signal 可能会同时激活多余一个线程(阻塞在条件变量上的线程)。结果是,当一个线程调用pthread_cond_signal 后,多个调用pthread_cond_wait 的线程返回,这种效应称为虚假唤醒。
虚假唤醒 解决 方法是将使用while,而不是if。

如果用if 作为判断的条件,当一个线程调用pthread_cond_signal后,阻塞的线程被唤醒,这个时候应该还是先检查下条件是否满足,再决定是否往下运行,如果不判断而往下运行导致错误。

对于signal 端,对于发送条件端:

  • 不一定要在mutex 已上锁的情况下调用signal
  • 在signal之前不要修改bool表达式的值.
  • 修改bool 表达式的值一般用mutex 锁保护
  • 注意区分signal 和 broadcast : broadcast 通常用于表明状态的变化,signal 表示资源可用

伪代码可以表示为

MutexLock mutex;
Condition cond;
std::deque<int> queue

void enqueue(int x)
{
	MutexLockGuard lock(mutex);
	queue.push_back(x);
	cond.notify();  //signal的包装
}

条件变量是非常底层的同步原语,很少直接使用,一般都是用来实现高层的同步措施。

分类 Muduo网络库编程 学习笔记

你可能感兴趣的:(Muduo)