Mutex在写入地图中保存的队列时为了线程安全

时间:2013-04-23 09:41:04

标签: c++ stl thread-safety mutex

我有一个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个单一的写线程。

2 个答案:

答案 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)

在修改两个结构时,对mapqueue需要同步的读写访问权限,包括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有两个选项可锁定mapqueue,或者map拥有互斥锁,mutex拥有queue {{1}} }。第二种方法是可取的,因为它减少了单个锁保持的时间长度,并且意味着多个线程可以同时更新多个队列。