东阳的学习笔记
多线程的 TcpServer,用到了 EventLoopThreadPoll class。
一、EventLoopThreadPoll
用 one loop per thread 的思想实现多线程 TcpServer 的关键步骤是在新建 TcpConnection 时从 event loop poll
里挑选一个 loop 给 TcpConnection 用。也就是说:
- 多线程 TcpServer 自己的(
与TcpConnection共享的
) EventLoop 只用来接受新连接,而新连接会用其他(来自线程池
) EventLoop 来执行IO。 - 而单线程 TcpServer 的 EventLoop 是与 TcpConnection 共享的.
muduo 的 event loop tool 由 EventLoopThreadPool class 表示,接口如下:
class EventLoopThreadPool : boost::noncopyable
{
public:
EventLoopThreadPool(EventLoop* baseLoop);
~EventLoopThreadPool();
void setThreadNum(int numThreads) { numThreads_ = numThreads; }
void start();
EventLoop* getNextLoop();
private:
EventLoop* baseLoop_;
bool started_;
int numThreads_;
int next_; // always in loop thread
boost::ptr_vector<EventLoopThread> threads_;
std::vector<EventLoop*> loops_;
};
二、TcpServer 每次新建一个 TcpConnection
TcpServer 每次新建一个 TcpConnection 就会调用 getNextLoop() 来取得 EventLoop,如果是单线程服务,每次返回的都是 baseLoop_,即 TcpServer 自己用的那个 loop_。
- 其中 setThreadNum() 的参数的意义见 TcpServer 代码注释。
/// Set the number of threads for handling input.
///
/// Always accepts new connection in loop's thread.
/// Must be called before @c start
/// @param numThreads
/// - 0 means all I/O in loop's thread, no thread will created.
/// this is the default value.
/// - 1 means all I/O in another thread.
/// - N means a thread pool with N threads, new connections
/// are assigned on a round-robin basis.
void setThreadNum(int numThreads);
TcpServer 只用增加一个成员函数和一个成员变量。
private:
/// Not thread safe, but in loop
void newConnection(int sockfd, const InetAddress& peerAddr);
+ /// Thread safe.
void removeConnection(const TcpConnectionPtr& conn);
+ /// Not thread safe, but in loop
+ void removeConnectionInLoop(const TcpConnectionPtr& conn);
typedef std::map<std::string, TcpConnectionPtr> ConnectionMap;
EventLoop* loop_; // the acceptor loop
const std::string name_;
boost::scoped_ptr<Acceptor> acceptor_; // avoid revealing Acceptor
+ boost::scoped_ptr<EventLoopThreadPool> threadPool_;
2.1 TcpServer::newConnection()
多线程 TcpServer 的改动很简单,新建连接只改了 3 行代码。
- 单线程时是把自己用的 loop_ 传给 TcpConnection;
- 多线程是每次从 EventLoopThreadPool 取得 ioLoop
void TcpServer::newConnection(int sockfd, const InetAddress& peerAddr)
{
loop_->assertInLoopThread();
char buf[32];
snprintf(buf, sizeof buf, "#%d", nextConnId_);
++nextConnId_;
std::string connName = name_ + buf;
LOG_INFO << "TcpServer::newConnection [" << name_
<< "] - new connection [" << connName
<< "] from " << peerAddr.toHostPort();
InetAddress localAddr(sockets::getLocalAddr(sockfd));
// FIXME poll with zero timeout to double confirm the new connection
+ EventLoop* ioLoop = threadPool_->getNextLoop(); // 每次从线程池中获取线程,loops_[next_]
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));
}
2.2 TcpServer::removeConnection()
多线程的连接的销毁也并不复杂,把原来的 removeConnection() 拆为两个函数,因为 TcpConnection 会在自己的 ioLoop_ 中调用 removeConnection(),所以需要把他移到 TcpServer
的 loop_ 线程(因为TcpServer 是无锁的
)
- &14再次把 connectDestroyed() 移到 TcpConnection 的 ioLoop_ 线程进行,是为了保证 TcpConnection 的 ConnectionCallback 始终在其 ioLoop调用,方便客户端的编写
void TcpServer::removeConnection(const TcpConnectionPtr& conn)
{
+ // FIXME: unsafe
+ loop_->runInLoop(boost::bind(&TcpServer::removeConnectionInLoop, this, +conn));
}
void TcpServer::removeConnectionInLoop(const TcpConnectionPtr& conn)
+{
loop_->assertInLoopThread();
! LOG_INFO << "TcpServer::removeConnectionInLoop [" << name_
<< "] - connection " << conn->name();
size_t n = connections_.erase(conn->name());
assert(n == 1); (void)n;
+ EventLoop* ioLoop = conn->getLoop();
! ioLoop->queueInLoop(
boost::bind(&TcpConnection::connectDestroyed, conn));
}
总而言之、TcpServer 和 TcpConnection 的代码都只处理单线程的情况(甚至都没有 mutex ),而借助 EventLoop::runInLoop() 并引入 EventLoopThreadPool让多线程 TcpServer 的实现易如反掌。
- 注意:ioLoop 和 loop_ 间的线程切换都发生再连接建立和断开的时刻,不影响正常业务的性能。
三、调度方法
muduo目前采用最简单的 round-robin 算法来选取 pool 中的 EventLoop。
轮询调度(Round Robin Scheduling)算法就是以轮询的方式依次将请求调度不同的服务器,即每次调度执行i = (i + 1) mod n,并选出第i台服务器。算法的优点是其简洁性,它无需记录当前所有连接的状态,所以它是一种无状态调度。
- 不允许 TcpConnection 在运行中更换 EventLoop,这对长连接和短连接服务都是适用的,不易造成偏载。
3.1 扩展:pool 共享
muduo 目前的设计是每个TcpServer 都有自己的 pool,不同Tcpserver之间不共享。
- 可以让多个 TcpServer 共享一个 EventLoopThreadPool.
- 另外一种可能是一个 EventLoop aLoop 供两个 TcpServer 使用(a 和 b),其中 a 是单线程服务程序。