我知道,当我在多个线程内的单个STL容器上执行操作时,我需要使用互斥锁。但是,我想知道此规则是否有任何例外。请考虑我正在尝试实施的简化方案。
我有多个线程向容器添加元素,操作被互斥锁定/解锁包围。然后线程以某种方式通知(例如在linux上使用eventfd)单线程专用于调度此容器中的元素。我想要做的是访问容器中的第一个元素而不使用互斥锁。示例代码基于deque,但请注意我可以使用任何具有队列功能的容器:
std::mutex locker;
std:deque<int> int_queue;
int fd; // eventfd
eventfd_t buffer;
bool some_condition;
线程1,2,3等
locker.lock ();
int_queue.push_back (1);
locker.unlock ();
eventfd_write (fd, 1);
专用于调度元素的线程:
while (true)
{
bool some_condition (true);
locker.lock ();
if (int_quque.empty () == false)
{
locker.unlock ();
}
else
{
locker.unlock ();
eventfd_read (fd, &buffer);
}
while (some_condition)
{
int& data (int_queue.front ());
some_condition = some_operation (data); // [1]
}
locker.lock ();
int_queue.pop ();
locker.unlock ();
}
[1]我会多次对signle元素执行some_operation(),这就是为什么我要在这里避免互斥锁定。这很贵。
我想知道这段代码是否会导致任何同步问题。
答案 0 :(得分:6)
您需要的是参考稳定性。即如果容器是push_back'd时第一个元素的引用没有失效,那么你可以这样使用容器。即使这样,你也需要获得锁定前面元素的引用。
我对std::condition_variable
更熟悉事件通知,所以我会使用它:
#include <mutex>
#include <condition_variable>
#include <deque>
std::mutex locker;
std::deque<int> int_queue;
std::condition_variable cv;
void thread_1_2_3()
{
// use lock_guard instead of explicit lock/unlock
// for exception safety
std::lock_guard<std::mutex> lk(locker);
int_queue_.push_back(1);
cv.notify_one();
}
void dispatch()
{
while (true)
{
bool some_condition = true;
std::unique_lock<std::mutex> lk(locker);
while (int_queue.empty())
cv.wait(lk);
// get reference to front under lock
int& data = int_queue.front();
lk.unlock();
// now use the reference without worry
while (some_condition)
some_condition = some_operation(data);
lk.lock();
int_queue.pop_front();
}
}
23.3.3.4 [deque.modifiers]关于push_back
:
deque两端的插入使所有迭代器无效 对deque,但对引用的有效性没有影响 deque的元素。
这是允许您在锁外部挂起该引用的关键。如果thread_1_2_3
开始在中间插入或删除,那么您将无法再继续使用此引用。
您不能以这种方式使用vector
。但是你可以用这种方式list
。检查要使用这种方式的每个容器以获得参考稳定性。
答案 1 :(得分:3)
我无法真正看透您的问题或代码,但一般来说,标准C ++库中的容器为您提供了一个宽松的保证,即不同元素的并发访问是线程安全的。但是一定要了解它的含义和局限性:如果你有一个随机访问容器,或元素的迭代器,你只使用它们来读取或更改元素值,那么只要你在不同的元素,结果应该是明确的。什么是不正确的是更改容器本身,因此必须序列化任何擦除或插入操作(例如,通过锁定对整个容器的访问),并确保在执行此操作时了解容器的迭代器和引用失效规则。 / p>
对于单个容器,您可能会说更多 - 例如,在基于树的容器中插入/擦除,并且在随机访问容器的中间插入/擦除几乎肯定需要全局锁定。在vector / deque中,您需要重新获取迭代器。在列表中,您可能会在不同的位置同时执行插入操作。
任何全球性操作,例如size()
和empty()
也需要序列化。
答案 2 :(得分:0)
对于此特定示例,这是不安全
int& data (int_queue.front ());
你接受第一个元素的引用,它可以被另一个线程移动,添加元素添加到队列强制它重新分配(deques通常实现为“环绕”数组)。如果您复制该值而不是参考,则取决于您可能的实施方式。如果您希望能够执行此操作,则std :: deque不会带有此规则的任何标准“例外”。当然可以编写一个类似于deque的数据结构,这样可以安全,但是deque不能保证写得像(并且不像是这样写)。
答案 3 :(得分:0)
你为什么要这样做?为什么消费者线程不会在锁中提取对象,然后在带外处理它?
假设您要避免的是必须复制容器外部的对象,更简单易于维护的方法可以使用(智能)指针的容器动态分配对象,在锁内提取它(最低成本)。然后,您不再需要考虑线程安全问题。
请注意,即使您可以在此特定方案中将其拉出,也不能使用多个使用者线程。我建议不要采用这种方法,只需找到一种不同的方法,在这种方法中,您可以满足您的要求,而不会走在前沿。多线程很难正确,很难调试甚至检测到存在问题。通过遵循常见模式,您可以更轻松地推理和维护代码。
答案 4 :(得分:0)
如果您确实想要一个无锁的队列,我还建议您查看http://drdobbs.com/cpp/210604448?pgno=2