【问题标题】:Best Data Structure for this Multithreaded Use Case: Is Intrusive List good?此多线程用例的最佳数据结构:侵入式列表好吗?
【发布时间】:2009-05-30 02:30:12
【问题描述】:

我需要设计一个支持以下操作的数据结构:

  • 数据结构中的搜索元素基于作为间隔的键。例如,区间 1-5 内的值可能是 3,区间 6-11 内的值可能是 5,依此类推。间隔是连续的,它们之间没有重叠。
  • 查找上一个和下一个区间 - 这几乎与搜索区间一样频繁。
  • 分割区间,加入连续区间
  • 并发:我将设计限制为一个写入线程和其他读取线程,如下所示。编写器线程可以拆分或连接区间,或修改区间中的值。任何读取器线程仅在一个间隔内读取值(读取器可能读取多个间隔,但我不必序列化所有读取 - 在两次读取之间进行写入是可以的)。每个读取器每次写入大约有 20-80 次读取。此外,我仍然需要确定读者人数,但应该是 2-8 人左右。

我考虑使用 list 在中间添加和删除元素。只有有限数量的间隔 - 所以使用 map 可能是不正确的。这种访问(一个写入器,多个读取器)不受 STL 列表的良好支持。 boost::intrusive::list 似乎合适。除了侵入性列表之外,我还必须获取锁来读取/写入间隔。

另外,我了解侵入式列表可用于比 STL 列表更好的缓存局部性(以及为包含的对象分配适当的内存)。

方法好吗?如果是,我也很想知道您使用 intrusive::list 的经验,特别是对于多线程应用程序。

【问题讨论】:

  • 这可能有助于提供一些详细信息,说明更新与读取的频率相比,以及在典型或最坏情况下您期望的读取器线程数。

标签: c++ multithreading boost thread-safety


【解决方案1】:

这里有 2 个不同的问题:

  1. 如何表示您的数据结构
  2. 如何以高效的方式使其线程安全

您的数据结构将为每次写入执行 (20-80) x (2-8) 次读取。

(1)。首先,假设您的范围是如下的数据结构


    struct Interval
    {
        Interval(int start, int length)
          : m_start(start),
            m_length(length)
        {}
        int m_start;
        int m_length;
        int value; // Or whatever
    };

由于读取量大大超过写入量,因此查找需要快速,而修改则不需要。

对数据结构使用列表意味着 O(N) 次查找和 O(1) 次修改 - 完全是错误的方式。

你的结构最简单的表示是向量。如果间隔按排序顺序保存,则查找为 O(logN),修改为 O(N)。

要实现这一点,只需在 Interval 中添加一个比较器:

bool operator<(const Interval& rhs) const
{
    return m_start < rhs.m_start;
}

然后您可以使用std::lower_bound 找到等于或小于您的搜索间隔的第一个间隔,单位为 O(logN)。

下一个和上一个间隔是 O(1) - 递减或递增返回的迭代器。

分割区间意味着在当前元素之后插入一个新元素并调整当前元素的长度 - O(N)。

加入两个区间意味着将下一个区间的长度添加到当前区间并擦除下一个区间 - O(N)。

您应该reserve() 向量中有足够的空间来容纳最大数量的元素,以最大限度地减少调整大小的开销。

(2)。继 Knuth 之后,“过早的优化是万恶之源”。

对持有你的 vector 的结构进行单个读/写锁定很可能就足够了。唯一可能的问题是 (2a) 由于读者独占锁而导致写入者饥饿,或 (2b) 由于写入者更新时间过长而导致读取者饥饿。

(2a) 如果(且仅当)您面临作家饥饿,您可以使锁定更加细化。 极可能不是这样。为此:

让你的向量通过指针而不是值来保持它的间隔。这样调整大小就不会在内存中移动对象。让每个间隔都包含一个读/写锁。

对于读取: 获取集合的读锁,然后获取所需的时间间隔。如果您不需要读取任何其他区间,请在获得区间锁后立即放弃集合锁,以允许其他线程继续执行。

如果您需要读取其他存储桶,您可以按任意顺序对其进行读取锁定,直到您放弃集合读取锁定,此时写入器可以添加或删除您尚未锁定的任何间隔。获取这些锁时顺序无关紧要,因为当您持有集合上的读取锁时写入者无法更改向量,并且读取锁不会竞争。

对于写入:

获取集合的写锁,然后获取所需的时间间隔。请注意,对于将添加或删除间隔的所有更新,您必须持有集合写锁。如果您只更新一个间隔,您可以放弃收集锁。否则,您需要持有写锁并在您将要修改的任何时间间隔上获取写锁。您可以按任何顺序获取间隔锁,因为没有集合锁,任何读取器都无法获取新的读锁。

上述工作对作者线程更加自私,这应该消除饥饿。

(2b) 如果您面临读者饥饿(这种情况更不可能),最好的解决方案是将集合写入和读取分开。通过共享指针持有集合,并对其有一个写锁。

对于读取: 获取写锁和shared_ptr 的副本。放弃写锁。读者现在可以在没有任何锁的情况下读取集合(它是不可变的)。

对于写入: 根据读者将 shared_ptr 带到集合中,放弃锁定。制作集合的私有副本并对其进行修改(因为它是私有副本,所以不需要锁定)。再次获取写锁并将现有的 shared_ptr 替换为您的新集合。完成旧集合的最后一个线程将销毁它。所有未来的线程都将使用新更新的集合。

请注意,根据您的问题描述,此算法仅适用于一位作者。

【讨论】:

  • 感谢您的详细回答。如果允许,我会给出 +5。
【解决方案2】:

并发二叉树可能是一个不错的选择,它允许对不同间隔的读取和写入并行进行。

【讨论】:

    猜你喜欢
    • 2012-02-19
    • 2017-11-13
    • 1970-01-01
    • 1970-01-01
    • 2020-04-18
    • 2017-09-26
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多