muduo网络库:线程之间的同步机制(使用eventfd函数,条件变量,线程池)

首先要从recipes-master/reactor/s03/test6开始说起。

///该程序用到了两种线程间同步方法
///一种是条件变量加互斥锁,一种是enventfd的wait/notify机制
#include "EventLoop.h"
#include "EventLoopThread.h"
#include <stdio.h>

void runInThread()
{
  printf("runInThread(): pid = %d, tid = %d\n",
         getpid(), muduo::CurrentThread::tid());
}

int main()
{
  // 主线程
  printf("main(): pid = %d, tid = %d\n",
         getpid(), muduo::CurrentThread::tid());


  // 创建EventLoopThread对象,并调用构造函数
  // 构造函数给对等线程bind一个threadFunc函数,注意此时loop_=NULL
  muduo::EventLoopThread loopThread;

  // 主线程将对等线程启动,
  // 在对等线程内部,startLoop启动线程,由于启动线程和运行线程需要一定的时间,所以 
  // 对等线程调用了con_.wait等待自己的线程启动完毕,然后返回它自己的enventloop地址给主线程,  
  // 此时对等线程调用loop()进入循环 
  //主线程得到一个对等线程的eventloop地址,但是此eventloop是属于对等线程的
  muduo::EventLoop* loop = loopThread.startLoop();

  // 主线程执行loop->runInLoop(),但是loop不属于主线程,属于对等线程
  // 添加进queueInloop,然后调用eventfd,notify对等线程,唤醒对等线程
  // 对等线程从loop中返回,执行runInThread函数
  // 主线程获得了对等线程的loop地址,就可以访问它里边的wakeupFd_(这里非常关键,虽然线程
  // 有自己独立的栈,但是如果获得其他线程的地址,也可以对他进行操作)
  loop->runInLoop(runInThread);
  sleep(1);
  // 2秒之后再执行runInThread
  loop->runAfter(2, runInThread);
  sleep(3);
  loop->quit();

  printf("exit main().\n");
}

就这一段简单的代码,困扰了我整整3天,我之前一直想不通线程之间是怎么通信的。今天好好地梳理了一下。

一、线程的内存模型

之前我写过一篇博客,从地址空间看进程和线程,里边介绍了线程的内存模型,用到这里,其实就是注意一点,线程是拥有自己“独立”的栈区的。“独立”是因为,如果你把这个栈区的地址暴露给其他线程,那么其他线程也还是可以访问的。这就给muduo网络库中使用eventfd来进行wait()/notify提供了可能。

回到代码中,那就是主线程在自己的栈区创建了一个loopThread的对象,这个对象可以启动一个对等线程(也就是所说的“子线程”)。

在这段代码中,简单来说就是对等线程启动了自己的线程,然后把自己在栈上的eventLoop给了主线程,主线程才有了给对等线程“发消息”的可能。
muduo::EventLoop* loop = loopThread.startLoop();

二、evetfd:线程(进程)通信的机制

这个函数原型是:

#include<sys/evetfd.h>
int eventfd(unsigned in initval, int flags)

这个函数会创建一个事件对象(eventfd object),用来实现进程(线程)之间的wait/notify机制。

要通信时,一个线程往这个eventfd object中写8个字节,相当于发送消息,然后另一个线程读到了这8个字节,相当于收到这个消息,然后就会进行相应的处理。这样就实现了wait/notify。

我们来看看runInLoop()函数是什么样的。
如果在本线程(也就是本loop, one loop per thread的思想)调用cb,就直接调用它,如果不在当前线程中调用cb, 就调用queueInLoop。

在test6中的代码loop->runInLoop(runInThread);
它的意思就是主线程执行loop->runInLoop(),但是loop不属于主线程,属于对等线程,主线程把它添加进queueInloop,然后调用eventfd,notify对等线程,唤醒对等线程。 对等线程从自己的loop中返回,执行runInThread函数。

void EventLoop::runInLoop(Functor&& cb)
{
  if (isInLoopThread())
  {
    cb();
  }
  else
  {
    queueInLoop(std::move(cb));
  }
}

queueInLoop函数如下,其中wakeup()函数就是封装了对eventfd进行的write操作。实现消息的发送。

void EventLoop::queueInLoop(Functor&& cb)
{
  {
  MutexLockGuard lock(mutex_);
  pendingFunctors_.push_back(std::move(cb));  // emplace_back
  }

  if (!isInLoopThread() || callingPendingFunctors_)
  {
    wakeup();
  }
}

wakeup()函数:

void EventLoop::wakeup()
{
  uint64_t one = 1;
  ssize_t n = sockets::write(wakeupFd_, &one, sizeof one);
  if (n != sizeof one)
  {
    LOG_ERROR << "EventLoop::wakeup() writes " << n << " bytes instead of 8";
  }
}
三、条件变量

EventLoopThread会启动自己的线程,并在其中运行loop, 并返回一个loop_的地址。因为线程启动和运行需要时间,所以这里是用了条件变量来等待线程创建好,线程函数运行起来,然后才return。

EventLoop* EventLoopThread::startLoop() 
{
  assert(!thread_.started());
  thread_.start();

  {
    MutexLockGuard lock(mutex_);
    while (loop_ == NULL)
    {
      cond_.wait();
    }
  }

  return loop_;
}

void EventLoopThread::threadFunc()
{
  EventLoop loop;

  // 如果用户设置了回调函数
  if (callback_)
  {
    callback_(&loop);
  }

  {
    MutexLockGuard lock(mutex_);
    loop_ = &loop;
    cond_.notify();
  }

  loop.loop();
  //assert(exiting_);
  loop_ = NULL;
}
四、EventThreadPool线程池

多线程TcpServer自己的eventLoop只用来接受新连接,而新连接用其他eventLoop来执行IO操作。
可以看一个例子:

void TcpServer::newConnection(int sockfd, const InetAddress& peerAddr)
{
  loop_->assertInLoopThread();
  EventLoop* ioLoop = threadPool_->getNextLoop();
  char buf[64];
  snprintf(buf, sizeof buf, "-%s#%d", ipPort_.c_str(), nextConnId_);
  ++nextConnId_;
  string connName = name_ + buf;

  LOG_INFO << "TcpServer::newConnection [" << name_
           << "] - new connection [" << connName
           << "] from " << peerAddr.toIpPort();
  InetAddress localAddr(sockets::getLocalAddr(sockfd));
  // FIXME poll with zero timeout to double confirm the new connection
  // FIXME use make_shared if necessary
  TcpConnectionPtr conn(new TcpConnection(ioLoop,
                                          connName,
                                          sockfd,
                                          localAddr,
                                          peerAddr));
  connections_[connName] = conn;
  conn->setConnectionCallback(connectionCallback_);
  conn->setMessageCallback(messageCallback_);
  conn->setWriteCompleteCallback(writeCompleteCallback_);
  conn->setCloseCallback(
      boost::bind(&TcpServer::removeConnection, this, _1)); // FIXME: unsafe
  ioLoop->runInLoop(boost::bind(&TcpConnection::connectEstablished, conn));
}


EventLoop* ioLoop = threadPool_->getNextLoop();
这里总是得到线程池中下一个ioloop的地址, 这是一种最简单的负载均衡的思想。


TcpConnectionPtr conn(new TcpConnection(ioLoop,
connName,
sockfd,
localAddr,
peerAddr));
这里创建了一个tcp连接对象,tcpServer得到它的地址。


ioLoop->runInLoop(boost::bind(&TcpConnection::connectEstablished, conn));
这里主线程让ioLoop所在的线程执行connectEstablised函数。

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值