具有细粒度锁的线程安全链表

时间:2011-10-28 23:01:59

标签: c++ linux multithreading boost linked-list

在一个程序中我有一个M类:

class M{
    /*
      very big immutable fields
    */
    int status;
};

我需要一个M类型的对象链接列表。

三种类型的线程正在访问列表:

  • 生产者:生成并将对象追加到列表的末尾。所有新生成的对象都具有status = NEW。 (操作时间= O(1))
  • 消费者:在列表的开头使用对象。如果消费者具有status = CONSUMER_ID,则消费者可以使用该对象。每个消费者都将第一个项目保留在它可以消费的链表中,因此消费量(摊销?)O(1)(见下面的注释)。
  • 析构函数:当有通知表明对象已正确使用时,删除已使用的对象(操作时间= O(1))。
  • 修饰符:根据状态图更改对象的状态。任何对象的最终状态是消费者的id(每个对象的操作时间= O(1))。

消费者的数量少于10.生产者的数量可能大到几百个。有一个修饰符。

注意:修饰符可以修改已经消耗的对象,因此存储的消费者项目可以来回移动。我没有找到任何更好的解决方案来解决这个问题(虽然,对象之间的比较是O(1),操作不再分摊O(1))。

表现非常重要。因此,我想使用原子操作或细粒度锁(每个对象一个)来避免不必要的阻塞。

我的问题是:

  1. 原子操作是首选,因为它们更轻。我想我必须使用锁来更新析构函数线程中的指针,我可以使用原子操作来处理其他线程之间的争用。如果我遗漏了某些内容并且有理由不能在状态字段上使用原子操作,请告诉我。

  2. 我认为我不能使用STL列表,因为它不支持细粒度锁。但是你会建议使用Boost :: Intrusive列表(而不是自己编写)吗? Here有人提到侵入式数据结构更难以实现线程安全吗?这对于细粒度锁是否正确?

  3. 生成器,使用者和析构函数将根据某些事件异步调用(我计划使用Boost :: asio。但我不知道如何运行修饰符以最小化其与其他线程的争用。选项包括:

    • 与制片人异步。
    • 与消费者异步。
    • 使用自己的计时器。
  4. 只有在某些条件成立时,任何此类呼叫才会在列表上运行。我自己的直觉是,我如何调用修饰符之间没有区别。我错过了什么吗?

    我的系统是Linux / GCC,我正在使用boost 1.47以防万一。

    类似的问题:Thread-safe deletion of a linked list node, using the fine-grained approach

4 个答案:

答案 0 :(得分:8)

  

表现非常重要。因此,我想使用原子操作或细粒度锁(每个对象一个)来避免不必要的阻塞。

通过增加竞争(访问相同数据)的线程将在不同核心上同时运行的可能性,这将使性能变差。如果锁定太精细,线程可能会争分(在缓存之间使用乒乓数据)并以慢速锁定步骤运行而不会阻塞锁定,从而导致可怕的性能。

您希望使用足够粗糙的锁,以便尽快相互争用相同数据块的线程。这将迫使调度程序安排非竞争线程,消除破坏性能的缓存ping-ponging。

你有一个普遍的误解,认为阻塞很糟糕。事实上,争用是不好的,因为它会降低内核的速度。阻止结束争用。阻止是因为它取消了对竞争线程的调度,允许安排非竞争线程(可以全速并发运行)。

答案 1 :(得分:2)

如果您已经计划使用Boost Asio,那么好消息!您现在可以停止编写自定义异步生产者 - 使用者队列。

Boost Asio io_service类是一个异步队列,因此您可以轻松地使用它将对象从生产者传递给消费者。使用io_service::post()方法将绑定的函数对象入队以进行另一个线程的异步回调。

boost::asio::io_service io_service_;

void produce()
{
    M* m = new M;
    io_service_.post(boost::bind(&consume, m));
}

void consume(M* m)
{
    delete m;
}

让你的生产者线程调用produce(),然后让你的消费者线程调用io_service_.run(),然后在你的消费者线程上回调consume()。即时生产者 - 消费者!

另外,如果您愿意,可以将各种其他异构事件排入io_service_以供消费者线程处理,例如网络读取和等待信号。 Boost Asio不仅仅是一个网络库 - 它也是一种表达预分析器,反应器,生产者 - 消费者,线程池或任何其他类型的线程体系结构的简单方法。

修改

哦,还有一个提示。不要创建专用生成器线程和专用消费者线程的单独池。只需为您机器上的每个核心创建一个线程(4核心机器=> 4个线程)。然后让所有这些线程调用io_service_.run()。使用io_service_以异步方式读取文件或网络或其他内容,然后再次使用io_service_来异步消耗生成的内容。

这是性能最高的线程架构。每个核心一个线程。

答案 2 :(得分:1)

正如@David Schwartz所说,阻塞并不总是很慢,并且旋转(在用户空间多线程应用程序中)可能非常危险。

此外,linux pthread库具有pthread_mutex的“智能”实现。它设计为“轻量级”,即当一个线程试图锁定已经获取的互斥锁时,它会旋转一段时间,在它阻塞之前多次尝试获取锁。尝试次数不足以损害您的系统甚至破坏实时要求(如果有的话)。额外的Linux特定功能是所谓的fast user space mutex(FUTEX),它可以减少系统调用的数量。主要的想法是,只有当一个线程真正需要在互斥锁上阻塞时才会执行mutex_lock系统调用(当线程锁定未获取的互斥锁时,它不会进行系统调用)。

实际上,在大多数情况下,您不需要重新发明轮子或引入一些非常具体的锁定技术。如果你不得不这样做,那么设计上的任何一个问题,或者你处理的是高度并发的环境(乍一看,10个消费者看起来并不像这样,而且所有这些看起来都超过了工程设计)。

  • 如果我是你,我宁愿使用条件变量+互斥锁保护列表。
  • 我要做的另一件事就是重新审视设计。当消费者需要进行搜索以找出列表是否包含具有其ID的项目时(如果是,删除/出列),为什么要使用一个全局列表?为每个消费者制作单独的清单可能会更好吗?在这种情况下,您可能可以摆脱状态字段。
  • 读访问是否比写访问更频繁?如果是这样,最好使用R / W锁或RCU
  • 如果我不满意pthread原语和futex的东西(如果我不愿意,我会通过测试证明锁定原语是瓶颈,而不是消费者的数量或我选择的算法),那么我试着用引用计数来考虑复杂的算法,将GC线程和所有更新的限制分开是原子的。

答案 3 :(得分:1)

我会建议你采用略有不同的解决问题的方法:

生产者:将对象排入共享队列(SQ)的末尾。醒来 修改器通过信号量。

producer()
{
  while (true)
  {
    o = get_object_from_somewhere ()
    atomic_enqueue (SQ.queue, o)
    signal(SQ.sem)
  }
}

消费者:从每个消费者队列(CQ [i])前面的Deque对象。

consumer()
{
  while (true)
  {
    wait (CQ[self].sem)
    o = atomic_dequeue (CQ[self].queue)
    process (o)
    destroy (o)
  }
}

析构函数:消费者完成后,析构函数不存在 消费者摧毁它。

修饰符:修饰符从共享队列中取消对象, 处理它们并将它们排入适当消费者的私人队列。

modifier()
{
  while (true)
  {
    wait (SQ.sem)
    o = atomic_dequeue (SQ.queue)
    FSM (o)
    atomic_enqueue (CQ [o.status].queue, o)
    signal (CQ [o.status].sem)
  }
}

伪代码中各种atomic_xxx函数的注释:this 并不一定意味着使用像CAS,CAS2这样的原子指令, LL / SC等。它可以使用原子,自旋锁或普通互斥体。一世 会建议以最直接的方式实施它 (例如互斥体)并在以后证明它是一个优化它 表现问题。