简体   繁体   English

设计具有低线程争用的多线程聊天服务器

[英]Design multi-threaded chat server with low thread contention

I was thinking how I would make a multi-threaded chat server with C++ in a some way that minimizes thread contention.我在考虑如何以某种方式使用 C++ 制作一个多线程聊天服务器,从而最大限度地减少线程争用。

In my initial design I have an std::vector of sockets in the server.在我的初始设计中,我在服务器中有一个 sockets 的std::vector When a client connects to the server, the socket is added to this vector of sockets.当客户端连接到服务器时,套接字被添加到 sockets 的这个向量中。

There is also an std::unordered_map<string, Socket*> that allows to look up the corresponding socket for a user name.还有一个std::unordered_map<string, Socket*>允许为用户名查找相应的套接字。 When the client logs in with it's user and password we add an entry to the hash map.当客户端使用其用户名和密码登录时,我们向 hash map 添加一个条目。 When the user logs out we delete the corresponding entry in the hash map.当用户注销时,我们删除 hash map 中的相应条目。

The client will send messages addressed to a user name.客户端将发送以用户名为地址的消息。 When they get to the server we use the hash map to look up the socket, and send the message though that socket.当他们到达服务器时,我们使用 hash map 查找套接字,并通过该套接字发送消息。

Since the server is multi-threaded, and the mentioned data structures can be read/written from different threads, we now need to guard them with some thread synchronization mechanism such as a mutex.由于服务器是多线程的,并且提到的数据结构可以从不同的线程读取/写入,我们现在需要使用一些线程同步机制来保护它们,例如互斥锁。 But I think doing so would kill performance because of thread contention.但我认为这样做会因为线程争用而降低性能。 Basically, all threads need to access these data structures in order to send messages, but only one of them can use them at the same time.基本上,所有线程都需要访问这些数据结构才能发送消息,但只有其中一个线程可以同时使用它们。 I think with this approach performance wouldn't be much better than doing it with a single thread.我认为使用这种方法的性能不会比使用单线程更好。

How can I improve my design for better performance?如何改进我的设计以获得更好的性能?

I think with this approach performance wouldn't be much better than doing it with a single thread.我认为使用这种方法的性能不会比使用单线程更好。

Not necessarily.不必要。 Since your map is a map of pointers , and not a map of objects Accessing the table is not the same thing as acessing the socket, and protecting the former does not mean that the later needs to be protected as well, even if it lives within the data structure.由于您的 map 是指针的 map,而不是对象的 map数据结构。

However, you will need to make sure that the lifetime of the object is safely handled.但是,您需要确保安全处理 object 的生命周期。 This is one of the cases where std::shared_ptr<> is your friend, as it guarantees thread safe ownership safety.这是std::shared_ptr<>是您的朋友的情况之一,因为它保证了线程安全的所有权安全。

For example:例如:

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);
  }
}

Obviously, Socket also needs to be have an internal mutex to handle contention when using the same socket concurently.显然,当同时使用同一个套接字时, Socket也需要有一个内部互斥体来处理争用。 However, if user1 sends a message to user2 while user3 sends a message to user4, the contention would be limited to the lookup within the map, while the rest of the operation would be concurent.但是,如果 user1 向 user2 发送消息,而 user3 向 user4 发送消息,则争用将仅限于 map 内的查找,而操作的 rest 将是并发的。

Since chat has high temporal correlation (because conversation) the logical answer is to cache results.由于聊天具有高度的时间相关性(因为对话),逻辑答案是缓存结果。 You need a weak ptr for invalidation.您需要一个弱 ptr 来进行失效。

The simple solution is to create a reference-counted message class and use message queues.简单的解决方案是创建一个引用计数消息 class 并使用消息队列。 If Alice wants to send a message to Bob and Charlie, you create an instance of the reference-counted message class, and then call a "queue message" function to queue instances of that same message to both Bob and Charlie.如果 Alice 想向 Bob 和 Charlie 发送消息,则创建引用计数消息 class 的实例,然后调用“队列消息”function 将同一消息的实例排队发送给 Bob 和 Charlie。

The "queue message" functions works as follows: “队列消息”功能的工作原理如下:

  1. Acquire the client map lock.获取客户端 map 锁。
  2. Find the client.找到客户。
  3. Lock the client send queue lock.锁定客户端发送队列锁。
  4. Release the client map lock.释放客户端 map 锁。
  5. Add the message to the client's send queue.将消息添加到客户端的发送队列。
  6. If the send queue was empty, call an async send function.如果发送队列为空,则调用异步发送 function。
  7. Release the client send queue lock.释放客户端发送队列锁。

The majority of the work your server does will be outside this "queue message" function entirely.您的服务器所做的大部分工作将完全不在此“队列消息” function 之外。 All of the sending, parsing, and receiving can take place without holding any locks at all.所有的发送、解析和接收都可以在没有任何锁的情况下进行。 When you receive a message, you can follow the same logic:当你收到一条消息时,你可以遵循同样的逻辑:

  1. Receive data.接收数据。
  2. Parse it into a message.将其解析为消息。
  3. Acquire the client's receive queue lock.获取客户端的接收队列锁。
  4. Put the message on the client's receive queue.将消息放在客户端的接收队列中。
  5. If the receive queue was empty, dispatch the client's message processing engine.如果接收队列为空,则调度客户端的消息处理引擎。
  6. Release the client's receive queue lock.释放客户端的接收队列锁。

The receive queue dispatch logic:接收队列调度逻辑:

  1. Acquire the client's receive queue lock.获取客户端的接收队列锁。
  2. If the queue is empty, release the lock and stop.如果队列为空,则释放锁并停止。
  3. Pull a message off the client's receive queue.从客户端的接收队列中拉出一条消息。
  4. Release the client's receive queue lock so newly-received messages can be queued.释放客户端的接收队列锁,以便新接收的消息可以排队。
  5. Process the received message.处理收到的消息。
  6. Go to step 1. Go 到步骤 1。

By the way, I was the primary developer of WebMaster's ConferenceRoom software.顺便说一句,我是 WebMaster 的 ConferenceRoom 软件的主要开发者。 So I've done this.所以我做了这个。 Handling ten thousand clients this way on hardware that's more than a decade old was no problem.在十多年前的硬件上以这种方式处理一万个客户是没有问题的。 Today, I would use boost to do much of the work for me.今天,我会使用 boost 为我完成大部分工作。

First and simple solution:第一个简单的解决方案:
If you have enough resources on the server or not many clients, I suggest to avoid most of the multi-thread complexity here and place all sending or receiving functionality to a single thread (one for send and another for receive operation).如果服务器上有足够的资源或客户端不多,我建议在这里避免大部分的多线程复杂性,并将所有发送或接收功能放在一个线程中(一个用于发送,另一个用于接收操作)。 So, threads have their working socket and only locks of send and receive clients' queues remain.因此,线程有它们的工作套接字,并且只保留发送和接收客户端队列的锁。 These locks can be handled by Producer/consumer pattern.这些锁可以由生产者/消费者模式处理。

More advanced but also more complex solution: You must use more optimized structures.更高级但也更复杂的解决方案:您必须使用更优化的结构。 Using "unordered_map" object makes your socket search mechanism very inefficient.使用“unordered_map” object 会使您的套接字搜索机制非常低效。 Also, you should not use exclusive locks everywhere that locks are needed, consider using nonexclusive locks anywhere possible as well.此外,您不应该在需要锁的任何地方使用排他锁,也可以考虑在任何可能的地方使用非排他锁。
Anyway, it is better to harness existing thread-safe and lock-free libraries.无论如何,最好利用现有的线程安全和无锁库。 You can find many of them online.你可以在网上找到很多。 I search one on Google for you:我在 Google 上为您搜索一个:
https://github.com/khizmax/libcds https://github.com/khizmax/libcds

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM