how to access the underlying default concurrent queue of a blocking collection

后端 未结 3 1245
广开言路
广开言路 2020-12-11 03:05

I have multiple producers and a single consumer. However if there is something in the queue that is not yet consumed a producer should not queue it again. (unique no duplica

相关标签:
3条回答
  • 2020-12-11 03:40

    I would suggest implementing your operations with lock so that you don't read and write the item in a way that corrupts it, making them atomic. For example, with any IEnumerable:

    object bcLocker = new object();
    
    // ...
    
    lock (bcLocker)
    {
        bool foundTheItem = false;
        foreach (someClass nextItem in myBlockingColl)
        {
            if (nextItem.Equals(item))
            {
                foundTheItem = true;
                break;
            }
        }
        if (foundTheItem == false)
        {
            // Add here
        }
    }
    
    0 讨论(0)
  • 2020-12-11 03:55

    In addition to the caveat Brian Gideon mentioned after Update, his solution suffers from these performance issues:

    • O(n) operations on the queue (queue.Contains(item)) have a severe impact on performance as the queue grows
    • locks limit concurrency (which he does mention)

    The following code improves on Brian's solution by

    • using a hash set to do O(1) lookups
    • combining 2 data structures from the System.Collections.Concurrent namespace

    N.B. As there is no ConcurrentHashSet, I'm using a ConcurrentDictionary, ignoring the values.

    In this rare case it is luckily possible to simply compose a more complex concurrent data structure out of multiple simpler ones, without adding locks. The order of operations on the 2 concurrent data structures is important here.

    public class NoDuplicatesConcurrentQueue<T> : IProducerConsumerCollection<T>
    {
        private readonly ConcurrentDictionary<T, bool> existingElements = new ConcurrentDictionary<T, bool>();
        private readonly ConcurrentQueue<T> queue = new ConcurrentQueue<T>();
    
        public bool TryAdd(T item)
        {
            if (existingElements.TryAdd(item, false))
            {
                queue.Enqueue(item);
                return true;
            }
            return false;
        }
    
        public bool TryTake(out T item)
        {
            if (queue.TryDequeue(out item))
            {
                bool _;
                existingElements.TryRemove(item, out _);
                return true;
            }
            return false;
        }
        ...
    }
    

    N.B. Another way at looking at this problem: You want a set that preserves the insertion order.

    0 讨论(0)
  • 2020-12-11 03:58

    This is an interesting question. This is the first time I have seen someone ask for a blocking queue that ignores duplicates. Oddly enough I could find nothing like what you want that already exists in the BCL. I say this is odd because BlockingCollection can accept a IProducerConsumerCollection as the underlying collection which has the TryAdd method that is advertised as being able to fail when duplicates are detected. The problem is that I see no concrete implementation of IProducerConsumerCollection that prevents duplicates. At least we can write our own.

    public class NoDuplicatesConcurrentQueue<T> : IProducerConsumerCollection<T>
    {
      // TODO: You will need to fully implement IProducerConsumerCollection.
    
      private Queue<T> queue = new Queue<T>();
    
      public bool TryAdd(T item)
      {
        lock (queue)
        {
          if (!queue.Contains(item))
          {
            queue.Enqueue(item);
            return true;
          }
          return false;
        }
      }
    
      public bool TryTake(out T item)
      {
        lock (queue)
        {
          item = null;
          if (queue.Count > 0)
          {
            item = queue.Dequeue();
          }
          return item != null;
        }
      }
    }
    

    Now that we have our IProducerConsumerCollection that does not accept duplicates we can use it like this:

    public class Example
    {
      private BlockingCollection<object> queue = new BlockingCollection<object>(new NoDuplicatesConcurrentQueue<object>());
    
      public Example()
      {
        new Thread(Consume).Start();
      }
    
      public void Produce(object item)
      {
        bool unique = queue.TryAdd(item);
      }
    
      private void Consume()
      {
        while (true)
        {
          object item = queue.Take();
        }
      }
    }
    

    You may not like my implementation of NoDuplicatesConcurrentQueue. You are certainly free to implement your own using ConcurrentQueue or whatever if you think you need the low-lock performance that the TPL collections provide.

    Update:

    I was able to test the code this morning. There is some good news and bad news. The good news is that this will technically work. The bad news is that you probably will not want to do this because BlockingCollection.TryAdd intercepts the return value from the underlying IProducerConsumerCollection.TryAdd method and throws an exception when false is detected. Yep, that is right. It does not return false like you would expect and instead generates an exception. I have to be honest, this is both surprising and ridiculous. The whole point of the TryXXX methods is that they should not throw exceptions. I am deeply disappointed.

    0 讨论(0)
提交回复
热议问题