0

I have a background worker that streams data and saves it to a ConcurrentQueue<T> which is what I need since it is a thread safe First In First Out collection, but I also need to do tasks like perform simple calculations or to pull data from this collection and I'm not sure what I need to use at this point. Here is some example pseudo code:

public class ExampleData
{
     public DateTime Date { get; set; }
     public decimal Value { get; set; }
}

public ConcurrentQueue<ExampleData> QueueCol { get; set; } = new();

public void AddToQueue(DateTime date, decimal value)
{
     QueueCol.Enqueue(new ExampleData() { Date = date, Value = value });
}

public void DisplayPastData()
{
     var count = QueueCol.Count();
     var prev1Data = count >= 2 ? QueueCol.ElementAt(count - 2) : null;
     var prev2Data = count >= 3 ? QueueCol.ElementAt(count - 3) : null;
     var prev3Data = count >= 4 ? QueueCol.ElementAt(count - 4) : null;

     if (prev1Data != null)
     {
         Console.WriteLine($"Date: {prev1Data.Date} Value: {prev1Data.Value}");
     }
     
     if (prev2Data != null)
     {
         Console.WriteLine($"Date: {prev2Data.Date} Value: {prev2Data.Value}");
     }

     if (prev3Data != null)
     {
         Console.WriteLine($"Date: {prev3Data.Date} Value: {prev3Data.Value}");
     }
}

This is a very rough example but even with displaying data most of it looks correct and then I will get dates completely out of left field like a date from the previous day in between dates from the current day and so because of ordering issues like that I know the data isn't correct so my question is how do I convert the concurrent queue to a new collection that will allow me to keep the order and to work with the data without giving incorrect results?

Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
DarthVegan
  • 1,719
  • 7
  • 25
  • 42
  • While display the queue data can be added to the queue which can give bad results. – jdweng Mar 23 '22 at 12:34
  • @jdweng Yeah I figured this was the reason for my data discrepancies so my question is how to I convert the data in the concurrent queue to a new collection that I can work with and also keeping the data in the correct order – DarthVegan Mar 23 '22 at 12:35
  • Do I get that right: You want to display the 3 (if exist) last recently enqueued elements? Or the _least_ recently enqueued (oldest)? – Fildor Mar 23 '22 at 12:54
  • Use QueueCol.CopyTo() method. – jdweng Mar 23 '22 at 12:57
  • @Fildor This is a very rough example but mostly to show the general ways I'm using it. Something simple like this example is showing data from all over the place and not even like what you would expect even when adding to the underlying collection like I am – DarthVegan Mar 23 '22 at 13:04
  • 1
    ^^ or [ToArray()](https://learn.microsoft.com/en-us/dotnet/api/system.collections.concurrent.concurrentqueue-1.toarray?view=net-6.0), but I'd only do this if the queue is never going to be really huge. – Fildor Mar 23 '22 at 13:08
  • @Fildor Yeah my data is fairly large with thousands of values usually so is there another recommendation for an issue like that? – DarthVegan Mar 23 '22 at 13:28
  • I'd probably actually populate a second datastructure when enqueueing that holds the desired "Display" information. That's fairly easy for outputting "the 3 most recently enqueued items" and a little bit more tricky for "up to 3 oldest elements" ... – Fildor Mar 23 '22 at 13:32
  • What other operations do you perform with the queue, beyond enqueuing items and displaying the three latest enqueued items? – Theodor Zoulias Mar 23 '22 at 15:50
  • @TheodorZoulias So I'm doing basic math operations like getting the sum of certain elements. One example is I'm getting the sum of odd elements. Example would be getting the value of 1 element from the end, 3 elements from the end, 5 elements from the end, etc – DarthVegan Mar 23 '22 at 17:02
  • @Fildor Getting the last 3 elements was the easiest for me to write pseudo code but I'm doing more complicated shit like the post I did above. Would something basic like this work do you think? var queueToList = new List(QueueCol.OrderBy(x => x.Date)) – DarthVegan Mar 23 '22 at 17:04
  • So you only enqueue items and inspect the items contained in the queue? You never dequeue anything? The queue keeps growing and growing? – Theodor Zoulias Mar 23 '22 at 17:06
  • @TheodorZoulias yes exactly what I'm doing – DarthVegan Mar 23 '22 at 17:11
  • Is it essential for your scenario to avoid blocking a thread that wants to enqueue items in the queue, while another thread performs calculations on the items contained in the queue? – Theodor Zoulias Mar 23 '22 at 17:17
  • @TheodorZoulias Yes I'm dealing with live data that comes in every second and have a bunch of methods that perform different calculations to display on a windows form in real time – DarthVegan Mar 23 '22 at 17:56

1 Answers1

2

The usage pattern you describe in your question makes a ConcurrentQueue<T> not a suitable collection for your scenario. As far as I can understand the requirements are:

  1. The producer(s) should be able to enqueue items in the collection without being blocked for any amount of time.
  2. The consumer(s) should be able to perform calculations on a snapshot of the collection, without creating an expensive copy of the collection, and without interfering in any way with the producer(s).

The collection that seems more suitable for your scenario out of the box, is the ImmutableList<T>. This collection can be updated with lock-free Interlocked operations, and it is essentially a snapshot by itself (because it is immutable). Here is how you could use it in a multithreading scenario, with thread-safety and without blocking any thread:

private ImmutableList<ExampleData> _data = ImmutableList<ExampleData>.Empty;

public ImmutableList<ExampleData> Data => Volatile.Read(ref _data);

public void AddToQueue(DateTime date, decimal value)
{
    var newData = new ExampleData() { Date = date, Value = value };
    ImmutableInterlocked.Update(ref _data, (x, y) => x.Add(y), newData);
}

public void DisplayPastData()
{
    ImmutableList<ExampleData> snapshot = Volatile.Read(ref _data);
    int count = snapshot.Count;
    var prev1Data = count >= 2 ? snapshot[count - 2] : null;
    var prev2Data = count >= 3 ? snapshot[count - 3] : null;
    var prev3Data = count >= 4 ? snapshot[count - 4] : null;

    if (prev1Data != null)
    {
        Console.WriteLine($"Date: {prev1Data.Date} Value: {prev1Data.Value}");
    }

    if (prev2Data != null)
    {
        Console.WriteLine($"Date: {prev2Data.Date} Value: {prev2Data.Value}");
    }

    if (prev3Data != null)
    {
        Console.WriteLine($"Date: {prev3Data.Date} Value: {prev3Data.Value}");
    }
}

The immutable collections are not without disadvantages. They are a lot slower in comparison with the normal collections, they require significantly more memory, and they create significantly more garbage every time they are updated.

An optimal solution to your specific scenario could be a combination of a ConcurrentQueue<ExampleData> (recent data) and a List<ExampleData> (historic data). The producer(s) would enqueue items in the ConcurrentQueue<T>, and the single consumer would dequeue all the items from the ConcurrentQueue<T> and then add them in the List<T>. Then it would use the List<T> to do the calculations.

Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104