我在想如何用C++制作一个多线程聊天服务器,以最大限度地减少线程争用。
在我最初的设计中,我在服务器中有一个套接字的std::vector
。当客户端连接到服务器时,套接字被添加到这个套接字向量中。
还有一个std::unordered_map<string, Socket*>
,它允许在相应的套接字中查找用户名。当客户端使用其用户和密码登录时,我们会在哈希图中添加一个条目。当用户注销时,我们会删除哈希图中的相应条目。
客户端将发送寻址到用户名的消息。当他们到达服务器时,我们使用哈希映射来查找套接字,并通过该套接字发送消息。
由于服务器是多线程的,并且上述数据结构可以从不同的线程读取/写入,因此我们现在需要使用一些线程同步机制(如互斥锁)来保护它们。但我认为这样做会因为线程争用而影响性能。基本上,所有线程都需要访问这些数据结构才能发送消息,但只有其中一个线程可以同时使用它们。我认为使用这种方法,性能不会比使用单个线程好多少。
如何改进我的设计以获得更好的性能?
第一个简单的解决方案:
如果服务器上有足够的资源或客户端不多,我建议避免这里的大部分多线程复杂性,并将所有发送或接收功能放在一个线程中(一个用于发送,另一个用于接收操作)。因此,线程有其工作套接字,并且只保留发送和接收客户端队列的锁。这些锁可以由生产者/消费者模式处理
更高级但也更复杂的解决方案:必须使用更优化的结构。使用"unordered_map"对象会使套接字搜索机制效率非常低。此外,您不应该在任何需要锁的地方使用独占锁,也可以考虑在任何可能的地方使用非独占锁
无论如何,最好利用现有的线程安全和无锁库。你可以在网上找到很多。我在谷歌上为你搜索了一个:
https://github.com/khizmax/libcds
我认为使用这种方法,性能不会比使用单个线程好多少。
不一定。由于映射是指针的映射,而不是对象的映射问表与访问套接字不是一回事,保护前者并不意味着后者也需要保护,即使它位于数据结构中。
但是,您需要确保安全地处理对象的生存期。这是std::shared_ptr<>
是您的朋友的情况之一,因为它保证了线程安全的所有权安全。
例如:
std::mutex table_mtx;
std::unordered_map<string, std::shared_ptr<Socket>> sockets;
void send(const std::string& msg, const std::string& dst_name) {
std::shared_ptr<Socket> dst;
{
std::lock_guard<std::mutex> lock(table_mtx);
// Increments the ref-count on the socket, so even if it's removed
// from the map, it won't be deleted until we are done with it.
dst = sockets.at(dst_name);
}
if(dst) {
dst->send(msg);
}
}
显然,Socket
还需要有一个内部互斥,以便在同时使用同一套接字时处理争用。然而,如果用户1向用户2发送消息,而用户3向用户4发送消息,则争用将限于映射内的查找,而操作的其余部分将是一致的。
简单的解决方案是创建一个引用计数的消息类并使用消息队列。如果Alice想向Bob和Charlie发送消息,那么您可以创建一个引用计数消息类的实例,然后调用"队列消息"函数将同一消息的实例排队给Bob和Charlie。
"队列消息"功能的工作原理如下:
- 获取客户端映射锁
- 找到客户
- 锁定客户端发送队列锁定
- 释放客户端映射锁定
- 将消息添加到客户端的发送队列中
- 如果发送队列为空,请调用异步发送函数
- 释放客户端发送队列锁定
服务器所做的大部分工作将完全在"队列消息"功能之外。所有的发送、解析和接收都可以在根本不持有任何锁的情况下进行。当你收到一条信息时,你可以遵循同样的逻辑:
- 接收数据
- 将其解析为消息
- 获取客户端的接收队列锁定
- 将消息放入客户端的接收队列中
- 如果接收队列为空,则调度客户端的消息处理引擎
- 释放客户端的接收队列锁定
接收队列调度逻辑:
- 获取客户端的接收队列锁定
- 如果队列为空,请释放锁并停止
- 从客户端的接收队列中提取一条消息
- 释放客户端的接收队列锁定,以便可以对新接收的消息进行排队
- 处理收到的消息
- 转至步骤1
顺便说一句,我是WebMaster ConferenceeRoom软件的主要开发人员。所以我做到了。在已有十多年历史的硬件上以这种方式处理一万个客户端是没有问题的。今天,我会用boost为我做很多工作。
由于聊天具有很高的时间相关性(因为对话),因此逻辑答案是缓存结果。你需要一个弱ptr来失效。