ConcurrentBag TryTake method, which item is removed?

1.6k Views Asked by At

With reference from MSDN ConcurrentBag<T>::TryTake method.

Attempts to remove and return an object from the ConcurrentBag<T>.

I am wondering about on which basis it removes object from the Bag, as per my understanding dictionary add and remove works on the basis of HashCode.

If concurrent bag has nothing to do with the hashcode, what will happen when the object values get change during the add and remove.

For example:

public static IDictionary<string, ConcurrentBag<Test>> SereverConnections
    = new ConcurrentDictionary<string, ConcurrentBag<Test>>();

public class Student
{
    public string FirstName { get; set; }
    Student student = new Student();
    SereverConnections.Add(student.FirstName{"bilal"});
}

// have change the name from student.FirstName="khan";
// property of the object has been changed

Now the object properties values has been changed.

What will be the behavior of when I remove ConcurrentBag<T>::TryTake method? how it will track the object is same when added?

Code:

public class Test
{
    public HashSet<string> Data = new HashSet<string>();
    public static IDictionary<string, ConcurrentBag<Test>> SereverConnections
        = new ConcurrentDictionary<string, ConcurrentBag<Test>>();

    public string SessionId { set; get; }
    public Test()
    {
        SessionId = Guid.NewGuid().ToString();
    }
    public void Add(string Val)
    {
        lock (Data) Data.Add(Val);
    }

    public void Remove(string Val)
    {
        lock (Data) Data.Remove(Val);
    }

    public void AddDictonary(Test Val)
    {
        ConcurrentBag<Test> connections;
        connections = SereverConnections["123"] = new ConcurrentBag<Test>();

        connections.Add(this);
    }

    public void RemoveDictonary(Test Val)
    {
        SereverConnections["123"].TryTake(out Val);
    }

    public override int GetHashCode()
    {
        return SessionId.GetHashCode();
    }
}

//calling
class Program
{
    static void Main(string[] args)
    {
        Test test = new Test();

        test.AddDictonary(test);

        test.RemoveDictonary(test);//remove test.
        test.RemoveDictonary(new Test());//new object
    }
}
1

There are 1 best solutions below

0
Theodor Zoulias On

I am wondering about on which basis it removes object from the Bag,

The exact behavior of the ConcurrentBag<T>.TryTake method is not documented, so officially you are not allowed to make any assumption about which item will be removed. Any assumption that you make might be invalidated when the next version of the .NET runtime is released, since Microsoft reserves the right to change any undocumented behavior of any API without notice.

The current implementation of the ConcurrentBag<T> is based on something like a ThreadLocal<Queue<T>>. When you add an item in the collection, it is enqueued in the local queue (the queue of the current thread). When you take an item from the collection, you are given an item dequeued from the local queue, or, if the local queue is empty, an item stolen from another thread's local queue. Here is an experimental demonstration of this behavior:

ConcurrentBag<int> bag = new();
for (int i = 1; i <= 5; i++) bag.Add(i);
Thread t = new(_ => { for (int i = 6; i <= 10; i++) bag.Add(i); });
t.Start();
t.Join();
Console.WriteLine($"Contents: {String.Join(", ", bag)}");
Console.Write("Consumed: ");
while (bag.TryTake(out int item)) Console.Write($"{item}, ");

There are 2 threads involved, the current thread and the thread t. The current thread adds the items 1 - 5. The other thread adds the items 6 - 10. Then the current thread enumerates the collection, and finally it consumes it.

Output:

Contents: 10, 9, 8, 7, 6, 5, 4, 3, 2, 1
Consumed: 5, 4, 3, 2, 1, 6, 7, 8, 9, 10, 

Notice the difference between enumerating the collection and consuming it. The enumeration resembles a stack, and the consumption resembles a queue that midway becomes a stack. The first item that is taken (5) is the last item added by the current thread. The last item that is taken (10) is the last item added by the other thread. The consuming order would be different if the other thread had done it. In that case it would be:

Consumed: 10, 9, 8, 7, 6, 1, 2, 3, 4, 5, 

Online demo.

At this point you might have realized that when the consumption order is important, the ConcurrentBag<T> is not a suitable collection. The main application for this collection is for creating object-pools. If you want to reduce memory allocations by storing reusable objects in a pool, the ConcurrentBag<T> might be the collection of choice. For practically any other use you are advised to stay away from this collection. Not only it's annoyingly unpredictable, but it is also equipped with a highly inefficient enumerator. Each time an enumeration starts, all the items in the collection are copied in a new array. So for example if the collection has 1,000,000 items and you call bag.First(), a new T[] array with 1,000,000 length will be created behind the scenes, just for returning a single item! A collection that is more suitable for use in general multithreading is the ConcurrentQueue<T>. The method ConcurrentQueue<T>.Enqueue enqueues the item at the end of the queue, so it is much closer conceptually to the method List<T>.Add than the method ConcurrentBag<T>.Add.

(I have posted more thoughts about the ConcurrentBag<T> in this answer.)