这里有 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 替换为您的新集合。完成旧集合的最后一个线程将销毁它。所有未来的线程都将使用新更新的集合。
请注意,根据您的问题描述,此算法仅适用于一位作者。