There seems to be no practical way of removing an empty collection (even if it is synchronized) from a concurrent dictionary without having race condition issues. There are certain facts preventing this from being possible, as discussed in the comments under both the question and the OP's self answer.
What I wrote in my comment, however, seemed feasible and I wanted to give it a try.
I want to discuss the drawbacks of this implementation right after, and I should also say that your comments (if received any) are what is most valuable to me.
First, the usage:
static void Main(string[] args)
{
var myDictionary = new ConcurrentDictionary<string, IList<int>>();
IList<int> myList = myDictionary.AddSelfRemovingList<string, int>("myList");
myList.Add(5);
myList.Add(6);
myList.Remove(6);
myList.Remove(5);
IList<int> existingInstance;
// returns false:
bool exists = myDictionary.TryGetValue("myList", out existingInstance);
// throws HasAlreadyRemovedSelfException:
myList.Add(3);
}
AddSelfRemovingList
is an extension method to make things easier.
For the discussion part:
- It is not acceptable for the removal of an item from a collection to have a side effect of removing the collection reference from the owning dictionary.
- It is also not good practice to make the collection obsolete (unusable) when all its items are removed. There is a strong possibility that the consumer of the collection wants to clear and re-fill the collection and this implementation does not allow that.
- It forces the use of
IList<T>
abstraction and a custom implementation over List<T>
Although this provides a real thread-safe way of removing a just emptied collection from the dictionary, there seem to be more cons than pros to it. This should only be used in a closed context where the collections inside the concurrent dictionary are exposed to the outside, and where the immediate removal of a collection when emptied, even if some other thread is accessing it at the moment, is essential.
Here is the extension method to create and add the self removing list to the dictionary:
public static class ConcurrentDictionaryExtensions
{
public static IList<TValue> AddSelfRemovingList<TKey, TValue>(this ConcurrentDictionary<TKey, IList<TValue>> dictionaryInstance, TKey key)
{
var newInstance = new SelfRemovingConcurrentList<TKey, TValue>(dictionaryInstance, key);
if (!dictionaryInstance.TryAdd(key, newInstance))
{
throw new ArgumentException("ownerAccessKey", "The passed ownerAccessKey has already exist in the parent dictionary");
}
return newInstance;
}
}
And finally; here is the synchronized, self-removing implementation of IList<T>
:
public class SelfRemovingConcurrentList<TKey, TValue> : IList<TValue>
{
private ConcurrentDictionary<TKey, IList<TValue>> owner;
private TKey ownerAccessKey;
List<TValue> underlyingList = new List<TValue>();
private bool hasRemovedSelf;
public class HasAlreadyRemovedSelfException : Exception
{
}
internal SelfRemovingConcurrentList(ConcurrentDictionary<TKey, IList<TValue>> owner, TKey ownerAccessKey)
{
this.owner = owner;
this.ownerAccessKey = ownerAccessKey;
}
private void ThrowIfHasAlreadyRemovedSelf()
{
if (hasRemovedSelf)
{
throw new HasAlreadyRemovedSelfException();
}
}
[MethodImpl(MethodImplOptions.Synchronized)]
int IList<TValue>.IndexOf(TValue item)
{
ThrowIfHasAlreadyRemovedSelf();
return underlyingList.IndexOf(item);
}
[MethodImpl(MethodImplOptions.Synchronized)]
void IList<TValue>.Insert(int index, TValue item)
{
ThrowIfHasAlreadyRemovedSelf();
underlyingList.Insert(index, item);
}
[MethodImpl(MethodImplOptions.Synchronized)]
void IList<TValue>.RemoveAt(int index)
{
ThrowIfHasAlreadyRemovedSelf();
underlyingList.RemoveAt(index);
if (underlyingList.Count == 0)
{
hasRemovedSelf = true;
IList<TValue> removedInstance;
if (!owner.TryRemove(ownerAccessKey, out removedInstance))
{
// Just ignore.
// What we want to do is to remove ourself from the owner (concurrent dictionary)
// and it seems like we have already been removed!
}
}
}
TValue IList<TValue>.this[int index]
{
[MethodImpl(MethodImplOptions.Synchronized)]
get
{
ThrowIfHasAlreadyRemovedSelf();
return underlyingList[index];
}
[MethodImpl(MethodImplOptions.Synchronized)]
set
{
ThrowIfHasAlreadyRemovedSelf();
underlyingList[index] = value;
}
}
[MethodImpl(MethodImplOptions.Synchronized)]
void ICollection<TValue>.Add(TValue item)
{
ThrowIfHasAlreadyRemovedSelf();
underlyingList.Add(item);
}
[MethodImpl(MethodImplOptions.Synchronized)]
void ICollection<TValue>.Clear()
{
ThrowIfHasAlreadyRemovedSelf();
underlyingList.Clear();
hasRemovedSelf = true;
IList<TValue> removedInstance;
if (!owner.TryRemove(ownerAccessKey, out removedInstance))
{
// Just ignore.
// What we want to do is to remove ourself from the owner (concurrent dictionary)
// and it seems like we have already been removed!
}
}
[MethodImpl(MethodImplOptions.Synchronized)]
bool ICollection<TValue>.Contains(TValue item)
{
ThrowIfHasAlreadyRemovedSelf();
return underlyingList.Contains(item);
}
[MethodImpl(MethodImplOptions.Synchronized)]
void ICollection<TValue>.CopyTo(TValue[] array, int arrayIndex)
{
ThrowIfHasAlreadyRemovedSelf();
underlyingList.CopyTo(array, arrayIndex);
}
int ICollection<TValue>.Count
{
[MethodImpl(MethodImplOptions.Synchronized)]
get
{
ThrowIfHasAlreadyRemovedSelf();
return underlyingList.Count;
}
}
bool ICollection<TValue>.IsReadOnly
{
[MethodImpl(MethodImplOptions.Synchronized)]
get
{
ThrowIfHasAlreadyRemovedSelf();
return false;
}
}
[MethodImpl(MethodImplOptions.Synchronized)]
bool ICollection<TValue>.Remove(TValue item)
{
ThrowIfHasAlreadyRemovedSelf();
bool removalResult = underlyingList.Remove(item);
if (underlyingList.Count == 0)
{
hasRemovedSelf = true;
IList<TValue> removedInstance;
if (!owner.TryRemove(ownerAccessKey, out removedInstance))
{
// Just ignore.
// What we want to do is to remove ourself from the owner (concurrent dictionary)
// and it seems like we have already been removed!
}
}
return removalResult;
}
[MethodImpl(MethodImplOptions.Synchronized)]
IEnumerator<TValue> IEnumerable<TValue>.GetEnumerator()
{
ThrowIfHasAlreadyRemovedSelf();
return underlyingList.GetEnumerator();
}
[MethodImpl(MethodImplOptions.Synchronized)]
IEnumerator IEnumerable.GetEnumerator()
{
ThrowIfHasAlreadyRemovedSelf();
return underlyingList.GetEnumerator();
}
}