Una colección observable 'ThreadSafe' - C #

Oct 28 2020

Dado que no existe una colección concurrente en .net que permita eliminar elementos específicos, he reunido la siguiente clase.

Es importante tener en cuenta que solo es seguro para subprocesos hasta que LockTimeoutexpire la operación en una determinada.

El objetivo principal era protegerse contra las descaradas excepciones 'InvalidOperationException: Collection Was Modified', que ocurren si estoy enumerando en un hilo y agregando / quitando en otro.

He establecido el valor predeterminado LockTimeoutde 10 segundos, pero en realidad, 1 segundo aún sería suficiente (al menos en mi caso de usuario).

Por último, esta implementación específica también incorpora INotifyCollectionChangedy 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();
        }
    }
}

Respuestas

3 Johnbot Oct 29 2020 at 14:20

El Waitmétodo no es correcto. Si el tiempo de espera expira, el semáforo se libera sin ser tomado. Esto puede permitir que el código se ejecute inmediatamente después porque hay una ranura disponible (no realmente como ya se tomó, pero se lanzó por error) pero en el futuro, cuando Releasese llame, fallará porque el semáforo ya alcanzó su tamaño máximo.

private void Wait()
{
    while (!_semaphore.Wait(LOCK_TIMEOUT))
        _semaphore.Release();
}

Cada llamada exitosa a Wait debe estar emparejada con una llamada a Release. Una Waitdevolución falsa no tiene éxito.