首先要从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));
}
这里总是得到线程池中下一个ioloop的地址, 这是一种最简单的负载均衡的思想。
EventLoop* ioLoop = threadPool_->getNextLoop();
这里创建了一个tcp连接对象,tcpServer得到它的地址。
TcpConnectionPtr conn(new TcpConnection(ioLoop,
connName,
sockfd,
localAddr,
peerAddr));
这里主线程让ioLoop所在的线程执行connectEstablised函数。
ioLoop->runInLoop(boost::bind(&TcpConnection::connectEstablished, conn));