文章目录

  • ​​1.进程(线程)wait/notify​​

1.进程(线程)wait/notify

  • 一个线程通知一个等待中的线程,方法有3个:
  • 方法1:pipe
    (1)fd[0]对应管道的读端,fd[1]对应管道的写端,fd[0]只能用于读,不能用于写,fd[1]只能用于写,不能用于读,这意味着管道是单向的
    (2)等待线程等待fd[0]的可读事件,通知线程只要往fd[1]写入一个数据,fd[0]就变的可读了,等待线程就获得通知,就被唤醒了;
  • 方法2:socketpair
    (1)也有一对fd,任意一个fd都是既可读也可写的,与管道的区别在于,它可以用于双向通信;
  • 方法3:eventfd
    (1)只有一个fd,等待线程关注fd的可读事件,通知线程只要往fd写入一个数据,等待线程就获得通知,就被唤醒了;
    (2)muduo的线程唤醒使用evenfd
    (3)eventfd 是一个比 pipe 更高效的线程间事件通知机制,一方面它比 pipe 少用一个 file descripor,节省了资源;另一方面,eventfd 的缓冲区管理也简单得多,全部“buffer” 只有定长8 bytes,不像 pipe 那样可能有不定长的真正 buffer。
  • 总结
    (1)上面的3个方法还可以用于进程间的等待通知,线程间的通知方法除了上述3种,还可以用条件变量。
    (2)条件变量与上述3方法区别在于,他们都是fd,可以使用I/O复用来管理(poll,epoll)
  • 类图
    一个EventLoop对象可以包含多个Channel对象,他们是聚合关系,EventLoop对象不负责Channel的生存期;
    Channel作为TcpConnection,Acceptor,Connector的成员,是组合的关系,Channel对象的生存期是由这些对象来控制的;
    eg:29\jmuduo\muduo\net\TimerQueue.h中的 Channel timerfdChannel_;
  • (P29)muduo_base库源码分析:进程(线程)wait/notify_临界区

  • eg:29\jmuduo\muduo\net\EventLoop.h
    29\jmuduo\muduo\net\EventLoop.cc
  • runInLoop()流程图
    若调用runInLoop()的线程是当前I/O线程,isInLoopThread()若是当前线程要执行回调任务,那么就直接同步的调用cb;
    若不是当前线程调用了runInLoop(),queueInLoop()那么把任务添加到线程的队列中,以便I/O线程能够执行该任务;
  • (P29)muduo_base库源码分析:进程(线程)wait/notify_回调函数_02

  • EventLoop::runInLoop
// 在I/O线程中执行某个回调函数,该函数可以跨线程调用
void EventLoop::runInLoop(const Functor& cb)
{
if (isInLoopThread())
{
// 如果是当前IO线程调用runInLoop,则同步调用回调函数(任务)cb
cb();
}
else
{
// 如果是其它线程调用runInLoop,则异步地将cb添加到队列
//以便让EventLoop所对应的I/O线程来执行该回调函数
queueInLoop(cb);
}
}
  • EventLoop::queueInLoop
//将任务cb添加到队列中
void EventLoop::queueInLoop(const Functor& cb)
{
{
MutexLockGuard lock(mutex_);
pendingFunctors_.push_back(cb);//将任务添加到一个任务队列pendingFunctors中
}
//其他线程往I/O线程中添加一个任务,那么就需要唤醒这个I/O线程,以便其能
//及时地执行该任务
// 调用queueInLoop的线程不是当前IO线程需要唤醒
// 或者调用queueInLoop的线程是当前IO线程,并且此时正在调用pending functor,需要唤醒, if (!isInLoopThread() || callingPendingFunctors_)
// 只有当前IO线程的事件回调中调用queueInLoop才不需要唤醒
if (!isInLoopThread() || callingPendingFunctors_)
{
wakeup();
}
}
  • EventLoop::loop
while (!quit_)
{
activeChannels_.clear();
pollReturnTime_ = poller_->poll(kPollTimeMs, &activeChannels_);//返回通道activeChannels_
if (Logger::logLevel() <= Logger::TRACE)
{
printActiveChannels();
}
eventHandling_ = true;
for (ChannelList::iterator it = activeChannels_.begin();
it != activeChannels_.end(); ++it)
{
currentActiveChannel_ = *it;
currentActiveChannel_->handleEvent(pollReturnTime_);//handleEvent处理这些事件
}
currentActiveChannel_ = NULL;
eventHandling_ = false;
//事件处理完毕后,调用
doPendingFunctors();//向当前I/O线程添加回调任务,目的是让I/O
//线程也能执行一些计算任务。因为I/O线程不是很繁忙的时候,会一直处于阻塞不工作的状态。
}
  • 队列queueInLoop()流程图
    (1)若不是当前I/O线程((!isInLoopThread() == true时),则需要wakeup,A线程要把任务cb添加到B线程(他是I/O线程)的任务队列中,那么cb需要能够及时的被B执行,所以需要唤醒B,以便它能够执行。
    B是I/O线程,它处于loop中,poll()唤醒它,以便它能够执行到doPendingFunctors(),
    (2)若调用queueInLoop是当前线程((!isInLoopThread() == false时),并且此时正在调用pending functor,需要唤醒。这种情况只可能是doPendingFunctors()中又调用了queueInLoop(),即:当前IO线程处于doPendingFunctors(),在这个函数内部又调用了queueInLoop(),也需要唤醒当前的IO线程poll,否则doPendingFunctors()都执行了完毕之后,回来到poll,但是doPendingFunctors()这里面又调用了queueInLoop(),又添加了个任务,你没办法唤醒它,就没有办法及时的处理。
    (3)只有IO线程的事件回调中调用queueInLoop才不需要唤醒,在handleEvent中调用queueInLoop,将cb添加到队列中是不需要唤醒的,因为handleEvent处理完毕之后,会跑到doPendingFunctors(),所以说就不需要唤醒了。
  • (P29)muduo_base库源码分析:进程(线程)wait/notify_开发语言_03

  • doPendingFunctors
// 当调用该函数时,就处于调用这些回调任务的状态中
void EventLoop::doPendingFunctors()
{
//定义了一个空的向量
std::vector<Functor> functors;
callingPendingFunctors_ = true;

{
// 只保护这块内容的临界区
MutexLockGuard lock(mutex_);
functors.swap(pendingFunctors_);//交换完毕后,pendingFunctors_就变成了空
} //pendingFunctors_的回调任务都放到了functors中
/*
下面的内容没有保护,这是为啥呢?
(1)不是简单地在临界区内依次调用Functor,而是把回调列表swap到functors中,这样一方面减小了临界区的长度(意味着不会阻塞其它线程的queueInLoop():
又把回调任务添加进了pendingFunctors_,因为已经将pendingFunctors_交换到functors,下面的代码此时是不会执行的,所以下面的代码不需要取加锁)
另一方面,也避免了死锁(因为Functor可能再次调用queueInLoop())
lock//若其位置在callingPendingFunctors_ = true;之下
lock(该lock来自queueInLoop())
unlock//若其位置在 callingPendingFunctors_ = false;之上
由于此时还没有unlock,中间的是没有办法获得lock锁,递归了就会处于死锁的状态

(2)由于doPendingFunctors()调用的Functor可能再次调用queueInLoop(cb),这时,queueInLoop()就必须wakeup(),否则新增的cb可能就不能及时调用了

(3)muduo没有在loop()函数中反复执行doPendingFunctors()直到pendingFunctors为空,这是有意的,否则IO线程可能陷入死循环,无法处理IO事件。
因为doPendingFunctors()调用了functors(),又调用了queueInLoop()将任务添加进来,那么loop()中的doPendingFunctors()可能永远无法执行到空,
所以没有必要将所有的任务执行完毕,只是将交换出来的任务执行完毕而已。
*/
// 遍历functors列表,执行它
for (size_t i = 0; i < functors.size(); ++i)
{
// functors回调任务函数
functors[i]();
}
callingPendingFunctors_ = false;
}
  • eg:29\jmuduo\muduo\net\EventLoop.cc
    29\jmuduo\muduo\net\TimerQueue.cc
  • eg测试:29\jmuduo\tests\Reactor_test05.cc
    29\jmuduo\tests\CMakeLists.txt
  • 测试:
  • (P29)muduo_base库源码分析:进程(线程)wait/notify_临界区_04