You can absolutely be thread-safe in your use of immutable dictionary. The data structure itself is perfectly thread-safe, but you applying changes to it in a multi-threaded environment must be carefully written to avoid data loss in your own code.
Here is a pattern I use frequently for just such a scenario. It requires no locks, since the only mutation we do is a single memory assignment. If you have to set multiple fields, you need to use a lock.
using System.Threading;
public class Something {
private ImmutableDictionary<string, string> dict = ImmutableDictionary<string, string>.Empty;
public void Add(string key, string value) {
// It is important that the contents of this loop have no side-effects
// since they can be repeated when a race condition is detected.
do {
var original = _dict;
if (local.ContainsKey(key)) {
return;
}
var changed = original.Add(key,value);
// The while loop condition will try assigning the changed dictionary
// back to the field. If it hasn't changed by another thread in the
// meantime, we assign the field and break out of the loop. But if another
// thread won the race (by changing the field while we were in an
// iteration of this loop), we'll loop and try again.
} while (Interlocked.CompareExchange(ref this.dict, changed, original) != original);
}
}
In fact, I use this pattern so frequently I've defined a static method for this purpose:
/// <summary>
/// Optimistically performs some value transformation based on some field and tries to apply it back to the field,
/// retrying as many times as necessary until no other thread is manipulating the same field.
/// </summary>
/// <typeparam name="T">The type of data.</typeparam>
/// <param name="hotLocation">The field that may be manipulated by multiple threads.</param>
/// <param name="applyChange">A function that receives the unchanged value and returns the changed value.</param>
public static bool ApplyChangeOptimistically<T>(ref T hotLocation, Func<T, T> applyChange) where T : class
{
Requires.NotNull(applyChange, "applyChange");
bool successful;
do
{
Thread.MemoryBarrier();
T oldValue = hotLocation;
T newValue = applyChange(oldValue);
if (Object.ReferenceEquals(oldValue, newValue))
{
// No change was actually required.
return false;
}
T actualOldValue = Interlocked.CompareExchange<T>(ref hotLocation, newValue, oldValue);
successful = Object.ReferenceEquals(oldValue, actualOldValue);
}
while (!successful);
Thread.MemoryBarrier();
return true;
}
Your Add method then gets much simpler:
public class Something {
private ImmutableDictionary<string, string> dict = ImmutableDictionary<string, string>.Empty;
public void Add(string key, string value) {
ApplyChangeOptimistically(
ref this.dict,
d => d.ContainsKey(key) ? d : d.Add(key, value));
}
}