For my question I have prepared a C# Fiddle and when you click “Run” there few times, you will see the exception in the log.
If you however compile and run my code listed below in Visual Studio 2022 or VS Code, then the following exception is thrown:
System.InvalidOperationException
HResult=0x80131509
Message=Operations that change non-concurrent collections must have exclusive access. A concurrent update was performed on this collection and corrupted its state. The collection's state is no longer correct.
Source=System.Private.CoreLib
StackTrace:
at System.ThrowHelper.ThrowInvalidOperationException_ConcurrentOperationsNotSupported()
at System.Collections.Generic.Dictionary`2.TryInsert(TKey key, TValue value, InsertionBehavior behavior)
at System.Collections.Generic.Dictionary`2.set_Item(TKey key, TValue value)
at SyncContextTest.PlayloadAccumulationHandler.AddCallback(Object state) in PlayloadAccumulationHandler.cs:line 38
at System.Threading.QueueUserWorkItemCallbackDefaultContext`1.Execute()
at System.Threading.ThreadPoolWorkQueue.Dispatch()
at System.Threading.PortableThreadPool.WorkerThread.WorkerThreadStart()
Here is my simple test case –
In Program.cs I create 100 threads and then make them call handler.Add()
:
class Program
{
private const int NUM_THREADS = 100;
private static PayloadAccumulationHandler handler = new();
static void Main(string[] args)
{
ThreadStart ts = new(RunMe);
Thread[] threads = Enumerable.Range(0, NUM_THREADS)
.Select(_ => new Thread(ts))
.ToArray();
foreach (Thread t in threads)
{
t.Start();
}
foreach (Thread t in threads)
{
t.Join();
}
}
private static void RunMe()
{
int threadId = Environment.CurrentManagedThreadId;
handler.Add("microsoft", "de", (uint)(threadId % 3));
}
}
And in the PayloadAccumulationHandler.cs I try to use SynchronizationContext.Post():
public class PayloadAccumulationHandler
{
private static readonly SynchronizationContext SyncContextTest = SynchronizationContext.Current ?? new SynchronizationContext();
private static Dictionary<string, uint> dict = [];
public void Add(string brand, string country, uint useCaseId)
{
Console.WriteLine($"Add({brand}, {country}, {useCaseId})");
string key = $"{brand}/{country}/{useCaseId}";
SyncContextTest.Post(AddCallback, key);
}
public void AddCallback(object? state)
{
int threadId = Environment.CurrentManagedThreadId;
if (state is string key)
{
Console.WriteLine($"AddCallback: {threadId} {key}");
if (dict.TryGetValue(key, out uint value))
{
dict[key] = value + 1;
}
else
{
dict[key] = 1; // throws System.InvalidOperationException!
}
}
}
}
The dict is of type Dictionary
and not ConcurrentDictionary
on purpose – because I wanted to verify that the code in AddCallback
method runs in the same thread.
But it is not! Am I doing something wrong or maybe assuming something wrong?
I was expecting SynchronizationContext.Post()
to act similar to Android handlers, where you post callbacks at the handler and it ensures that their code runs on the same thread.
Also, I have noticed that SynchronizationContext.Current
is null and the new SynchronizationContext()
is run in my console app. Should I somehow tell/configure that new instance to use a single thread for executing the callbacks?
8