Uma coleção observável 'ThreadSafe' - C #
Visto que não existe uma coleção simultânea em .net que permite que itens específicos sejam removidos, juntei a seguinte classe.
É importante observar que ele é apenas thread-safe até que LockTimeoutuma determinada operação expire.
O objetivo principal era evitar exceções atrevidas 'InvalidOperationException: Collection Was Modified', que ocorrem se eu estiver enumerando em um thread e adicionando / removendo em outro.
Eu padronizei o LockTimeoutpara 10 segundos, mas na realidade, 1 segundo ainda seria suficiente (no meu caso de usuário, pelo menos).
Por último, esta implementação específica também incorpora INotifyCollectionChangede INotifyPropertyChanged.
public class ThreadsafeObservableCollection<T> : IList<T>, INotifyCollectionChanged, INotifyPropertyChanged
{
public event NotifyCollectionChangedEventHandler CollectionChanged;
public event PropertyChangedEventHandler PropertyChanged;
private readonly ConcurrentQueue<PropertyChangedEventArgs> _propertyChangedEvents = new ConcurrentQueue<PropertyChangedEventArgs>();
private readonly ConcurrentQueue<NotifyCollectionChangedEventArgs> _collectionChangedEvents = new ConcurrentQueue<NotifyCollectionChangedEventArgs>();
private readonly ObservableCollection<T> _collection;
private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1, 1);
private static TimeSpan LOCK_TIMEOUT = TimeSpan.FromSeconds(10);
public ThreadsafeObservableCollection()
{
_collection = new ObservableCollection<T>();
_collection.CollectionChanged += _collection_CollectionChanged;
(_collection as INotifyPropertyChanged).PropertyChanged += _collection_PropertyChanged;
}
private void Wait()
{
while (!_semaphore.Wait(LOCK_TIMEOUT))
_semaphore.Release();
}
public void Add(T item)
{
Wait();
try
{
_collection.Add(item);
}
finally
{
_semaphore.Release();
}
FireOutstandingEvents();
}
public void Clear()
{
Wait();
try
{
_collection.Clear();
}
finally
{
_semaphore.Release();
}
FireOutstandingEvents();
}
public bool Contains(T item)
{
Wait();
bool result;
try
{
result = _collection.Contains(item);
}
finally
{
_semaphore.Release();
}
FireOutstandingEvents();
return result;
}
public int Count
{
get
{
Wait();
int count;
try
{
count = _collection.Count;
}
finally
{
_semaphore.Release();
}
FireOutstandingEvents();
return count;
}
}
public bool IsReadOnly => false;
public T this[int index]
{
get
{
Wait();
T item;
try
{
item = _collection[index];
}
finally
{
_semaphore.Release();
}
FireOutstandingEvents();
return item;
}
set
{
Wait();
try
{
_collection[index] = value;
}
finally
{
_semaphore.Release();
}
FireOutstandingEvents();
}
}
public void CopyTo(T[] array, int arrayIndex)
{
Wait();
try
{
_collection.CopyTo(array, arrayIndex);
}
finally
{
_semaphore.Release();
}
FireOutstandingEvents();
}
public int IndexOf(T item)
{
Wait();
int index;
try
{
index = _collection.IndexOf(item);
}
finally
{
_semaphore.Release();
}
FireOutstandingEvents();
return index;
}
public void Insert(int index, T item)
{
Wait();
try
{
_collection.Insert(index, item);
}
finally
{
_semaphore.Release();
}
FireOutstandingEvents();
}
public bool Remove(T item)
{
Wait();
bool result;
try
{
result = _collection.Remove(item);
}
finally
{
_semaphore.Release();
}
FireOutstandingEvents();
return result;
}
public void RemoveAt(int index)
{
Wait();
try
{
_collection.RemoveAt(index);
}
finally
{
_semaphore.Release();
}
FireOutstandingEvents();
}
private void FireOutstandingEvents()
{
while (_propertyChangedEvents.TryDequeue(out PropertyChangedEventArgs arg))
PropertyChanged?.Invoke(this, arg);
while (_collectionChangedEvents.TryDequeue(out NotifyCollectionChangedEventArgs arg))
CollectionChanged?.Invoke(this, arg);
}
private void _collection_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
_propertyChangedEvents.Enqueue(e);
}
private void _collection_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
_collectionChangedEvents.Enqueue(e);
}
public IEnumerator<T> GetEnumerator()
{
Wait();
try
{
return ((IEnumerable<T>)_collection.ToArray()).GetEnumerator();
}
finally
{
_semaphore.Release();
}
}
IEnumerator IEnumerable.GetEnumerator()
{
Wait();
try
{
return _collection.ToArray().GetEnumerator();
}
finally
{
_semaphore.Release();
}
}
}
Respostas
O Waitmétodo não está correto. Se o tempo limite expirar, o semáforo será liberado sem ser obtido. Isso pode permitir que o código seja executado imediatamente depois, porque há um slot disponível (não realmente como já foi usado, mas erroneamente liberado), mas no futuro, quando Releasefor chamado, ele falhará porque o semáforo já atingiu seu tamanho máximo.
private void Wait()
{
while (!_semaphore.Wait(LOCK_TIMEOUT))
_semaphore.Release();
}
Cada chamada bem-sucedida para Wait deve ser emparelhada com uma chamada para Release. Um Waitretorno falso não é bem-sucedido.