I have 2 dictionaries in a scoped service within a Blazor server application I use to manage state for multi tenancy. It has come to my attention that there may be concurrency issues with users modifying the dictionaries on different threads.
public Dictionary<string, GlobalDataModel> Global { get; } = new();
public Dictionary<string, Dictionary<long, LocalDataModel>> Local { get; } = new();
I think I'm leaning towards leaving these as normal dictionaries and locking sync root when modifying or iterating over.
If I were to add an item to a collection within the containing class, would it be:
if (Local.ContainsKey(tenantId) == false)
{
lock (syncRoot)
{
Local.Add(tenantId, new Dictionary<long, LocalDataModel>());
}
}
or:
lock (syncRoot)
{
if (Local.ContainsKey(tenantId) == false)
{
{
Local.Add(tenantId, new Dictionary<long, LocalDataModel>());
}
}
}
Then if I were to copy different parts of the service collection to lists from an external class so it can be safely iterated would I just lock at the Service level, the Dictionary level, or the DataModel level, or is it dependent?
Say I wanted a resources collection within a specific project. Would it be:
[Inject] private IDataAdaptor DataAdaptor { get; set; };
var resources;
lock (DataAdaptor)
{
resources = DataAdaptor.Local[TenantId][ProjectId].Resources;
}
or:
lock (DataAdaptor.Local[TenantId][ProjectId].Resources)
{
resources = DataAdaptor.Local[TenantId][ProjectId].Resources;
}
And vice versa for different parts of the collection, et Tenants, Projects etc...
I assume I have to lock on the object because syncRoot
isn't accessible outside the containing class, or do I create a SyncRoot
object in the class where I'm copying and lock on that?
Obviously multi threading is a new concept.
CodePudding user response:
The Dictionary<TKey,TValue>
is thread-safe only when used by multiple readers and zero writers. As soon as you add a writer in the mix, in other words if the dictionary is not frozen and can be mutated, then it is thread-safe for nothing. Every access to the dictionary, even the slightest touch like reading its Count
, should be synchronized, otherwise its behavior is undefined.
Synchronizing a dictionary can be quite inefficient, especially if you have to enumerate it and do something with each of its entries. In this case you have to either create a copy of the dictionary (.ToArray()
) and enumerate the copy, or lock
for the whole duration of the iteration, blocking all other threads that might want to do trivial reads or writes. Alternative collections that are better suited for multithreaded environments are the ConcurrentDictionary<TKey,TValue>
and the ImmutableDictionary<TKey,TValue>
.
In case you want to educate yourself systematically about thread-safety, here is an online resource: Threading in C# - Thread Safety by Joseph Albahari.