From d81e19da8c72c3f523626b2e519154de7c543438 Mon Sep 17 00:00:00 2001 From: Vladimir Pecanac Date: Tue, 15 Sep 2026 15:34:52 +0200 Subject: [PATCH 1/2] ConcurrentBag in C#: retarget net10.0, List instead of ArrayList, Clear test Retarget both projects from net7.0 to net10.0 and bump the test packages (Microsoft.NET.Test.Sdk 18.10.1, xunit 2.9.3, xunit.runner.visualstudio 4.0.0, coverlet.collector 10.0.1). Replace the non-generic ArrayList with List in RemoveFromConcurrentBag, RemoveFromConcurrentBagConcurrently and AccessItemFromAConcurrentBag, and update the three tests that asserted the ArrayList type. Drop the dead array allocation in ConcurrentBagToArrayMethod, split CreateConcurrentBag into the three construction forms it was demonstrating with its locals removed, document the Count-then-CopyTo race in ConcurrentBagCopyToMethod, convert Program.cs to top-level statements that print the count and the empty flag, and add the missing Clear test. Build: 0 warnings, 0 errors. Tests: 8 of 8 passing on SDK 10.0.302, runtime 10.0.10. --- .../ConcurrentBagDemo.cs | 42 ++++++++++--------- .../ConcurrentBagInCSharp.csproj | 2 +- .../ConcurrentBagInCSharp/Program.cs | 19 +++------ .../Tests/ConcurrentBagInCSharpTest.cs | 24 +++++++---- .../ConcurrentBagInCSharp/Tests/Tests.csproj | 10 ++--- 5 files changed, 52 insertions(+), 45 deletions(-) diff --git a/collections-csharp/ConcurrentBagInCSharp/ConcurrentBagInCSharp/ConcurrentBagDemo.cs b/collections-csharp/ConcurrentBagInCSharp/ConcurrentBagInCSharp/ConcurrentBagDemo.cs index 7db71a42bc..a4463c3ac5 100644 --- a/collections-csharp/ConcurrentBagInCSharp/ConcurrentBagInCSharp/ConcurrentBagDemo.cs +++ b/collections-csharp/ConcurrentBagInCSharp/ConcurrentBagInCSharp/ConcurrentBagDemo.cs @@ -1,21 +1,24 @@ using System.Collections.Concurrent; -using System.Collections; namespace ConcurrentBagInCSharp { public class ConcurrentBagDemo { - public static ConcurrentBag CreateConcurrentBag() + public static ConcurrentBag CreateEmptyConcurrentBag() { - ConcurrentBag myNumbers = new(); + return new ConcurrentBag(); + } - var myConcurrentBag = new ConcurrentBag() { 2, 4, 6, 8, 10 }; - var myConcurrentBagCount = myConcurrentBag.Count; + public static ConcurrentBag CreateConcurrentBagWithInitialItems() + { + return new ConcurrentBag() { 2, 4, 6, 8, 10 }; + } + public static ConcurrentBag CreateConcurrentBag() + { var myList = new List() { 1, 2, 3, 4, 5, 6, 7 }; - var anotherConcurrentBag = new ConcurrentBag(myList); - return anotherConcurrentBag; + return new ConcurrentBag(myList); } public static ConcurrentBag CreateAndAddToConcurrentBagConcurrently() @@ -30,9 +33,9 @@ public static ConcurrentBag CreateAndAddToConcurrentBagConcurrently() return numbersBag; } - public static ArrayList RemoveFromConcurrentBag(ConcurrentBag numbersBag) + public static List RemoveFromConcurrentBag(ConcurrentBag numbersBag) { - var result = new ArrayList(); + var result = new List(); if (numbersBag.TryTake(out int number)) { result.Add(number); @@ -41,9 +44,9 @@ public static ArrayList RemoveFromConcurrentBag(ConcurrentBag numbersBag) return result; } - public static ArrayList RemoveFromConcurrentBagConcurrently(ConcurrentBag bag) + public static List RemoveFromConcurrentBagConcurrently(ConcurrentBag bag) { - var numbersList = new ArrayList(); + var numbersList = new List(); Parallel.For(0, 20, i => { if (bag.TryTake(out int number)) @@ -56,9 +59,9 @@ public static ArrayList RemoveFromConcurrentBagConcurrently(ConcurrentBag b return numbersList; } - public static ArrayList AccessItemFromAConcurrentBag(ConcurrentBag bag) + public static List AccessItemFromAConcurrentBag(ConcurrentBag bag) { - var result = new ArrayList(); + var result = new List(); if (bag.TryPeek(out int number)) { @@ -81,20 +84,21 @@ public static void AccessItemFromAConcurrentBagConcurrently(ConcurrentBag b public static int[] ConcurrentBagToArrayMethod(ConcurrentBag bag) { - var myArray = new int[bag.Count]; - myArray = bag.ToArray(); - - return myArray; + return bag.ToArray(); } public static int[] ConcurrentBagCopyToMethod(ConcurrentBag bag) { + // Count and CopyTo are two separate operations. In a multithreaded program another + // thread can add an item between them, and CopyTo then throws ArgumentException + // because the destination is too short. Size the destination from a snapshot, + // or generously, whenever other threads can still write to the bag. var someArray = new int[bag.Count]; bag.CopyTo(someArray, 0); return someArray; } - + public static void ConcurrentBagClearMethod(ConcurrentBag bag) { bag.Clear(); @@ -102,4 +106,4 @@ public static void ConcurrentBagClearMethod(ConcurrentBag bag) Console.WriteLine($"My concurrent bag contains {bag.Count} item."); // My concurrent bag contains 0 item. } } -} \ No newline at end of file +} diff --git a/collections-csharp/ConcurrentBagInCSharp/ConcurrentBagInCSharp/ConcurrentBagInCSharp.csproj b/collections-csharp/ConcurrentBagInCSharp/ConcurrentBagInCSharp/ConcurrentBagInCSharp.csproj index f02677bf64..dfb40caafc 100644 --- a/collections-csharp/ConcurrentBagInCSharp/ConcurrentBagInCSharp/ConcurrentBagInCSharp.csproj +++ b/collections-csharp/ConcurrentBagInCSharp/ConcurrentBagInCSharp/ConcurrentBagInCSharp.csproj @@ -2,7 +2,7 @@ Exe - net7.0 + net10.0 enable enable diff --git a/collections-csharp/ConcurrentBagInCSharp/ConcurrentBagInCSharp/Program.cs b/collections-csharp/ConcurrentBagInCSharp/ConcurrentBagInCSharp/Program.cs index e9882e00e5..0bc89713ac 100644 --- a/collections-csharp/ConcurrentBagInCSharp/ConcurrentBagInCSharp/Program.cs +++ b/collections-csharp/ConcurrentBagInCSharp/ConcurrentBagInCSharp/Program.cs @@ -1,13 +1,6 @@ -namespace ConcurrentBagInCSharp -{ - internal class Program - { - static void Main(string[] args) - { - var myConcurrentBag = ConcurrentBagDemo.CreateAndAddToConcurrentBagConcurrently(); - var myConcurrentBagCount = myConcurrentBag.Count; - var isMyConcurrentBagEmpty = myConcurrentBag.IsEmpty; - - } - } -} \ No newline at end of file +using ConcurrentBagInCSharp; + +var myConcurrentBag = ConcurrentBagDemo.CreateAndAddToConcurrentBagConcurrently(); + +Console.WriteLine($"Count: {myConcurrentBag.Count}"); +Console.WriteLine($"IsEmpty: {myConcurrentBag.IsEmpty}"); diff --git a/collections-csharp/ConcurrentBagInCSharp/Tests/ConcurrentBagInCSharpTest.cs b/collections-csharp/ConcurrentBagInCSharp/Tests/ConcurrentBagInCSharpTest.cs index f33e3c9807..2f2ac57395 100644 --- a/collections-csharp/ConcurrentBagInCSharp/Tests/ConcurrentBagInCSharpTest.cs +++ b/collections-csharp/ConcurrentBagInCSharp/Tests/ConcurrentBagInCSharpTest.cs @@ -1,5 +1,4 @@ using ConcurrentBagInCSharp; -using System.Collections; using System.Collections.Concurrent; namespace Tests @@ -25,32 +24,32 @@ public void GivenAnEmptyConcurrentBag_WhenAddingToAConcurrentBag_ThenReturnsAPop } [Fact] - public void GivenAConcurrentBag_WhenRemovingFromAConcurrentBag_ThenReturnsAnArrayList() + public void GivenAConcurrentBag_WhenRemovingFromAConcurrentBag_ThenReturnsAList() { var bag = ConcurrentBagDemo.CreateAndAddToConcurrentBagConcurrently(); var result = ConcurrentBagDemo.RemoveFromConcurrentBag(bag); - Assert.IsType(result); + Assert.IsType>(result); Assert.Single(result); } [Fact] - public void GivenAConcurrentBag_WhenRemovingFromAConcurrentBagConcurrently_ThenReturnsAnArrayList() + public void GivenAConcurrentBag_WhenRemovingFromAConcurrentBagConcurrently_ThenReturnsAList() { var bag = ConcurrentBagDemo.CreateAndAddToConcurrentBagConcurrently(); var result = ConcurrentBagDemo.RemoveFromConcurrentBagConcurrently(bag); - Assert.IsType(result); + Assert.IsType>(result); Assert.NotEmpty(result); } [Fact] - public void GivenAConcurrentBag_WhenReadingFromAConcurrentBag_ThenReturnsAnArrayList() + public void GivenAConcurrentBag_WhenReadingFromAConcurrentBag_ThenReturnsAList() { var bag = ConcurrentBagDemo.CreateAndAddToConcurrentBagConcurrently(); var result = ConcurrentBagDemo.AccessItemFromAConcurrentBag(bag); - Assert.IsType(result); + Assert.IsType>(result); Assert.Single(result); } @@ -64,6 +63,17 @@ public void GivenAConcurrentBag_WhenConvertingToAnArray_ThenReturnsAnArray() Assert.NotEmpty(result); } + [Fact] + public void GivenAConcurrentBag_WhenClearingAConcurrentBag_ThenReturnsAnEmptyConcurrentBag() + { + var bag = ConcurrentBagDemo.CreateAndAddToConcurrentBagConcurrently(); + + ConcurrentBagDemo.ConcurrentBagClearMethod(bag); + + Assert.True(bag.IsEmpty); + Assert.Empty(bag); + } + [Fact] public void GivenAConcurrentBag_WhenCopyingToAnArray_ThenReturnsAnArray() { diff --git a/collections-csharp/ConcurrentBagInCSharp/Tests/Tests.csproj b/collections-csharp/ConcurrentBagInCSharp/Tests/Tests.csproj index ebcbe28532..6084a8665e 100644 --- a/collections-csharp/ConcurrentBagInCSharp/Tests/Tests.csproj +++ b/collections-csharp/ConcurrentBagInCSharp/Tests/Tests.csproj @@ -1,7 +1,7 @@ - net7.0 + net10.0 enable enable @@ -9,13 +9,13 @@ - - - + + + runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all From bbf2a714a7acaff1dcd37eaaa2e3fef43742aa5b Mon Sep 17 00:00:00 2001 From: Vladimir Pecanac Date: Tue, 15 Sep 2026 15:45:36 +0200 Subject: [PATCH 2/2] Add the ordering demonstration: own-queue LIFO drain and a steal from a dedicated thread DrainOwnQueue fills a bag and drains it on the same thread, which returns the items last in, first out. DrainStolenQueue fills the bag on one dedicated thread and drains it on another, so the consumer steals from the head of the producer's queue and the items come back oldest first. Both use dedicated Thread objects with Join(), never Task.Run: the pool is free to hand the producer and the consumer to the same thread, which makes the consumer pop its own queue LIFO instead of stealing and the assertion intermittently red. Two tests added. The suite runs 10 of 10 on five consecutive runs. --- .../ConcurrentBagDemo.cs | 48 +++++++++++++++++++ .../Tests/ConcurrentBagInCSharpTest.cs | 16 +++++++ 2 files changed, 64 insertions(+) diff --git a/collections-csharp/ConcurrentBagInCSharp/ConcurrentBagInCSharp/ConcurrentBagDemo.cs b/collections-csharp/ConcurrentBagInCSharp/ConcurrentBagInCSharp/ConcurrentBagDemo.cs index a4463c3ac5..3eedaecb6c 100644 --- a/collections-csharp/ConcurrentBagInCSharp/ConcurrentBagInCSharp/ConcurrentBagDemo.cs +++ b/collections-csharp/ConcurrentBagInCSharp/ConcurrentBagInCSharp/ConcurrentBagDemo.cs @@ -82,6 +82,54 @@ public static void AccessItemFromAConcurrentBagConcurrently(ConcurrentBag b }); } + public static List DrainOwnQueue() + { + var bag = new ConcurrentBag(); + for (var i = 1; i <= 5; i++) + { + bag.Add(i); + } + + var taken = new List(); + while (bag.TryTake(out var item)) + { + taken.Add(item); + } + + return taken; + } + + public static List DrainStolenQueue() + { + var bag = new ConcurrentBag(); + var taken = new List(); + + // Dedicated threads, never Task.Run: the thread pool is free to hand both pieces of + // work to the same thread, and a consumer running on the producer's thread pops its + // own queue last in, first out instead of stealing. + var producer = new Thread(() => + { + for (var i = 0; i < 5; i++) + { + bag.Add(i); + } + }); + producer.Start(); + producer.Join(); + + var consumer = new Thread(() => + { + while (bag.TryTake(out var item)) + { + taken.Add(item); + } + }); + consumer.Start(); + consumer.Join(); + + return taken; + } + public static int[] ConcurrentBagToArrayMethod(ConcurrentBag bag) { return bag.ToArray(); diff --git a/collections-csharp/ConcurrentBagInCSharp/Tests/ConcurrentBagInCSharpTest.cs b/collections-csharp/ConcurrentBagInCSharp/Tests/ConcurrentBagInCSharpTest.cs index 2f2ac57395..c8c0846fca 100644 --- a/collections-csharp/ConcurrentBagInCSharp/Tests/ConcurrentBagInCSharpTest.cs +++ b/collections-csharp/ConcurrentBagInCSharp/Tests/ConcurrentBagInCSharpTest.cs @@ -63,6 +63,22 @@ public void GivenAConcurrentBag_WhenConvertingToAnArray_ThenReturnsAnArray() Assert.NotEmpty(result); } + [Fact] + public void GivenAConcurrentBagFilledOnOneThread_WhenDrainingOnThatThread_ThenReturnsItemsLastInFirstOut() + { + var result = ConcurrentBagDemo.DrainOwnQueue(); + + Assert.Equal(new List { 5, 4, 3, 2, 1 }, result); + } + + [Fact] + public void GivenAConcurrentBagFilledOnOneThread_WhenDrainingOnADifferentThread_ThenReturnsItemsOldestFirst() + { + var result = ConcurrentBagDemo.DrainStolenQueue(); + + Assert.Equal(new List { 0, 1, 2, 3, 4 }, result); + } + [Fact] public void GivenAConcurrentBag_WhenClearingAConcurrentBag_ThenReturnsAnEmptyConcurrentBag() {