【问题标题】:std::deque and multi thread accessstd::deque 和多线程访问
【发布时间】:2016-06-15 07:21:54
【问题描述】:

请帮助我理解这一点。我是多线程编程和排队技术的新手。
我目前有 2 个线程在 same deque 上工作,如下所示:std::deque< VideoFrame > inputQueue。 (VideoFrame 是我的数据结构)。
每个线程都有指向该共享资源的指针:std::deque< VideoFrame > *p_inputQueue
我还使用了一个全局临界区:std::mutex mtxlock;

比方说,第一个线程的工作就是不断地将push_back的数据传给deque

mtxlock.lock();
p_inputQueue->push_back(frame);
mtxlock.unlock();

对于第二个线程,它有一个循环来访问队列的第一个元素,做一些计算(不改变值)然后pop_front。所以我想知道更好的方法是什么?
这个:

mtxlock.lock();
VideoFrame *pFrame = &p_inputQueue->front();    
//A very long computing process using pointer *pFrame
p_inputQueue->pop_front();
mtxlock.unlock();

或使用迭代器:

mtxlock.lock();
std::deque< VideoFrame >::iterator it = p_inputQueue->begin();
//A very long computing process using *it
p_inputQueue->pop_front();
mtxlock.unlock();

我还想知道我是否需要像这样锁定整个队列?


编辑: 到目前为止,感谢您提供的所有答案和 cmets。我只是想把事情说清楚一点。将元素从队列复制/移动到对象并不是一个便宜的过程,所以我考虑使用指针/迭代器。抱歉,如果没有更早编辑。

【问题讨论】:

    标签: c++ multithreading queue


    【解决方案1】:

    您绝对不想在处理期间持有锁。你有几个选择:

    1. 如果对象的复制成本很低,就复制它,调用pop_front,释放锁,然后处理复制。

    2. 如果您使用 C++11 并且对象移动起来很便宜,请移动它,调用 pop_front,释放锁,然后处理新对象。

    3. 使用deque&lt;shared_ptr&lt;VideoFrame&gt;&gt;。将shared_ptr 复制/移动到前面,调用pop_front,释放锁,然后使用shared_ptr 的副本处理对象。

    4. 使用unique_ptr 而不是shared_ptr。获取锁,将unique_ptr移出前面,调用pop_front,释放锁,然后使用移动到的unique_ptr处理对象。

    5. 使用deque&lt;VideoFrame*&gt; 并确保在完成后使用deleteVideoFrame

    【讨论】:

    • 所以你的意思是,如果我使用第 5 个选项,我真的不需要 lock 或为我的队列调用 pop_front,只需 delete
    • @TrungNguyen 你确实需要一个锁来保护对双端队列的并发访问,你确实需要调用pop_front 来从双端队列中删除指针。您必须避免让一个线程访问双端队列,而另一个线程可能正在修改它。所有这些方法的目标是确保锁的持有时间不会超过必要的时间。
    • 如果我使用deque&lt;VideoFrame*&gt;,当我调用VideoFrame* pFrame = p_inputQueue-&gt;front(); 之后调用p_inputQueue-&gt;pop_front(); 所以pFrame 在我调用delete pFrame; 之前仍然是一个有效指针?
    • @TrungNguyen 是的,pop_front 只是摆脱了双端队列上的指针。您不再需要它的价值,因为您拥有它的副本。摆脱常规指针对其指向的东西没有影响(除非它指向自己!)。
    【解决方案2】:

    我怀疑您的两种方法之间是否存在任何显着差异。

    我会推荐的一件事(如果您的程序逻辑允许的话)是在开始处理之前将元素从队列中取出并解锁队列。这样您就不会在过多的时间内阻塞另一个线程。显然,只有在处理逻辑除了队列访问本身之外没有其他潜在的竞争条件时,这才有可能:

    mtxlock.lock();
    
    //make a copy
    VideoFrame frame = p_inputQueue->front();    
    p_inputQueue->pop_front();
    mtxlock.unlock();
    
    //A very long computing process using object frame, after unlock
    

    如果VideoFrame 对象太重或无法复制,请考虑保留指向它们的指针队列(即std::deque&lt;VideoFrame*&gt; 甚至std::deque&lt;std::unique_ptr&lt;VideoFrame&gt;&gt;)。

    【讨论】:

    • 我要指出@Trung Nguyen 的两种方法都是错误,因为他使用指针/迭代器而不是制作副本,这在pop_front() 之后无效。虽然@Smeeheey 的回答正确地制作了副本。
    • @Mine - 他在完成指针/迭代器之后才会调用pop_front()
    • 谢谢,是的,在我的情况下,将元素复制到对象是一种不切实际的方法。这就是我尝试使用指针或迭代器的原因。对于时间锁定也是我的担忧之一。我真的很想获得使用多线程的优势,但是处理时间需要花费大量时间,即使我的程序也像单线程一样工作,因为第一个线程总是等待第二个线程完成它的工作。
    • 在这种情况下,我强烈建议您将设计切换为使用指针队列。这将解决您锁定整个处理的问题。
    【解决方案3】:

    在我看来,这两种方法都不太理想。我倾向于使用简单的指针而不是迭代器。但是,在 deque按值 存储您正在处理的对象时,使用迭代器/指针是有问题的。

    如果您将框架对象按值存储在 deque 中,这是否意味着它们复制起来很便宜?他们可以移动吗?如果是这样,我很想在处理之前将它们复制/移出 deque,以便您更快地释放锁。

    或者,您可以将 指针 存储到 deque 中的帧而不是值。

    无论哪种方式,您发布的方法的问题在于,如果在您锁定 mutex 之后但在解锁 之前发生异常,则它不是 异常安全双端队列 将保持锁定状态。它还会锁定 deque 以延长处理时间:

    // not exception safe
    mtxlock.lock();
    
    // Taking the address means locking the whole queue
    // until you have finished with the one element
    VideoFrame* pFrame = &p_inputQueue->front();
    
    //A very long computing process using pointer *pFrame
    p_inputQueue->pop_front();
    
    // not exception safe
    mtxlock.unlock();
    

    我倾向于从 deque 复制/移动对象并使用std::lock_guard 来确保异常安全(如果出现异常,它将自动释放锁)

    // ready to copy/move the object by value
    VideoFrame frame;
    
    { // start a new block for the lock
    
        // get a local automatic lock
        std::lock_guard<std::mutex> lock(mtxlock); 
    
        // copy/move the object out of the queue
        // so you can release the lock immediately
        frame = std::move(p_inputQueue->front());
        p_inputQueue->pop_front();
    
    } // lock is released here
    
    // Now it doesn't matter how long it takes
    // to process the object
    

    或者将 pointers 存储在 deque 中:

    // store pointers in the queue
    VideoFrame* pFrame;
    
    { // start a new block for the lock
    
        // get a local automatic lock
        std::lock_guard<std::mutex> lock(mtxlock);
    
        // copy the pointer out of the queue
        // so you can release the lock immediately
        pFrame = p_inputQueue->front();
        p_inputQueue->pop_front();
    
    } // lock is released here
    
    // Now it doesn't matter how long it takes
    // to process the object
    

    如果您决定将指针存储在 deque 中,那么我建议您使用 智能指针,例如 std::unique_ptr。这相当于您按 value 存储框架:

    // store unique pointers in the queue
    std::deque<std::unique_ptr<VideoFrame>>* p_inputQueue;
    
    // ...
    
    std::unique_ptr<VideoFrame> pFrame;
    
    { // start a new block for the lock
    
        // get a local automatic lock
        std::lock_guard<std::mutex> lock(mtxlock);
    
        // move the unique pointer out of the queue
        // so you can release the lock immediately
        pFrame = std::move(p_inputQueue->front());
        p_inputQueue->pop_front();
    
    } // lock is released here
    
    // Now it doesn't matter how long it takes
    // to process the object
    

    【讨论】:

      【解决方案4】:

      假设您实际上是按照预期的方式使用迭代器(迭代集合),您作为消费者的迭代器方法将锁定队列,直到队列中的每个元素都被处理(a)。这将防止生产者在发生这种情况时将新元素写入队列。

      在单一生产者、单一消费者的情况下,这可能是也可能不是问题,具体取决于生产和消费的相对速度。

      我的意思是,队列的使用通常是为了处理生产和消费解耦的情况,否则你可以直接从生产者调用消费者代码。

      例如,您可能会在生产端遇到周期性的高活动突发,这将导致许多商品进入队列,因为消费端跟不上。

      在这种情况下,队列被用作保持点,因此消费方可以在低活动生产时间赶上。

      在消耗队列上所有元素的过程中锁定队列基本上会在那个时候停止生产者,这意味着缓冲是无用的。


      如果您切换到多消费者范式,也会导致真正的问题。

      多个消费者的全部意义在于平衡多个消费者之间的负载,以提高整体吞吐量。如果每个消费者只是简单地抓取队列并处理其上的所有项目,则不会发生这种情况。

      在这种情况下的任何给定时间点,一次只有一个消费者是活跃的。


      (a) 如果您只是使用迭代器来获取第一个元素,那么您的锁可能会有更精细的分辨率。

      但是,这宁愿一开始就无法使用迭代器,所以我认为这不太可能。

      【讨论】:

      • “使用您的迭代器方法作为消费者将锁定队列,直到队列中的每个元素都已被处理” - OP 的代码 sn-p 中没有任何内容表明会出现这种情况。他锁定,处理单个元素,然后解锁。您在哪里看到在再次解锁之前消耗每个元素的循环?
      • @Smeeheey,事实上 OP 实际上是 使用 一个迭代器(不管发布的代码如何)。使用迭代器来获取队列的第一个元素没有什么意义,迭代器通常意味着......嗯......迭代一个集合:-)我会用这个假设澄清我的答案。
      • 他专门比较了使用指向前端元素的指针和使用指向前端元素的迭代器。没有什么可以超越前面的元素。
      • @Smeeheey,在这种情况下,使用迭代器完全是浪费时间。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2014-05-23
      • 2011-05-05
      • 2019-12-03
      • 1970-01-01
      • 2021-09-10
      • 2017-04-21
      • 1970-01-01
      相关资源
      最近更新 更多