I am trying to write a program where I schedule items for removal by putting them in a collection from different threads and cleaning them up in a single thread that iterates of the collection and disposes the items.
Before doing so, I wondered what would yield the optimal performance so I've tried ConcurrentBag
, ConcurrentStack
and ConcurrentQueue
and measured the time required to add 10,000,000 items.
I used the following program to test this:
class Program
{
static List<int> list = new List<int>();
static ConcurrentBag<int> bag = new ConcurrentBag<int>();
static ConcurrentStack<int> stack = new ConcurrentStack<int>();
static ConcurrentQueue<int> queue = new ConcurrentQueue<int>();
static void Main(string[] args)
{
run(addList);
run(addBag);
run(addStack);
run(addQueue);
Console.ReadLine();
}
private static void addList(int obj) { lock (list) { list.Add(obj); } }
private static void addStack(int obj) { stack.Push(obj); }
private static void addQueue(int obj) { queue.Enqueue(obj); }
private static void addBag(int obj) { bag.Add(obj); }
private static void run(Action<int> action)
{
Stopwatch stopwatch = Stopwatch.StartNew();
Parallel.For(0, 10000000, new ParallelOptions() { MaxDegreeOfParallelism = # },
action);
stopwatch.Stop();
Console.WriteLine(action.Method.Name + " takes " + stopwatch.Elapsed);
}
}
where # is the amount of threads used.
But the results are rather confusing:
With 8 threads:
- addList takes 00:00:00.8166816
- addBag takes 00:00:01.0368712
- addStack takes 00:00:01.0902852
- addQueue takes 00:00:00.6555039
With 1 thread:
- addList takes 00:00:00.3880958
- addBag takes 00:00:01.5850249
- addStack takes 00:00:01.2764924
- addQueue takes 00:00:00.4409501
So, no matter how many threads, it seems that just locking a plain old list is faster then using any of the concurrent collections, except, perhaps the queue if it needs to handle a lot of writes.
EDIT: after the comments below about Garbage and Debug build: Yes, this influences the benchmark. Debug build influence would be linear, Garbage would be increasing with higher memory usage.
Yet running the same test multiple times gives roughly the same results.
I moved the initialization of the collection to right before the test run and collect the garbage after the run now, like this:
list = new List<int>();
run(addList);
list = null;
GC.Collect();
With MaxDegreeOfParallelism
set to 8 I get the following results:
- addList takes 00:00:7959546
- addBag takes 00:00:01.08023823
- addStack takes 00:00:01.1354566
- addQueue takes 00:00:00.6597145
with give or take 0.02 seconds deviation each time I run the code.