web-dev-qa-db-fra.com

Collection observable rapide et sécurisée pour les threads

ObservableCollections déclenche des notifications pour chaque action effectuée sur elles. Premièrement, ils ne peuvent pas ajouter ou supprimer des appels en bloc, deuxièmement, ils ne sont pas thread-safe.

Est-ce que cela ne les ralentit pas? Cant nous avons une alternative plus rapide? Certains disent que ICollectionView est entouré d'une ObservableCollection est rapide? How true est cette affirmation.

26
user778654

ObservableCollection peut être rapide, s'il veut à. :-)

Le code ci-dessous est un très bon exemple de collection observable plus rapide et sécurisée pour les threads que vous pouvez étendre davantage à votre guise.

using System.Collections.Specialized;

public class FastObservableCollection<T> : ObservableCollection<T>
{
    private readonly object locker = new object();

    /// <summary>
    /// This private variable holds the flag to
    /// turn on and off the collection changed notification.
    /// </summary>
    private bool suspendCollectionChangeNotification;

    /// <summary>
    /// Initializes a new instance of the FastObservableCollection class.
    /// </summary>
    public FastObservableCollection()
        : base()
    {
        this.suspendCollectionChangeNotification = false;
    }

    /// <summary>
    /// This event is overriden CollectionChanged event of the observable collection.
    /// </summary>
    public override event NotifyCollectionChangedEventHandler CollectionChanged;

    /// <summary>
    /// This method adds the given generic list of items
    /// as a range into current collection by casting them as type T.
    /// It then notifies once after all items are added.
    /// </summary>
    /// <param name="items">The source collection.</param>
    public void AddItems(IList<T> items)
    {
       lock(locker)
       {
          this.SuspendCollectionChangeNotification();
          foreach (var i in items)
          {
             InsertItem(Count, i);
          }
          this.NotifyChanges();
       }
    }

    /// <summary>
    /// Raises collection change event.
    /// </summary>
    public void NotifyChanges()
    {
        this.ResumeCollectionChangeNotification();
        var arg
             = new NotifyCollectionChangedEventArgs
                  (NotifyCollectionChangedAction.Reset);
        this.OnCollectionChanged(arg);
    }

    /// <summary>
    /// This method removes the given generic list of items as a range
    /// into current collection by casting them as type T.
    /// It then notifies once after all items are removed.
    /// </summary>
    /// <param name="items">The source collection.</param>
    public void RemoveItems(IList<T> items)
    {
        lock(locker)
        {
           this.SuspendCollectionChangeNotification();
           foreach (var i in items)
           {
             Remove(i);
           }
           this.NotifyChanges();
        }
    }

    /// <summary>
    /// Resumes collection changed notification.
    /// </summary>
    public void ResumeCollectionChangeNotification()
    {
        this.suspendCollectionChangeNotification = false;
    }

    /// <summary>
    /// Suspends collection changed notification.
    /// </summary>
    public void SuspendCollectionChangeNotification()
    {
        this.suspendCollectionChangeNotification = true;
    }

    /// <summary>
    /// This collection changed event performs thread safe event raising.
    /// </summary>
    /// <param name="e">The event argument.</param>
    protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
    {
        // Recommended is to avoid reentry 
        // in collection changed event while collection
        // is getting changed on other thread.
        using (BlockReentrancy())
        {
            if (!this.suspendCollectionChangeNotification)
            {
                NotifyCollectionChangedEventHandler eventHandler = 
                      this.CollectionChanged;
                if (eventHandler == null)
                {
                    return;
                }

                // Walk thru invocation list.
                Delegate[] delegates = eventHandler.GetInvocationList();

                foreach
                (NotifyCollectionChangedEventHandler handler in delegates)
                {
                    // If the subscriber is a DispatcherObject and different thread.
                    DispatcherObject dispatcherObject
                         = handler.Target as DispatcherObject;

                    if (dispatcherObject != null
                           && !dispatcherObject.CheckAccess())
                    {
                        // Invoke handler in the target dispatcher's thread... 
                        // asynchronously for better responsiveness.
                        dispatcherObject.Dispatcher.BeginInvoke
                              (DispatcherPriority.DataBind, handler, this, e);
                    }
                    else
                    {
                        // Execute handler as is.
                        handler(this, e);
                    }
                }
            }
        }
    }
}

De plus, ICollectionView situé au-dessus de ObservableCollection est activement au courant des modifications et effectue un filtrage, un regroupement et un tri relativement rapides par rapport à toute autre liste source.

Encore une fois, les collections observables peuvent ne pas être une réponse parfaite pour des mises à jour plus rapides des données, mais elles font plutôt bien leur travail.

72
WPF-it

Voici une compilation de quelques solutions que j'ai faites. L'idée de collection a changé l'invocation prise à partir de la première réponse.

Il semble également que l'opération "Reset" devrait être synchrone avec le fil principal, sans quoi des choses étranges arriveraient à CollectionView et CollectionViewSource. 

Je pense que cela est dû au fait que le gestionnaire "Réinitialiser" tente de lire immédiatement le contenu de la collection et qu'il devrait être déjà en place. Si vous effectuez une réinitialisation asynchrone et que vous ajoutez immédiatement certains éléments également asynchrones, les éléments nouvellement ajoutés peuvent être ajoutés deux fois.

public interface IObservableList<T> : IList<T>, INotifyCollectionChanged
{
}

public class ObservableList<T> : IObservableList<T>
{
    private IList<T> collection = new List<T>();
    public event NotifyCollectionChangedEventHandler CollectionChanged;
    private ReaderWriterLock sync = new ReaderWriterLock();

    protected virtual void OnCollectionChanged(NotifyCollectionChangedEventArgs args)
    {
        if (CollectionChanged == null)
            return;
        foreach (NotifyCollectionChangedEventHandler handler in CollectionChanged.GetInvocationList())
        {
            // If the subscriber is a DispatcherObject and different thread.
            var dispatcherObject = handler.Target as DispatcherObject;

            if (dispatcherObject != null && !dispatcherObject.CheckAccess())
            {
                if ( args.Action == NotifyCollectionChangedAction.Reset )
                    dispatcherObject.Dispatcher.Invoke
                          (DispatcherPriority.DataBind, handler, this, args);
                else
                    // Invoke handler in the target dispatcher's thread... 
                    // asynchronously for better responsiveness.
                    dispatcherObject.Dispatcher.BeginInvoke
                          (DispatcherPriority.DataBind, handler, this, args);
            }
            else
            {
                // Execute handler as is.
                handler(this, args);
            }
        }
    }

    public ObservableList()
    {
    }

    public void Add(T item)
    {
        sync.AcquireWriterLock(Timeout.Infinite);
        try
        {
            collection.Add(item);
            OnCollectionChanged(
                    new NotifyCollectionChangedEventArgs(
                      NotifyCollectionChangedAction.Add, item));
        }
        finally
        {
            sync.ReleaseWriterLock();
        }
    }

    public void Clear()
    {
        sync.AcquireWriterLock(Timeout.Infinite);
        try
        {
            collection.Clear();
            OnCollectionChanged(
                    new NotifyCollectionChangedEventArgs(
                        NotifyCollectionChangedAction.Reset));
        }
        finally
        {
            sync.ReleaseWriterLock();
        }
    }

    public bool Contains(T item)
    {
        sync.AcquireReaderLock(Timeout.Infinite);
        try
        {
            var result = collection.Contains(item);
            return result;
        }
        finally
        {
            sync.ReleaseReaderLock();
        }
    }

    public void CopyTo(T[] array, int arrayIndex)
    {
        sync.AcquireWriterLock(Timeout.Infinite);
        try
        {
            collection.CopyTo(array, arrayIndex);
        }
        finally
        {
            sync.ReleaseWriterLock();
        }
    }

    public int Count
    {
        get
        {
            sync.AcquireReaderLock(Timeout.Infinite);
            try
            {
                return collection.Count;
            }
            finally
            {
                sync.ReleaseReaderLock();
            }
        }
    }

    public bool IsReadOnly
    {
        get { return collection.IsReadOnly; }
    }

    public bool Remove(T item)
    {
        sync.AcquireWriterLock(Timeout.Infinite);
        try
        {
            var index = collection.IndexOf(item);
            if (index == -1)
                return false;
            var result = collection.Remove(item);
            if (result)
                OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, item, index));
            return result;
        }
        finally
        {
            sync.ReleaseWriterLock();
        }
    }

    public IEnumerator<T> GetEnumerator()
    {
        return collection.GetEnumerator();
    }

    System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
    {
        return collection.GetEnumerator();
    }

    public int IndexOf(T item)
    {
        sync.AcquireReaderLock(Timeout.Infinite);
        try
        {
            var result = collection.IndexOf(item);
            return result;
        }
        finally
        {
            sync.ReleaseReaderLock();
        }
    }

    public void Insert(int index, T item)
    {
        sync.AcquireWriterLock(Timeout.Infinite);
        try
        {
            collection.Insert(index, item);
            OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item, index));
        }
        finally
        {
            sync.ReleaseWriterLock();
        }
    }

    public void RemoveAt(int index)
    {
        sync.AcquireWriterLock(Timeout.Infinite);
        try
        {
            if (collection.Count == 0 || collection.Count <= index)
                return;
            var item = collection[index];
            collection.RemoveAt(index);
            OnCollectionChanged(
                    new NotifyCollectionChangedEventArgs(
                       NotifyCollectionChangedAction.Remove, item, index));
        }
        finally
        {
            sync.ReleaseWriterLock();
        }
    }

    public T this[int index]
    {
        get
        {
            sync.AcquireReaderLock(Timeout.Infinite);
            try
            {
                var result = collection[index];
                return result;
            }
            finally
            {
                sync.ReleaseReaderLock();
            }
        }
        set
        {
            sync.AcquireWriterLock(Timeout.Infinite);
            try
            {
                if (collection.Count == 0 || collection.Count <= index)
                    return;
                var item = collection[index];
                collection[index] = value;
                OnCollectionChanged(
                        new NotifyCollectionChangedEventArgs(
                           NotifyCollectionChangedAction.Replace, value, item, index));
            }
            finally
            {
                sync.ReleaseWriterLock();
            }
        }

    }
}
4
norekhov

Je ne peux pas ajouter de commentaires car je ne suis pas encore assez cool, mais le fait de partager ce problème que j'ai rencontré vaut probablement la peine d'être publié même si ce n'est pas vraiment une réponse. J'ai continué à recevoir une exception "Index était hors limite" en utilisant cette FastObservableCollection, à cause de BeginInvoke. Apparemment, les modifications en cours de notification peuvent être annulées avant que le gestionnaire ne soit appelé. C'est pourquoi, j'ai résolu comme suit le quatrième paramètre de BeginInvoke appelé à partir de la méthode OnCollectionChanged (au lieu d'utiliser l'argument event args):

dispatcherObject.Dispatcher.BeginInvoke
                          (DispatcherPriority.DataBind, handler, this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));

Au lieu de cela:

dispatcherObject.Dispatcher.BeginInvoke
                          (DispatcherPriority.DataBind, handler, this, e);

Cela a résolu le problème "Index était hors de portée" que je rencontrais. Voici une explication plus détaillée/code snpipet: Où puis-je obtenir un CollectionView thread-safe?

2
jsirr13

Un exemple où est créée une liste Observable synchronisée:

newSeries = new XYChart.Series<>();
ObservableList<XYChart.Data<Number, Number>> listaSerie;
listaSerie = FXCollections.synchronizedObservableList(FXCollections.observableList(new ArrayList<XYChart.Data<Number, Number>>()));
newSeries.setData(listaSerie);
0
Raul