【问题标题】:Efficient way to lock std::list锁定 std::list 的有效方法
【发布时间】:2014-07-15 19:03:35
【问题描述】:

我知道std::list 不是线程安全的。在我的应用程序线程中,不断将元素添加到全局列表中。另一个线程从列表中获取元素并一个一个地处理它们。但是,我不希望处理线程在处理完成时一直锁定列表。

所以处理线程锁定列表,获取元素,解锁列表并处理元素。在处理过程中,其他线程不断向列表中添加元素。一旦处理结束,处理线程再次锁定列表删除已处理元素并解锁它。

以下是伪代码:

std::list<int> mylist ; /* Global list of integers */

void add_thread(int element) /* Threads adding element to the list */
{
   write_lock();
   mylist.push_back(element);
   write_unlock();

   return;
}

void list_processing_thread() /* Processes elements from the list */
{
    for (std::list<int>::iterator it=mylist.begin(); it!=mylist.end(); ++it)
    {
        read_lock();
        int element = *it;
        read_unlock();

        process_element(element);

        write_lock();
        mylist.remove(element);
        write_unlock();
    }

    return;
}

这是正确的方法(以有效的方式处理列表元素)吗?会不会有什么麻烦?

【问题讨论】:

  • 你的场景是多个生产者,一个消费者,对吗?
  • @Maxim 是的,这是正确的。

标签: c++ multithreading list


【解决方案1】:

所以处理线程锁定列表,获取元素,解锁列表并处理元素。在处理过程中,其他线程不断向列表中添加元素。一旦处理结束,处理线程再次锁定列表删除已处理元素并解锁它。

生产者线程相互竞争,并与消费者线程竞争访问列表。

您可以通过为每个生产者提供自己的队列(std::list + std::mutex 或自旋锁)来消除生产者之间的争用。这样:

  1. 生产者将项目发布到自己的队列中。创建一个元素的临时std::list,然后锁定互斥体,将该元素拼接到队列中,解锁互斥体。
  2. 当消费者准备好时,它会一一锁定那些生产者队列,并将元素拼接到自己的队列中。拼接std::list是O(1)运算。
  3. 如果需要排序,每个元素都应该有一个时间戳,以便消费者可以按时间对其合并队列中的所有元素进行排序。

上述方法还使您的关键部分非常短,因为在互斥锁锁定时您所做的只是std::list 拼接,这只是一些指针修改。

或者,只需使用英特尔® 线程构建模块中的concurrent_bounded_queue 类。

【讨论】:

    【解决方案2】:

    不,这不安全。是的,它会带来麻烦。 详细说明: 应该只有一个锁,即写锁。读者不需要互相辩护。 在访问开始和结束迭代器之前,您需要在处理线程中获得锁,因为它们可能被写入线程破坏。获得锁后,复制列表并释放锁。完成处理后,获取锁,并编辑原始列表。 祝你好运

    【讨论】:

    • 读锁,因为线程同时读取写入器可以等待...?
    • 是的,安德鲁,读者不需要在作者写的时候读,所以这里正确的锁可能是读/写锁,它允许多个读者,但在作者拥有 (独占)写锁。
    • 在这种情况下,似乎有多个作家和一个读者;)
    【解决方案3】:

    以下是您的问题的可能解决方案,在 C++11 中,但您可以轻松地将其移植到 C++-03 + boost 或您拥有的任何线程库:

    std::deque<int> queue;
    std::mutex mtx;
    std::condition_variable ready;
    
    void producer()
    {
      while (true)
      {
        int element = produce();
    
        std::unique_lock<std::mutex> lock(mtx);
        queue.push_back(element);
        lock.unlock(); 
        ready.notify_one();
      }
    }
    
    void consumer()
    {
      while (true)
      {
        std::unique_lock<std::mutex> lock(mtx);
        ready.wait(lock, [](){ ! queue.empty() });
        int element = queue.front();
        queue.pop_front();
        lock.unlock();
    
        consume(element);
      }
    }
    

    【讨论】:

    • 这是一个有点幼稚的实现。 queue.push_back(element); 在互斥锁被锁定时分配内存 - 可扩展性差。这就是为什么 std::list 在这种情况下更好的原因,因为您可以分配一个新元素,然后锁定互斥体并将其拼接到目标列表中,从而使您的临界区非常快 - 仅 4 个指针更新。
    【解决方案4】:

    你可以用不同的性能水平做很多事情,但有两个非常简单:

    • 在处理线程中使用std::list::swap 将队列与空队列交换,这样,如果锁争用会减慢速度,至少您将一次性获得所有积压的项目,从而最大限度地减少伤害

    • 使用单写/多读锁(例如posix_rwlock_rdlock 等)

    【讨论】:

      【解决方案5】:

      首先,这个方法很好,你应该锁定一小段时间,获取当前项目,解锁,并在列表解锁并且可以访问以供其他线程使用时处理项目。

      但是,我建议您在列表未锁定时不要依赖列表迭代器。迭代器是偷偷摸摸的,许多数据结构在添加/删除项目时可能会损坏迭代器。

      我建议您使用以下方法:

      while(list.empty() == false)
      {
        lock();
        int element = list.front();
        list.pop_front();
        unlock();
      
        process(element);
      }
      

      最好的!

      【讨论】:

      • 谢谢。但是list.empty() 即使在将元素添加到list 时也可以工作?或者还需要锁定那里?
      • 是的,你需要锁才能调用list.empty()。此外,如果列表为空,您需要一些条件变量来等待(生产者比消费者慢会导致消费者旋转)。
      • 当我们只想在处理成功时删除元素时,保持检查list.empty() 变得有点棘手:) 而且我们也不能像您在回答中提到的那样使用迭代器。
      猜你喜欢
      • 2012-05-27
      • 2013-06-06
      • 1970-01-01
      • 1970-01-01
      • 2017-06-21
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2012-04-07
      相关资源
      最近更新 更多