2

I have multiple threads generating items and sticking them in a common ConcurrentQueue:

private ConcurrentQueue<GeneratedItem> queuedItems = new ConcurrentQueue<GeneratedItem>();

private void BunchOfThreads () {
    // ...
    queuedItems.Enqueue(new GeneratedItem(...));
    // ...
}

I have another single consumer thread but the way it needs to work in the context of this application is, occasionally, it just needs to grab everything currently in the threads' queue, removing it from that queue, all in one shot. Something like:

private Queue<GeneratedItem> GetAllNewItems () {

    return queuedItems.TakeEverything(); // <-- not a real method

}

I think I looked through all the documentation (for the collection and its implemented interfaces) but I didn't seem to find anything like a "concurrently take all objects from queue", or even "concurrently swap contents with another queue".

I could do this no problem if I ditch the ConcurrentQueue and just protect a normal Queue with a lock, like this:

private Queue<GeneratedItem> queuedItems = new Queue<GeneratedItem>();

private void BunchOfThreads () {
    // ...
    lock (queuedItems) {
        queuedItems.Enqueue(new GeneratedItem(...));
    }
    // ...
}

private Queue<GeneratedItem> GetAllNewItems () {

    lock (queuedItems) {
        Queue<GeneratedItem> newItems = new Queue<Event>(queuedItems);
        queuedItems.Clear();
        return newItems;
    }

}

But, I like the convenience of the ConcurrentQueue and also since I'm just learning C# I'm curious about the API; so my question is, is there a way to do this with one of the concurrent collections?

Is there perhaps some way to access whatever synchronization object ConcurrentQueue uses and lock it for myself for my own purposes so that everything plays nicely together? Then I can lock it, take everything, and release?

Jason C
  • 38,729
  • 14
  • 126
  • 182
  • May I know what did you go with in the end? Trying to solve the same issue myself. – Lee Jul 24 '22 at 14:19

2 Answers2

3

It depends what you want to do. As per the comments in the source code

//number of snapshot takers, GetEnumerator(), ToList() and ToArray() operations take snapshot.

This works by internally calling ToList() which in turn works on m_numSnapshotTakers and a spin mechanism

/// Copies the <see cref="ConcurrentQueue{T}"/> elements to a new <see
/// cref="T:System.Collections.Generic.List{T}"/>.
/// </summary>
/// <returns>A new <see cref="T:System.Collections.Generic.List{T}"/> containing a snapshot of
/// elements copied from the <see cref="ConcurrentQueue{T}"/>.</returns>
private List<T> ToList()
{
   // Increments the number of active snapshot takers. This increment must happen before the snapshot is 
   // taken. At the same time, Decrement must happen after list copying is over. Only in this way, can it
   // eliminate race condition when Segment.TryRemove() checks whether m_numSnapshotTakers == 0. 
   Interlocked.Increment(ref m_numSnapshotTakers);

   List<T> list = new List<T>();
   try
   {
       //store head and tail positions in buffer, 
       Segment head, tail;
       int headLow, tailHigh;
       GetHeadTailPositions(out head, out tail, out headLow, out tailHigh);

       if (head == tail)
       {
           head.AddToList(list, headLow, tailHigh);
       }
       else
       {
           head.AddToList(list, headLow, SEGMENT_SIZE - 1);
           Segment curr = head.Next;
           while (curr != tail)
           {
               curr.AddToList(list, 0, SEGMENT_SIZE - 1);
               curr = curr.Next;
           }
           //Add tail segment
           tail.AddToList(list, 0, tailHigh);
       }
   }
   finally
   {
       // This Decrement must happen after copying is over. 
       Interlocked.Decrement(ref m_numSnapshotTakers);
   }
   return list;
}

If a snapshot is all you want, then you are in luck. However, there is seemingly no built in way to get and remove all the items from a ConcurrentQueue in a thread safe manner. You will need to bake your own synchronisation by using lock or similar. Or roll your own (which might not be all that difficult looking at the source).

TheGeneral
  • 79,002
  • 9
  • 103
  • 141
  • 2
    Thanks; especially for the source link (this is going to be a great resource). Yeah it looks like they have some sort of homebrew read/write spin lock thing going on. I think it's probably quicker to do my own synchronization right now but I'm going to play around with an extension later, it does look like it should be pretty straightforward. – Jason C Feb 10 '20 at 21:48
1

There is no such method, because it is ambiguous what TakeEverything should actually do:

  1. Take item by item until Queue is empty and then return taken items.
  2. Lock whole access to the queue, take a snapshot (take all items in a loop) = clear the queue, unlock, return the snapshot.

Consider first scenario and imagine that other threads are writing to the queue at the time you are removing the items one by one from the queue - should TakeEverything method include those in the result?

If yes then you can just write it as:

public List<GeneratedItem> TakeEverything()
{
    var list = new List<GeneratedItem>();

    while (queuedItems.TryDequeue(out var item))
    {
        list.Add(item);
    }

    return list;
}

If no then I would still use ConcurrentQueue (because all the instance members - methods and properties - from ordinary Queue are not thread safe) and implement custom lock for every read/write access, so you make sure you are not adding items while "taking everything" from the queue.

  • 2
    Thanks. #2 is what I had in mind. Yeah I was thinking about maybe just wrapping it in a read/write lock but basically taking the write lock for the take-and-clear operation and the read lock for *everything* else (letting the ConcurrentQueue's internal synchronization handle access in all those cases). It seemed a little redundant though but, I guess I do have to do *something*. – Jason C Feb 10 '20 at 21:50
  • 2
    Yeah I mean if performance is the issue I would go with custom implementation. `ConcurrentQueue` does not actually use ordinary lock internally - it uses CAS https://en.wikipedia.org/wiki/Compare-and-swap `SpinWait` + `Interlocked.CompareExchange` The non-generic Queue has static `Synchronized` method that returns thread safe version: https://docs.microsoft.com/en-us/dotnet/api/system.collections.queue.synchronized?view=netframework-4.8 and you can use `SyncRoot` for your custom locking. Here is impl: https://github.com/microsoft/referencesource/blob/master/mscorlib/system/collections/queue.cs –  Feb 10 '20 at 22:11