【问题标题】:STL and multithreadSTL 和多线程
【发布时间】:2011-08-13 19:08:27
【问题描述】:

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

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

【问题讨论】:

  • 没有stl::mutexstl:deque -- 你指的是什么编译器/库/标准?
  • 没有捷径,这里需要加锁。
  • 请不要这样做if ( condition == false ),这只会损害可读性。

标签: c++ stl


【解决方案1】:

您需要的是参考稳定性。 IE。如果在容器被 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:

双端队列两端的插入会使所有迭代器失效 到双端队列,但对引用的有效性没有影响 双端队列的元素。

这是允许您在锁外挂在该参考上的关键。如果thread_1_2_3在中间开始插入或擦除,那么你就不能再挂这个引用了。

您不能以这种方式使用vector。但是您可以通过这种方式使用list。检查您要以这种方式使用的每个容器的参考稳定性。

【讨论】:

    【解决方案2】:

    我无法真正看穿您的问题或您的代码,但总的来说,标准 C++ 库中的容器为您提供了松散的保证,即不同元素的并发访问是线程安全的。但是,请务必了解其中的含义和限制:如果您有一个随机访问容器或元素的迭代器,并且您只使用它们来读取或更改元素值,那么只要您在不同的元素,结果应该是明确定义的。不好的是更改容器本身,因此必须对任何擦除或插入操作进行序列化(例如,通过锁定对整个容器的访问),并确保在执行此操作时了解容器的迭代器和引用失效规则。

    对于单个容器,您可能可以多说一些 - 例如,在基于树的容器中插入/擦除,在随机访问容器中间插入/擦除几乎肯定需要全局锁。在向量/双端队列中,您需要重新获取迭代器。在列表中,您可能可以在不同的位置同时执行插入操作。

    size()empty() 这样的全局操作也需要序列化。

    【讨论】:

      【解决方案3】:

      对于这个特定的例子,这是安全的

      int& data (int_queue.front ());
      

      你引用第一个元素,它可以被另一个线程移动,添加元素添加到队列中,强制它重新分配(deques 通常实现为“环绕”数组)。 如果您复制值而不是获取引用,则取决于您可能的实现方式。如果您希望能够做到这一点,则 std::deque 不会附带该规则的任何标准“例外”。当然可以编写一个类似于双端队列的数据结构,这样是安全的,但是双端队列不能保证这样写(也不可能这样写)。

      【讨论】:

      • 我知道他在哪里引用标准(无论如何是 N3242 草案),我显然弄错了。
      【解决方案4】:

      为什么要这样做?为什么消费者线程不提取锁内的对象,然后进行带外处理?

      假设您想要避免的是必须复制容器外部的对象,更简单更易于维护的方法可能是动态分配对象,使用(智能)指针容器和在锁内提取它(最低成本)。那么你就不再需要考虑线程安全问题了。

      请注意,即使您可以在这种特殊情况下实现这一点,您也不能使用多个消费者线程。我建议不要使用这种方法,而是找到一种不同的方法,在这种方法中你可以满足你的要求,而不会越过前沿。多线程很难做到正确,也很难调试甚至检测到存在问题。通过遵循常见模式,您可以使您的代码更易于推理和维护。

      【讨论】:

      • 我现在正在为 fas LAN(1 或 10 GbE)开发网络服务器。队列中的每个元素都是单个对象,表示在网络套接字上执行的任务。我所说的任务是指发送文件、文件的元数据、我的服务器的许多实例之间的一些同步信息等。例如,我在队列任务的末尾放置:“在 4 号套接字上发送大小为 3 GB 的文件 foo.zip”。消费者线程使用 epoll 轮询套接字,当套接字变为可写时,我访问队列中的第一个任务对象并在其中一个方法中执行 send() 系统调用。
      【解决方案5】:

      如果您确实想要一个无锁队列,我还建议您查看http://drdobbs.com/cpp/210604448?pgno=2

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2012-10-07
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多