Replies: 15 comments 1 reply
-
3ks, 我在尝试用c 写个一类似muduo的库,问题 1给了我启发,如何处理多个事件. 我感觉muduo设计的初衷就是: connptr 可以跨线程使用.(我用c 处理这个问题感觉挺难) .
具体的解释 还是等 硕神 回复吧.
王正勇
[email protected]
|
Beta Was this translation helpful? Give feedback.
-
可以跨线程,也可以只在处理这个 conn 的线程保存吧 |
Beta Was this translation helpful? Give feedback.
-
看看muduo的TcpServer的析构函数,我觉得你这两个问题就都解决了。 |
Beta Was this translation helpful? Give feedback.
-
这个和 TcpServer 的析构函数有什么关系呢?我现在关心的是一次事件循环中逻辑处理,貌似和 TcpServer析构没关系吧,对于服务端,TcpServer 这种类的析构函数,说实话我觉得完全不需要写都,错了对了都没什么关系 |
Beta Was this translation helpful? Give feedback.
-
你那个a.xxx b.xxx 原因就是因为TcpServer析构要用,谁跟你说一次事件循环了。 TcpServer的析构写不写你觉得无所谓,错了对了,你觉得没关系。 然而muduo是正确严谨的,作者觉得有必要,难道有问题么? |
Beta Was this translation helpful? Give feedback.
-
如果不在 mainthread 保存这个 conn_map,那么 TcpServer 的析构就不需要做这些额外的事情。我觉得析构要处理这个 map 并不是需要保存这个 map 的原因,我的问题是为什么要保存这个 map,你说析构需要处理,是保存 map 的果,我求的是因。 |
Beta Was this translation helpful? Give feedback.
-
如果不在TcpServer中保存所有连接,TcpServer析构的时候,怎么确保所有连接被断开。 保存这个map就是为了正确的处理连接。 |
Beta Was this translation helpful? Give feedback.
-
在多线程模式下,每个 conn 在链接建立后都有其对应的 loop,既然一个线程在建立链接后,仅由一个 loop 负责处理,如果只在那个处理这个 conn 的 loop 中保存这个 conn(也可以使用一个 conn_map) ,而不是在 main thread 中保存一个 conn_map。这样,除非客户端代码保存了一份这个 connptr, 那么这个 conn 的所有操作都是 race free 的,因为只在一个线程处理它。这样析构这个 conn 的时候,不需要 conn 所属线程以及 main thread 交互,只需要在 conn 所属的线程即可安全的析构(在客户端代码没有保存 connptr 的情况),节省了一次 runInLoop。当然这样要求额外引入一层 worker thread + master thread 的抽象(类似于 memcahed),但是却可以降低析构的成本。 |
Beta Was this translation helpful? Give feedback.
-
我明白你的意思,我是说,如果按照你这么做,TcpServer的析构函数怎么写。 按照你的说法,~TcpServer需要通知所有的Loop:“我要析构了,你们赶快断开所有连接。”,那么,这里怎么通知呢? TcpServer如果没有持有这些连接,它怎么得知现在析构Loop所在的线程是安全的。 muduo将connectDestroyed扔到连接所在Loop的dopending队列中,LoopThread析构前会将队列中任务处理完。 这样connection变动的回调也能被正常执行,连接也能正常退出。 还有,你的意思是handleClose顺带connectDestroyed的功能,一次执行不需要放入dopending队列么? 我觉得,removeConnection后面将connectDestroyed放入dopending队列中也是有意义的。 总之,就算TcpServer不保存conn的map,也需要将connectDestroyed放入队列中执行。 |
Beta Was this translation helpful? Give feedback.
-
void EventLoop::runInLoop(Functor&& cb)
{
if (isInLoopThread())
{
cb();
}
else
{
queueInLoop(std::move(cb));
}
} 你所谓的一次执行一定要放在 pending 队列的说法我觉得并不成立。 |
Beta Was this translation helpful? Give feedback.
-
connection 不会在 event handling 阶段析构正是优点所在,因此用户完全不用担心在事件处理函数中访问到失效的对象,大大减轻了编码的心智负担。 另外,我不认为 nginx 的做法有多大的意义。关键在于“一次 loop 返回的多个事件”的情况,两个独立事件在同一次 loop 中发生是偶然的(socket A 和 socket B 几乎同时变得可读,那它们可能在一次 loop 中处理,也可能在两次 loop 中处理),事件的处理顺序也是没有保证的,而且本来也应该避免不同 socket 上的事件有先后依赖关系。有可能 A 事件想让 B 事件失效,但是 B 事件在这次 loop 中已经先于 A 事件处理过了,那“使失效”就无效了。muduo TcpServer 会按 round-robin 方式将 sockets 分配到线程上,因此不同线程的事件也不可能在一次 loop 中相互影响,支持“事件失效”实在没有多大价值。 |
Beta Was this translation helpful? Give feedback.
-
本尊回复,太开心了! 硕神说的 muduo 没有必要实现这个优化,我觉得确实也有道理。nginx 是一个代理服务器,这种事件失效在它的逻辑中确实有其必要(比如一个请求失效后,其所对应的 upstream 链接也就应该失效不再处理,防止浪费资源)。而 muduo 是一个网络库,这种业务层的需求确实没有必须在网络库里面实现的理由。 但是硕神,关于 conn 不会在 eventhandling 阶段析构,这个本身已经由
有了这个 guard,可以保证当前的 conn 在全部的回调调用完前是不会被析构的。 还有就是想求教一下硕神,为什么 handleClose 里面不直接 remove channel 呢? |
Beta Was this translation helpful? Give feedback.
-
Channel::handleEvent 只能保证这个 Channel 所属的 TcpConnection 不会在这个 handleEvent 函数内析构,不能防止在 handleEvent 期间它去析构别的 TcpConnection。换句话说,A、B 两个 TcpConnection 在一次 loop 中触发了事件,那么在处理 A 的事件的时候它有可能主动关闭 B 连接,那么 muduo 库要做的(并且目前的代码已经做到的)是让 B 的析构发生在这次 loop 之后(这正是为什么要 queueInLoop 而不是 runInLoop),否则 Channel B 会变成 dangling pointer,channel B 的 handleEvent() 直接会 core dump。channel B 的 tie_.lock() 也起不了作用了,因为 Channel 对象本身已经析构了。 至于 handleClose() 里为什么不直接 remove channel,你可以把你的想法用代码具体完整实现出来,我们再来看它到底可行不可行。 |
Beta Was this translation helpful? Give feedback.
-
我写了一下,但是结合硕神你的回答,我理解了,分成两个函数,通过 enqueueLoop 去做,其实就是通过拷贝一个 connptr,让 conn 的析构延迟,防止硕神你上面说的 dangling poninter 问题。我之前只想到了析构这个 conn,忘记了它内部的那个 channel 此时可能有一个 poniter 在 poller 那里。所以直接像我之前说的合成一个函数,去掉 queueInLoop 是不可行的。 那我这么理解对吗:其实这个 queueInLoop 的函数到底是什么并不重要,只要绑定一个 connptr 保证析构的延迟即可,可以在 handleClose 里面直接 void TcpConnection::handleClose()
{
loop_->assertInLoopThread();
LOG_TRACE << "fd = " << channel_->fd() << " state = " << stateToString();
assert(state_ == kConnected || state_ == kDisconnecting);
// we don't close fd, leave it to dtor, so we can find leaks easily.
setState(kDisconnected);
channel_->disableAll();
TcpConnectionPtr guardThis(shared_from_this());
connectionCallback_(guardThis);
// must be the last line
closeCallback_(guardThis);
channel_->remove();
loop_->queueInLoop([guardThis]() {});
}
void TcpServer::removeConnectionInLoop(const TcpConnectionPtr& conn)
{
loop_->assertInLoopThread();
LOG_INFO << "TcpServer::removeConnectionInLoop [" << name_
<< "] - connection " << conn->name();
size_t n = connections_.erase(conn->name());
(void)n;
assert(n == 1);
} 上面的代码应该也可以解决硕神你说的那个问题吧,我改了以后稍微跑了下代码测试,没有看出什么问题,希望硕神指点一下。 另外硕神,可以谈一下我上面说的由单个 ioloop 保存各自处理的 connmap,而不是存储一份全局的 connmap,这样减少析构 conn 时候的 race condition 和 成本的这个问题吗? |
Beta Was this translation helpful? Give feedback.
-
你想要得到什么? |
Beta Was this translation helpful? Give feedback.
-
硕神你好,你的linux多线程服务端编程我拜读过了,你的网课也上过,muduo 也看了两遍,对我帮助非常大,很感谢你的的分享。
最近在读 nginx,两者都为事件驱动,不禁做了些比较,也有一些疑惑,想请教一下:
1.nginx 对于一次 loop 返回的多个事件,前面先处理的事件可能会导致后面事件的失效(比如第一个处理的事件在某些条件下关闭了这次返回的事件列表中排在第二位的事件对应的链接),其通过 event 中的 instance 字段基本实现了可以跳过这种失效的事件的情况,但是貌似在 muduo 中做不到,本来我以为 muduo 的 channel.tie 方法可以做到,但是仔细看了代码加上试验后发现并不能组织在这次 loop 中处理后面失效事件的问题(即使是使用单线程模型也是,因为 removeConnection 中调用了 queueInLoop,所以一个 connptr 的析构是不可能发生在一次 loop 的 eventhandling 阶段的),这个是否可以作为一个改进点呢?
2.这个也是由前面的问题引出的,muduo 中 mainthread(listen thread)保存了一份 conn_map,所以在多线程模式下, connptr 的处理显得很复杂,需要先在 conn's ioloop 中关闭对读写事件的关注,然后通过回调,在 mainthread erase map 中的数据,最后再通过回调,在 conn's ioloop 将 channel 从 eventloop 中移除掉。我主要不解的是
a.为什么一定要在 mainthread 保存一份 conn_map 呢,如果 conn 也仅有其所属的 eventloop 去处理,那么就更容易避免多线程 race 了?
b.handleClose 中先 disable 了全部事件,但是没有 remove channel,而是在 mainthread erase 之后在通过回调做这个事情,这两个步骤拆开的原因是?
Beta Was this translation helpful? Give feedback.
All reactions