我有一个map<int, queue<int>>
,其中有一个线程写入其中,即将消息推送到队列中。它们的密钥是指client_id
,队列为客户端保存消息。我希望这个读写线程安全。
目前,写入其中的线程执行类似这样的操作
map<int, queue<int>> msg_map;
if (msg_map.find(client_id) != msg_map.end())
{
queue<int> dummy_queue;
dummy_queue.push(msg); //msg is an int
msg_map.insert(make_pair(client_id, dummy_queue);
}
else
{
msg_map[client_id].push(msg);
}
有很多客户从这张地图上阅读和删除。
if (msg_map.find(client_id) != msg_map.end())
{
if (!msg_map.find(client_id)->second.empty())
{
int msg_rxed = msg_map[client_id].front();
//processing message
msg_map[client_id].pop();
}
}
我在互斥体上阅读this(之前没有使用它们),我想知道何时何地应该锁定互斥锁。我的困惑在于他们正在访问各个队列(保存在同一个地图中)。我是否锁定队列或地图?
是否有标准/可接受的方式来执行此操作 - 并且使用互斥锁是最好的方法吗?有0个客户端线程,只有1个单一的写线程。
答案 0 :(得分:1)
现在我们不会关注互斥体,我们稍后会在清理代码时稍后处理它(这样会更容易)。
首先,从你展示的代码中似乎没有理由使用有序的std::map
(对数复杂度),你可以使用效率更高的std::unordered_map
(平均常数时间复杂度)。选择完全取决于您,如果您不需要订购容器,您只需更改其声明:
std::map<int, std::queue<int>> msg_map;
// or
std::unordered_map<int, std::queue<int>> msg_map; // C++11 only though
现在,地图在设计上非常有效,但如果你坚持为每个操作进行查找,那么你将失去地图的所有优势。
关于作者线程,所有您的代码块(对于编写者)可以被这一行有效地替换:
msg_map[client_id].push(msg);
请注意,operator[]
和std::map
的{{1}}定义为:
使用
std::unordered_map
作为键和默认构造的映射值,将新元素插入容器,并返回对新构造的映射值的引用。如果已存在具有key
键的元素,则不执行任何插入,并返回对其映射值的引用。
关于您的读者线程,您不能直接使用key
,因为如果特定operator[]
当前不存在,它会创建一个新条目,因此您需要缓存由{返回的迭代器{1}}为了重用它,从而避免无用的查找:
client_id
在处理之前我立即find
消息的原因是因为它会在我们添加互斥锁时提高并发性(我们可以更快地解锁互斥锁,这总是好的)。
@hmjd关于多个锁(一个用于地图,一个用于每个队列)的想法很有意思,但基于你向我们展示的代码我不同意:你从额外的并发中获得的任何好处很可能会被否定锁定队列互斥锁所需的额外时间(实际上,锁定互斥锁是一项非常昂贵的操作),更不用说您必须处理的额外代码复杂性。我会把我的钱押在一个互斥锁上(保护地图和所有队列一次)更有效率。
顺便说一句,如果你想使用效率更高的auto iter = msg_map.find(client_id);
// iter will be either std::map<int, std::queue<int>>::iterator
// or std::unordered_map<int, std::queue<int>>::iterator
if (iter != msg_map.end()) {
std::queue<int>& q = iter->second;
if (!q.empty()) {
int msg = q.front();
q.pop();
// process msg
}
}
(pop
不会遇到这个问题),单个互斥锁就可以解决迭代器失效问题。
假设C ++ 11,只需在地图上声明std::mutex
:
std::unordered_map
保护编写器线程非常简单,只需在访问映射之前锁定互斥锁:
std::map
保护读者线程几乎没有任何困难,唯一的技巧是你可能想要解除互斥锁ASAP以提高并发性,因此你必须使用std::unique_lock
(可以提前解锁) )而不是std::lock_guard
(只有当它超出范围时才能解锁):
std::mutex msg_map_mutex;
std::map<int, std::queue<int>> msg_map; // or std::unordered_map
如果您不能使用C ++ 11,则必须替换std::lock_guard<std::mutex> lock(msg_map_mutex);
// the lock is held while the lock_guard object stays in scope
msg_map[client_id].push(msg);
等。无论你的平台提供什么(pthreads,Win32,...)或boost等价物(它具有与新的C ++ 11类一样易于使用和易于使用的优点,不同于平台 - 具体原语)。
答案 1 :(得分:0)
在修改两个结构时,对map
和queue
需要同步的读写访问权限,包括map
:
map<int, queue<int>> msg_map;
if (msg_map.find(client_id) != msg_map.end())
{
queue<int> dummy_queue;
dummy_queue.push(msg); //msg is an int
msg_map.insert(make_pair(client_id, dummy_queue);
}
else
{
msg_map[client_id].push(msg); // Modified here.
}
mutex
有两个选项可锁定map
和queue
,或者map
拥有互斥锁,mutex
拥有queue
{{1}} }。第二种方法是可取的,因为它减少了单个锁保持的时间长度,并且意味着多个线程可以同时更新多个队列。