Une collection observable 'ThreadSafe' - C #

Oct 28 2020

Voyant qu'une collection simultanée n'existe pas dans .net qui permet de supprimer des éléments spécifiques, j'ai rassemblé la classe suivante.

Il est important de noter que ce n'est que threadsafe jusqu'à ce LockTimeoutqu'une opération donnée expire.

L'objectif principal était de se prémunir contre les exceptions effrontées «InvalidOperationException: Collection Was Modified», qui se produisent si j'énumère dans un thread et que j'ajoute / supprime dans un autre.

J'ai réglé par défaut les LockTimeout10 secondes, mais en réalité, 1 seconde serait encore suffisante (dans mon cas utilisateur au moins).

Enfin, cette implémentation spécifique intègre également INotifyCollectionChangedet 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();
        }
    }
}

Réponses

3 Johnbot Oct 29 2020 at 14:20

La Waitméthode n'est pas correcte. Si le délai expire, le sémaphore est libéré sans être pris. Cela peut permettre au code de s'exécuter immédiatement après car il y a un emplacement disponible (pas vraiment tel qu'il a déjà été pris mais publié par erreur) mais à l'avenir, quand Releaseil sera appelé, il échouera car le sémaphore a déjà atteint sa taille maximale.

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

Chaque appel réussi à Wait doit être associé à un appel à Release. Un Waitretour faux échoue.