Eine 'ThreadSafe'-beobachtbare Sammlung - C #

Oct 28 2020

Da es in .net keine gleichzeitige Sammlung gibt, mit der bestimmte Elemente entfernt werden können, habe ich die folgende Klasse zusammengestellt.

Es ist wichtig zu beachten, dass es nur threadsicher ist, bis der Vorgang LockTimeoutfür einen bestimmten Vorgang abläuft.

Das Hauptziel war es, sich vor frechen "InvalidOperationException: Collection Was Modified" -Ausnahmen zu schützen, die auftreten, wenn ich in einem Thread aufzähle und in einem anderen hinzufüge / entferne.

Ich habe die Standardeinstellung LockTimeoutauf 10 Sekunden festgelegt, aber in Wirklichkeit würde 1 Sekunde immer noch ausreichen (zumindest in meinem Benutzerfall).

Schließlich beinhaltet diese spezifische Implementierung auch INotifyCollectionChangedund 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();
        }
    }
}

Antworten

3 Johnbot Oct 29 2020 at 14:20

Die WaitMethode ist nicht korrekt. Wenn das Timeout abläuft, wird das Semaphor freigegeben, ohne genommen zu werden. Dies kann dazu führen, dass Code unmittelbar danach ausgeführt wird, da ein Steckplatz verfügbar ist (nicht wirklich, da er bereits belegt, aber fälschlicherweise freigegeben wurde), aber in Zukunft, wenn er Releaseaufgerufen wird, schlägt er fehl, weil das Semaphor bereits seine maximale Größe erreicht hat.

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

Jeder erfolgreiche Anruf bei Wait muss mit einem Anruf bei gekoppelt werden Release. Eine WaitRückgabe von false ist nicht erfolgreich.