C++ STL与多线程
我知道,当我在多个线程中对单个STL容器执行操作时,我需要使用互斥体。但是我想知道这条规则是否有例外。请考虑我正在尝试实现的简化场景。 我有多个线程将元素添加到容器中,操作被互斥锁/解锁包围。然后线程以某种方式通知(例如,在linux上使用eventfd)专用于调度此容器中元素的单个线程。我想做的是访问容器中的第一个元素,而不使用互斥体。基于deque的示例代码,但请注意,我可以使用任何具有队列功能的容器:C++ STL与多线程,c++,stl,C++,Stl,我知道,当我在多个线程中对单个STL容器执行操作时,我需要使用互斥体。但是我想知道这条规则是否有例外。请考虑我正在尝试实现的简化场景。 我有多个线程将元素添加到容器中,操作被互斥锁/解锁包围。然后线程以某种方式通知(例如,在linux上使用eventfd)专用于调度此容器中元素的单个线程。我想做的是访问容器中的第一个元素,而不使用互斥体。基于deque的示例代码,但请注意,我可以使用任何具有队列功能的容器: std::mutex locker; std:deque<int>
std::mutex locker;
std:deque<int> int_queue;
int fd; // eventfd
eventfd_t buffer;
bool some_condition;
专用于分派元素的线程:
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] 我将多次对Single元素执行一些_操作(),这就是为什么我希望在这里避免互斥锁。太贵了
我想知道这个代码是否会导致任何同步问题或什么。
< P>我不能真正地看到你的问题或代码,但是一般来说,标准C++库中的容器为你提供了一个松散的保证,即在不同元素上的并发访问是线程安全的。但是,一定要理解其含义和局限性:如果您有一个随机访问容器或元素的迭代器,并且您只使用这些容器或迭代器来读取或更改元素值,那么只要您在不同的元素上这样做,结果应该是定义良好的。不好的是更改容器本身,因此必须序列化任何擦除或插入操作(例如,通过锁定对整个容器的访问),并确保在执行此操作时了解容器的迭代器和引用无效规则 对于单个容器,您可能能够说得更多一些——例如,在基于树的容器中插入/擦除,并且在随机访问容器的中间插入/擦除几乎肯定需要全局锁。在vector/deque中,您需要重新获取迭代器。在列表中,您可以在不同的位置同时执行插入任何全局操作,如
size()
和empty()
也需要序列化。对于此特定示例,这是不安全的
当引用第一个元素时,它可能会被另一个线程移动,该线程将元素添加到队列中,迫使它重新分配(DEQUE通常被实现为“环绕”数组)。如果复制值而不是引用,则根据实现的不同,您可能会侥幸逃脱。如果您想做到这一点,std::deque不会附带任何标准的“例外”。当然,在安全的情况下,可以编写类似于deque的数据结构,但不能保证deque的编写方式是那样的(也不一定是那样写的)。您需要的是引用稳定性。也就是说,当容器被推回时,如果对第一个元素的引用没有失效,您可以这样使用容器。即使这样,您也需要获得锁下的front元素的引用
我更熟悉事件通知的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();
}
}
#包括
#包括
#包括
互斥锁锁;
std::deque int_队列;
std::条件变量cv;
无效线程_1_2_3()
{
//使用锁定/解锁保护,而不是显式锁定/解锁
//例外安全
标准:锁紧装置lk(储物柜);
内部队列向后推(1);
cv.通知_one();
}
无效分派()
{
while(true)
{
bool some_condition=true;
标准:唯一锁lk(锁柜);
while(int_queue.empty())
cv.等待(lk);
//在锁定状态下获取前参考
int&data=int_queue.front();
lk.unlock();
//现在不用担心就使用引用
while(某些情况)
某些条件=某些操作(数据);
lk.lock();
int_queue.pop_front();
}
}
23.3.3.4[deque.modifiers]是关于推回的:
deque两端的插入将使所有迭代器无效
但不影响引用的有效性
德克的元素
这是允许您在锁外保持引用的关键。如果<代码> thRead 1x2y3在中间开始插入或擦除,则不能再挂在该引用上。
不能以这种方式使用向量。但是你可以这样使用列表。检查每个容器的参考稳定性。为什么要这样做?为什么使用者线程不提取锁内的对象,然后在带外处理它
假设您想要避免的是必须将对象复制到容器之外,一种更简单、更易于维护的方法可以是动态分配对象,使用一个包含(智能)指针的容器并在锁中提取它(成本最低)。那么你就不再需要考虑线程安全问题了。
请注意,即使您可以在这个特定场景中实现这一点,也不能使用多个使用者线程。我建议您不要使用这种方法,而只需找到一种不同的方法,在这种方法中,您可以满足您的需求,而不必走到最前沿。多线程很难正确执行,而且很难调试甚至检测到存在问题。通过遵循常见模式,您的代码更易于推理和维护。如果您确实想要无锁队列,我还建议您查看没有stl::mutex
或stl:deque
——您指的是什么编译器/库/标准?这里没有快捷方式,需要锁定。如果需要,请不要执行(condition==false)
这只会影响可读性。我知道他引用了标准中的内容(不管怎样,是N3242草案),显然我弄错了。我正在研究
int& data (int_queue.front ());
#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();
}
}