muduo网络库学习之EventLoop(二):进程(线程)wait/notify 和 EventLoop::runInLoop

1、进程(线程)wait/notify

pipe
socketpair
eventfd

eventfd 是一个比 pipe 更高效的线程间事件通知机制,一方面它比 pipe 少用一个 file descripor,节省了资源;另一方面,eventfd 的缓冲区管理也简单得多,全部“buffer” 只有定长8 bytes,不像 pipe 那样可能有不定长的真正 buffer。


 C++ Code 
1
2
3
4
5
6
7
8
9
 
// 该函数可以跨线程调用
void EventLoop::quit()
{
    quit_ =  true;
     if (!isInLoopThread())
    {
        wakeup();
    }
}

如果不是当前IO线程调用quit,则需要唤醒(wakeup())当前IO线程,因为它可能还阻塞在poll的位置(EventLoop::loop()),这样再次循环判断 while (!quit_) 才能退出循环。

一般情况下如果没有调用quit(),poll没有事件发生,也会超时返回(默认10s),但会继续循环。

时序分析:

构造一个EventLoop对象,构造函数初始化列表,构造poller_, timeQueue_, wakeupFd_, wakeupChannel_ 等成员,在函数体中:
 C++ Code 
1
2
3
4
 
wakeupChannel_->setReadCallback(
    boost::bind(&EventLoop::handleRead,  this));
// we are always reading the wakeupfd
wakeupChannel_->enableReading();

调用Channel::setReadCallback 注册wakeupChannel_ 的回调函数为EventLoop::handleRead, 调用Channel::enableReading(); 接着调用Channel::update(); 进而调用EventLoop::UpdataChannel(); 最后调用
Poller::updataChannel();(虚函数,具体由子类实现),此函数内添加一个新channel或者
更新此channel关注的事件,现在是将wakeupChannel_ 添加进PollPoller::channels_(假设Poller类用PollerPoller类实现) 中,并使用wakeupChannel_.fd_ 和 wakeupChannel_.events_ 构造一个struct pollfd, 并压入pollfds_; 以后将关注wakeupChannel_ (wakeupFd_) 的可读事件。

可以联想到的是当有多个socket 连接上来时,会存在多个channel,每个channel可以注册自己感兴趣的可读/可写事件的回调函数,并enableReading/Wirting,当然也可以disable Read/Write.

事件循环开始EventLoop::loop(),内部调用poll()(这里假设调用的是PollPoller::poll(), 内部调用::poll())。::poll() 阻塞返回即事件发生,如timerfd_超时可读; socket 有数据可读/可写;  非IO线程调用EventLoop:quit(), 进而调用wakeup(), 非IO线程 往wakeupFd_ 中write 入8个字节数据,此时wakeupFd_可读。现在假设是wakeupFd_ 可读,PollPoller::poll()调用PollPoller::fillActiveChannels()(虚函数), 函数内使用(struct pollfd).revents 设置此channel的revents_,然后将此channel 压入EventLoop::activeChannels_ 中后返回。PollPoller::poll() 返回,EventLoop::loop()中遍历activeChannels_,对每个活动channel调用Channel::handleEvent(),进而调用每个channel注册的读/写回调函数。
由上面分析可知,wakeupChannel_ 的回调函数为EventLoop::handleRead,函数内调用read 掉 wakeupFd_ 的数据,避免一直触发。

2、EventLoop::loop、runInLoop、queueInLoop、doPendingFunctors

EventLoop 有个pendingFunctors_ 成员:
 C++ Code 
1
2
 
typedef boost::function< void()> Functor;
std::vector<Functor> pendingFunctors_;

四个函数的流程图和实现如下:





 C++ Code 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
 
// 事件循环,该函数不能跨线程调用
// 只能在创建该对象的线程中调用
void EventLoop::loop()
{ // 断言当前处于创建该对象的线程中
  assertInLoopThread();
     while (!quit_)
    {
        pollReturnTime_ = poller_->poll(kPollTimeMs, &activeChannels_);

        eventHandling_ =  true;
         for (ChannelList::iterator it = activeChannels_.begin();
                it != activeChannels_.end(); ++it)
        {
            currentActiveChannel_ = *it;
            currentActiveChannel_->handleEvent(pollReturnTime_);
        }
        currentActiveChannel_ =  NULL;
        eventHandling_ =  false;
        doPendingFunctors();
    }
}

// 为了使IO线程在空闲时也能处理一些计算任务

// 在I/O线程中执行某个回调函数,该函数可以跨线程调用
void EventLoop::runInLoop( const Functor& cb)
{
   if (isInLoopThread())
  {
     // 如果是当前IO线程调用runInLoop,则同步调用cb
    cb();
  }
   else
  {
     // 如果是其它线程调用runInLoop,则异步地将cb添加到队列,让IO线程处理
    queueInLoop(cb);
  }
}

void EventLoop::queueInLoop( const Functor& cb)
{
  {
  MutexLockGuard lock(mutex_);
  pendingFunctors_.push_back(cb);
  }

   // 调用queueInLoop的线程不是当前IO线程则需要唤醒当前IO线程,才能及时执行doPendingFunctors();

   // 或者调用queueInLoop的线程是当前IO线程(比如在doPendingFunctors()中执行 functors[i]() 时又调用了queueInLoop())
  // 并且此时正在调用pending functor,需要唤醒当前IO线程
  // 因为在此时doPendingFunctors() 过程中又添加了任务,故循环回去poll的时候需要被唤醒返回,进而继续执行doPendingFunctors()

   // 只有当前IO线程的事件回调中调用queueInLoop才不需要唤醒
 //  即在handleEvent()中调用queueInLoop 不需要唤醒,因为接下来马上就会执行doPendingFunctors();
   if (!isInLoopThread() || callingPendingFunctors_)
  {
    wakeup();
  }
}

// 该函数只会被当前IO线程调用
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;
}


   

这样,TimeQueue的两个公有成员函数都可以跨线程调用,因为即使是被非IO线程调用,也会放进Queue,然后让当前IO线程来执行:

 C++ Code 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
 

// 该函数可以跨线程调用
TimerId TimerQueue::addTimer( const TimerCallback &cb,
                             Timestamp when,
                              double interval)
{
    Timer *timer =  new Timer(cb, when, interval);

    loop_->runInLoop(    // addTimeInLoop 只能在当前IO线程调用
        boost::bind(&TimerQueue::addTimerInLoop,  this, timer));

     return TimerId(timer, timer->sequence());
}
// 该函数可以跨线程调用
void TimerQueue::cancel(TimerId timerId)
{
    loop_->runInLoop(    // cancelInLoop 只能在当前IO线程调用
        boost::bind(&TimerQueue::cancelInLoop,  this, timerId));
}

进而EventLoop类中的定时器操作函数 runAt, runAfter, runEvery, cancel 都可以跨线程调用,因为实现中调用了TimerQueue::addTimer 和 TimeQueue::cancel .

关于doPendingFunctors 的补充说明:

1、不是简单地在临界区内依次调用Functor,而是把回调列表swap到functors中,这样一方面减小了临界区的长度(意味着不会阻塞其它线程的queueInLoop()),另一方面,也避免了死锁(因为Functor可能再次调用queueInLoop())

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

3、muduo没有反复执行doPendingFunctors()直到pendingFunctors_为空而是每次poll 返回就执行一次,这是有意的,否则IO线程可能陷入死循环,无法处理IO事件。


测试代码:
 C++ Code 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
 
#include <muduo/net/EventLoop.h>
//#include <muduo/net/EventLoopThread.h>
//#include <muduo/base/Thread.h>

#include <stdio.h>

using  namespace muduo;
using  namespace muduo::net;

EventLoop *g_loop;
int g_flag =  0;

void run4()
{
    printf( "run4(): pid = %d, flag = %d\n", getpid(), g_flag);
    g_loop->quit();
}

void run3()
{
    printf( "run3(): pid = %d, flag = %d\n", getpid(), g_flag);
    g_loop->runAfter( 3, run4);
    g_flag =  3;
}

void run2()
{
    printf( "run2(): pid = %d, flag = %d\n", getpid(), g_flag);
    g_loop->queueInLoop(run3);
}

void run1()
{
    g_flag =  1;
    printf( "run1(): pid = %d, flag = %d\n", getpid(), g_flag);
    g_loop->runInLoop(run2);
    g_flag =  2;
}

int main()
{
    printf( "main(): pid = %d, flag = %d\n", getpid(), g_flag);

    EventLoop loop;
    g_loop = &loop;

    loop.runAfter( 2, run1);
    loop.loop();
    printf( "main(): pid = %d, flag = %d\n", getpid(), g_flag);
}

执行结果如下:
simba@ubuntu:~/Documents/build/debug/bin$ ./reactor_test05
20131108 02:17:05.204800Z  2319 TRACE IgnoreSigPipe Ignore SIGPIPE - EventLoop.cc:51
main(): pid = 2319, flag = 0
20131108 02:17:05.207647Z  2319 TRACE updateChannel fd = 4 events = 3 - EPollPoller.cc:104
20131108 02:17:05.208332Z  2319 TRACE EventLoop EventLoop created 0xBF9382D4 in thread 2319 - EventLoop.cc:76
20131108 02:17:05.208746Z  2319 TRACE updateChannel fd = 5 events = 3 - EPollPoller.cc:104
20131108 02:17:05.209198Z  2319 TRACE loop EventLoop 0xBF9382D4 start looping - EventLoop.cc:108
20131108 02:17:07.209614Z  2319 TRACE poll 1 events happended - EPollPoller.cc:65
20131108 02:17:07.218039Z  2319 TRACE printActiveChannels {4: IN }  - EventLoop.cc:271
20131108 02:17:07.218162Z  2319 TRACE readTimerfd TimerQueue::handleRead() 1 at 1383877027.218074 - TimerQueue.cc:62
run1(): pid = 2319, flag = 1
run2(): pid = 2319, flag = 1
run3(): pid = 2319, flag = 2
20131108 02:17:10.218763Z  2319 TRACE poll 1 events happended - EPollPoller.cc:65
20131108 02:17:10.218841Z  2319 TRACE printActiveChannels {4: IN }  - EventLoop.cc:271
20131108 02:17:10.218860Z  2319 TRACE readTimerfd TimerQueue::handleRead() 1 at 1383877030.218854 - TimerQueue.cc:62
run4(): pid = 2319, flag = 3
20131108 02:17:10.218885Z  2319 TRACE loop EventLoop 0xBF9382D4 stop looping - EventLoop.cc:133
main(): pid = 2319, flag = 3
simba@ubuntu:~/Documents/build/debug/bin$ 

此程序是单线程,以后再举多线程的例子。由前面文章可知,timerfd_ = 4,2s后执行定时器回调函数run1(),在run1()内

g_loop->runInLoop(run2); 由于是在当前IO线程,故马上执行run2(),此时flag 还是为1;在run2()内g_loop->queueInLoop(run3);

即把run3()添加到队列,run2()返回,继续g_flag=2,此时loop内已经处理完事件,执行doPendingFunctors(),就执行了run3(), 

run3()内设置另一个3s定时器,run3()执行完回到loop继续poll, 3s后超时执行run4(),此时flag=3。

参考:
《UNP》
muduo manual.pdf
《linux 多线程服务器编程:使用muduo c++网络库》


  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
### 回答1: Linux多线程服务端编程是指使用Muduo C网络在Linux操作系统中进行多线程的服务端编程。Muduo C网络是一个基于事件驱动的网络,采用了Reactor模式,并且在底层使用了epoll来实现高效的I/O复用。 使用Muduo C网络进行多线程服务端编程有以下几个步骤: 1. 引入Muduo C网络:首先需要下载并引入Muduo C网络的源代码,然后在编写代码时包含相应的头文件。 2. 创建并初始化EventLoop:首先需要创建一个EventLoop对象,它用于接收和分发事件。通过初始化函数进行初始化,并在主线程中调用它的loop()函数来运行事件循环。 3. 创建TcpServer:然后创建一个TcpServer对象,它负责监听客户端的连接,并管理多个TcpConnection对象。通过设置回调函数,可以在特定事件发生时处理相应的逻辑。 4. 创建多个EventLoopThread:为了提高并发性能,可以创建多个EventLoopThread对象,每个对象负责一个EventLoop,从而实现多线程处理客户端的连接和请求。 5. 处理事件:在回调函数中处理特定事件,例如有新的连接到来时会调用onConnection()函数,可以在该函数中进行一些初始化操作。当有数据到来时会调用onMessage()函数,可以在该函数中处理接收和发送数据的逻辑。 6. 运行服务端:在主线程中调用TcpServer的start()函数来运行服务端,等待客户端的连接和请求。 总的来说,使用Muduo C网络进行Linux多线程服务端编程可以更好地利用多核处理器的性能优势。每个线程负责处理特定事件,通过事件驱动模式实现高效的网络编程。这样可以提高服务器的并发能力,提高系统的整体性能。 ### 回答2: Linux多线程服务端编程是指在Linux平台上使用多线程的方式来编写网络服务器程序。而使用muduo C网络是一种常见的方法,它提供了高效的网络编程接口,可以简化多线程服务器的开发过程。 muduo C网络基于Reactor模式,利用多线程实现了高并发的网络通信。在使用muduo C进行多线程服务端编程时,我们可以按照以下步骤进行: 1. 引入muduo:首先需要导入muduo C网络的头文件,并链接对应的文件,以供程序调用。 2. 创建线程池:利用muduo C中的ThreadPool类创建一个线程池,用于管理和调度处理网络请求的多个线程。 3. 创建TcpServer对象:使用muduo C中的TcpServer类创建一个服务器对象,监听指定的端口,并设置好Acceptor、TcpConnectionCallback等相关回调函数。 4. 定义业务逻辑:根据具体的业务需求,编写处理网络请求的业务逻辑代码,如接收客户端的请求、处理请求、发送响应等。 5. 注册业务逻辑函数:将定义好的业务逻辑函数注册到TcpServer对象中,以便在处理网络请求时调用。 6. 启动服务器:调用TcpServer对象的start函数,启动服务器,开始监听端口并接收客户端请求。 7. 处理网络请求:当有客户端连接到服务器时,muduo C会自动分配一个线程去处理该连接,执行注册的业务逻辑函数来处理网络请求。 8. 释放资源:在程序结束时,需要调用相应的函数来释放使用的资源,如关闭服务器、销毁线程池等。 通过使用muduo C网络,我们可以简化多线程服务端编程的过程,提高服务器的并发处理能力。因为muduo C网络已经实现了底层的网络通信细节,我们只需要专注于编写业务逻辑代码,从而减少开发的工作量。同时,muduo C的多线程模型可以有效地提高服务器的并发性能,满足高并发网络服务的需求。 ### 回答3: Linux多线程服务端编程是指在Linux操作系统上开发多线程的服务器应用程序。使用muduo C网络有助于简化开发过程,提供高效的网络通信能力。 muduo C网络是一个基于Reactor模式的网络,适用于C++语言,由Douglas Schmidt的ACE网络演化而来。它提供了高度并发的网络编程能力,封装了许多底层细节,使得开发者能够更加专注于业务逻辑的实现。 在开发过程中,首先需要创建一个muduo C的EventLoop对象来管理事件循环。然后,可以利用TcpServer类来创建服务器并监听指定的端口。当有新的客户端请求到达时,muduo C会自动调用用户定义的回调函数处理请求。 在处理请求时,可以使用muduo C提供的ThreadPool来创建多个工作线程。这些工作线程将负责处理具体的业务逻辑。通过将工作任务分配给不同的线程,可以充分利用多核服务器的计算资源,提高服务器的处理能力。 在具体的业务逻辑中,可以使用muduo C提供的Buffer类来处理网络数据。Buffer类提供了高效的数据读写操作,可以方便地进行数据解析与封装。 此外,muduo C还提供了TimerQueue类来处理定时任务,可以用于实现定时事件的调度与管理。这对于一些需要定期执行的任务非常有用,如心跳检测、定时备份等。 总之,使用muduo C网络可以简化Linux多线程服务端编程的开发过程,提供高效的并发能力。通过合理地利用多线程和其他的相关组件,可以实现高性能、稳定可靠的网络服务端应用程序。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值