简体   繁体   English

具有序列 ID 的线程安全的固定大小的循环缓冲区

[英]Thread-safe fixed-size circular buffer with sequence ids

I need a queue with these capabilities:我需要一个具有以下功能的队列:

  • fixed-size (ie circular buffer)固定大小(即循环缓冲区)
  • queue items have ids (like a primary key), which are sequential队列项具有 ids(如主键),它们是顺序的
  • thread-safe (used from multiple ASP.NET Core requests)线程安全(用于多个 ASP.NET Core 请求)

To avoid locking, I tried a ConcurrentQueue but found race conditions.为了避免锁定,我尝试ConcurrentQueue但发现了竞争条件。 So I'm trying a custom approach.所以我正在尝试一种自定义方法。

public interface IQueueItem
{
    long Id { get; set; }
}

public class CircularBuffer<T> : LinkedList<T> where T : class, IQueueItem
{
    public CircularBuffer(int capacity) => _capacity = capacity;
    private readonly int _capacity;

    private long _counter = 0;
    private readonly object _lock = new();

    public void Enqueue(T item)
    {
        lock (_lock) {         // works but feels "heavy"
            _counter++;
            item.Id = _counter;
            if (Count == _capacity) RemoveFirst();
            AddLast(item);
        }
    }
}

And to test:并测试:

public class Item : IQueueItem
{
    public long Id { get; set; }
    //...
}

public class Program
{
    public static void Main()
    {
        var q = new CircularBuffer<Item>(10);
        Parallel.For(0, 15, i => q.Enqueue(new Item()));
        Console.WriteLine(string.Join(", ", q.Select(x => x.Id)));
    }
}

Which gives correct output (is ordered even though enqueued by competing threads, and has fixed size with oldest items dequeued):它给出了正确的输出(即使被竞争线程排队,并且具有固定的大小,最旧的项目已出队):

6, 7, 8, 9, 10, 11, 12, 13, 14, 15 6、7、8、9、10、11、12、13、14、15

In reality, I have web requests that read (ie enumerate) that queue.实际上,我有读取(即枚举)该队列的 Web 请求。

The problem : if one thread is enumerating the queue while another thread is adding to it, I will have errors.问题:如果一个线程正在枚举队列,而另一个线程正在添加队列,我将遇到错误。 (I could use a ToList() before the read, but for a large queue that will suck up all the server's memory as this could be done many times a second by multiple requests). (我可以在读取之前使用ToList() ,但是对于会占用所有服务器内存的大型队列,因为这可以通过多个请求每秒完成多次)。 How can I deal with that scenario?我该如何处理这种情况? I used a linked list, but I'm flexible to use any structure.我使用了链表,但我可以灵活地使用任何结构。

(Also, that seems to be a really heavy lock section; is there a more performant way?) (另外,这似乎是一个非常重的锁定部分;有没有更高效的方法?)

UPDATE更新
As asked in comments below: I expect the queue to have from a few hundred to a few tens of thousand items, but the items themselves are small (just a few primitive data types).正如下面评论中所问的:我希望队列有几百到几万个项目,但项目本身很小(只有几个原始数据类型)。 I expect an enqueue every second.我希望每秒都有一个排队。 Reads from web requests are less often, let's say a few times per minute (but can occur concurrently to the server writing to the queue).从 Web 请求中读取的频率较低,比如说每分钟几次(但可能与写入队列的服务器同时发生)。

Since ConcurrentQueue is out in this question, you can try fixed array.由于 ConcurrentQueue 不在此问题中,您可以尝试使用固定数组。

IQueueItem[] items = new IQueueItem[SIZE];
long id = 0;

Enqueue is simple.入队很简单。

void Enqueue(IQueueItem item)
{
    long id2 = Interlocked.Increment(ref id);
    item.Id = id2 - 1;
    items[id2 % SIZE] = item;
}

To output the data, you just need copy the array to a new one, then sort it.要输出数据,您只需将数组复制到一个新数组,然后对其进行排序。 (of course, it can be optimized here) (当然这里可以优化)

var arr = new IQueueItem[SIZE];
Array.Copy(items, arr, SIZE);
return arr.Where(a => a != null).OrderBy(a => a.Id);

There may be some gaps in the array because of the concurrent insertions, you can take a sequence till a gap is found.由于并发插入,数组中可能存在一些间隙,您可以采取序列直到找到间隙。

var e = arr.Where(a => a != null).OrderBy(a => a.Id);
var firstId = e.First().Id;
return e.TakeWhile((a, index) => a.Id - index == firstId);

Based on the metrics that you provided in the question, you have plenty of options.根据您在问题中提供的指标,您有很多选择。 The anticipated usage of the CircularBuffer<T> is not really that heavy. CircularBuffer<T>的预期使用并没有那么重。 Wrapping a lock -protected Queue<T> should work pretty well.包装一个受lock保护的Queue<T>应该可以很好地工作。 The cost of copying the contents of the queue into an array on each enumeration (copying 10,000 elements a few times per second) is unlikely to be noticeable.在每次枚举时将队列的内容复制到数组中(每秒复制 10,000 个元素几次)的成本不太可能引起注意。 Modern machines can do such things in the blink of an eye.现代机器可以在眨眼之间做这样的事情。 You'd have to enumerate the collection thousands of times per second for this to start (slightly) becoming an issue.您必须每秒枚举数千次集合才能开始(稍微)成为一个问题。

For the sake of variety I'll propose a different structure as internal storage: the ImmutableQueue<T> class.为了多样化,我将提出一个不同的结构作为内部存储: ImmutableQueue<T>类。 Its big plus is that it can be enumerated freely by multiple threads concurrently.它的最大优点是可以由多个线程同时自由枚举。 You don't have to worry about concurrent mutations, because this collection is immutable.您不必担心并发突变,因为这个集合是不可变的。 Nobody can change it after it has been created, ever.任何人都无法在它创建后对其进行更改。

The way that you update this collection is by creating a new collection and discarding the previous one.更新此集合的方式是创建一个新集合并丢弃以前的集合。 This collection has methods Enqueue and Dequeue that don't mutate the existing collection, but instead they return a new collection with the desirable mutation.此集合具有方法EnqueueDequeue ,它们不会改变现有集合,而是返回具有所需突变的新集合。 This sounds extremely inefficient, but actually it's not.这听起来效率极低,但实际上并非如此。 The new collection reuses most of the internal parts of the existing collection.新集合重用了现有集合的大部分内部部分。 Of course it's much more expensive compared to mutating a Queue<T> , probably around 10 times more expensive, but you hope that you'll get even more back in return by how cheap and non-contentious is to enumerate it.当然,与变异Queue<T>相比,它要贵得多,可能要贵 10 倍左右,但您希望通过枚举它的廉价和无争议的方式获得更多回报。

public class ConcurrentCircularBuffer<T> : IEnumerable<T> where T : IQueueItem
{
    private readonly object _locker = new();
    private readonly int _capacity;
    private ImmutableQueue<T> _queue = ImmutableQueue<T>.Empty;
    private int _count = 0;
    private long _lastId = 0;

    public ConcurrentCircularBuffer(int capacity) => _capacity = capacity;

    public void Enqueue(T item)
    {
        lock (_locker)
        {
            item.Id = ++_lastId;
            _queue = _queue.Enqueue(item);
            if (_count < _capacity)
                _count++;
            else
                _queue = _queue.Dequeue();
        }
    }

    public IEnumerator<T> GetEnumerator()
    {
        var enumerator = Volatile.Read(ref _queue).GetEnumerator();
        while (enumerator.MoveNext())
            yield return enumerator.Current;
    }

    IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}

The class that implements the IQueueItem interface should be implemented like this:实现IQueueItem接口的类应该这样实现:

public class QueueItem : IQueueItem
{
    private long _id;

    public long Id
    {
        get => Volatile.Read(ref _id);
        set => Volatile.Write(ref _id, value);
    }
}

Otherwise it might be possible for a thread to see an IQueueItem instance with uninitialized Id .否则,线程可能会看到IQueueItem具有未初始化Id的实例。 For an explanation you can read this article by Igor Ostrovsky.有关解释,您可以阅读 Igor Ostrovsky 的这篇文章。 I am not 100% sure that it's possible, but neither I can guarantee that it's impossible.我不能 100% 确定这是可能的,但我也不能保证这是不可能的。 Even with the Volatile in place, it still looks fragile to me to delegate the responsibility of initializing the Id to an external component.即使有了Volatile ,将初始化Id的责任委托给外部组件对我来说仍然很脆弱。

Here is another implementation, using a Queue<T> with locking.这是另一个实现,使用带有锁定的Queue<T>

public interface IQueueItem
{
    long Id { get; set; }
}

public class CircularBuffer<T> : IEnumerable<T> where T : class, IQueueItem
{
    private readonly int _capacity;
    private readonly Queue<T> _queue;
    private long _lastId = 0;
    private readonly object _lock = new();

    public CircularBuffer(int capacity) {
        _capacity = capacity;
        _queue = new Queue<T>(capacity);
    }

    public void Enqueue(T item)
    {
        lock (_lock) {
            if (_capacity < _queue.Count)
                _queue.Dequeue();
            item.Id = ++_lastId;
            _queue.Enqueue(item);
        }
    }

    public IEnumerator<T> GetEnumerator()
    {
        lock (_lock) {
            var copy = _queue.ToArray();
            return ((IEnumerable<T>)copy).GetEnumerator();
        }
    }

    IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();

}

And to test:并测试:

public class Item : IQueueItem
{
    private long _id;

    public long Id
    {
        get => Volatile.Read(ref _id);
        set => Volatile.Write(ref _id, value);
    }
}

public class Program
{
    public static void Main()
    {
        var q = new CircularBuffer<Item>(10);
        Parallel.For(0, 15, i => q.Enqueue(new Item()));
        Console.WriteLine(string.Join(", ", q.Select(x => x.Id)));
    }
}

Result:结果:

6, 7, 8, 9, 10, 11, 12, 13, 14, 15 6、7、8、9、10、11、12、13、14、15

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM