STL和多线程

时间:2011-08-13 19:08:27

标签: c++ stl

我知道,当我在多个线程内的单个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(),这就是为什么我要在这里避免互斥锁定。这很贵。

我想知道这段代码是否会导致任何同步问题。

5 个答案:

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