BlockingCollection (t) performance
Pendant un certain temps dans mon entreprise, nous avons utilisé une implémentation ObjectPool<T>
locale qui permet de bloquer l'accès à son contenu. C'est assez simple: un Queue<T>
, un object
pour verrouiller, et un AutoResetEvent
pour signaler à un "emprunt" de fil lorsqu'un élément est ajouté.
La viande de la classe est vraiment ces deux méthodes:
public T Borrow() {
lock (_queueLock) {
if (_queue.Count > 0)
return _queue.Dequeue();
}
_objectAvailableEvent.WaitOne();
return Borrow();
}
public void Return(T obj) {
lock (_queueLock) {
_queue.Enqueue(obj);
}
_objectAvailableEvent.Set();
}
Nous avons utilisé ceci et quelques autres classes de collection au lieu de celles fournies par System.Collections.Concurrent
parce que nous utilisons.net 3.5, pas 4.0. Mais récemment, nous avons découvert que puisque nous utilisons Extensions réactives , nous avons en fait l'espace de noms Concurrent
à notre disposition (dans le système.Le filetage.DLL).
Naturellement, je me suis dit que depuis BlockingCollection<T>
est l'une des classes de base de l'espace de noms Concurrent
, elle offrirait probablement de meilleures performances que tout ce que moi ou mes coéquipiers avons écrit.
J'ai donc essayé d'écrire une nouvelle implémentation qui fonctionne très simplement:
public T Borrow() {
return _blockingCollection.Take();
}
public void Return(T obj) {
_blockingCollection.Add(obj);
}
À ma grande surprise, selon quelques simples tests (emprunt / retour au pool quelques milliers de fois à partir de plusieurs threads), notre implémentation originale Bat significativement BlockingCollection<T>
en termes de performance. Ils semblent tous deux fonctionner correctement ; c'est juste que notre implémentation originale semble être beaucoup plus rapide.
Ma question:
- Pourquoi en serait-il? Est-ce peut-être parce que
BlockingCollection<T>
offre une plus grande flexibilité (je comprends que cela fonctionne en enveloppant unIProducerConsumerCollection<T>
), ce qui introduit nécessairement frais généraux de performance? - Est-ce juste une utilisation erronée de la classe
BlockingCollection<T>
? - Si c'est une utilisation appropriée de
BlockingCollection<T>
, est-ce que je n'utilise pas correctement? Par exemple, est leTake
/Add
approche trop simpliste, et il y a un moyen beaucoup plus performant d'obtenir la même fonctionnalité?
À moins que quelqu'un n'ait un aperçu à offrir en réponse à cette troisième question, il semble que nous nous en tiendrons à notre implémentation originale pour l'instant.
2 réponses
Il y a quelques possibilités potentielles, ici.
Tout d'abord, BlockingCollection<T>
dans les Extensions réactives est un backport, et pas exactement le même que la version finale.net 4. Je ne serais pas surpris si la performance de ce backport diffère de.Net 4 RTM (bien que je n'ai pas profilé cette collection, spécifiquement). Une grande partie du TPL fonctionne mieux dans. NET 4 que dans le backport. net 3.5.
Cela étant dit, je soupçonne que votre implémentation surpassera {[0] } si vous avez un fil de production unique et un fil de consommation unique. Avec un producteur et un consommateur, votre verrou aura un impact plus faible sur la performance totale, et l'événement de réinitialisation est un moyen très efficace d'attendre du côté des consommateurs.
Cependant, BlockingCollection<T>
est conçu pour permettre à de nombreux threads producteurs de très bien "mettre en file d'attente" les données. Cela ne fonctionnera pas bien avec votre implémentation, car la contention de verrouillage commencera à devenir problématique assez rapidement.
Cela étant dit, Je encore une idée fausse ici:
... il offrirait probablement de meilleures performances que tout ce que moi ou mes coéquipiers avons écrit.
Ce n'est souvent pas vrai. Les classes de collection framework fonctionnent généralement très bien , mais ne sont souvent pas l'option la plus performante pour un scénario donné. Cela étant dit, ils ont tendance à bien performer tout en étant très flexibles et très robustes. Ils ont souvent tendance à évoluer très bien. Classes de collection" Home-written" souvent surpasser les collections de cadre dans des scénarios spécifiques, mais ont tendance à être problématique lorsqu'ils sont utilisés dans des scénarios en dehors de celui pour lequel ils ont été spécifiquement conçus. Je soupçonne que c'est une de ces situations.
J'ai essayé BlockingCollection
contre un combo ConurrentQueue/AutoResetEvent
(similaire à la solution D'OP, mais sans verrouillage) dans.Net 4, et ce dernier combo était donc beaucoup plus rapide pour mon cas d'utilisation, que J'ai abandonné BlockingCollection. Malheureusement, c'était il y a presque un an et je n'ai pas pu trouver les résultats de référence.
L'utilisation d'un AutoResetEvent séparé ne rend pas les choses trop compliquées. En fait, on pourrait même l'abstraire, une fois pour toutes, en un BlockingCollectionSlim
....
BlockingCollection repose en interne sur une ConcurrentQueue aussi, mais fait {[13] } un jonglage supplémentaire avec sémaphores minceset jetons d'Annulation, ce qui donne des fonctionnalités supplémentaires, mais à un coût, même lorsqu'il n'est pas utilisé. Il convient également de noter que BlockingCollection n'est pas marié à ConcurrentQueue, mais peut également être utilisé avec d'autres implémenteurs de IProducerConsumerCollection
.
A (testé, mais pas encore durci au combat) unbounded bare bones blockingcollectionslim implémentation:
class BlockingCollectionSlim<T>
{
private readonly ConcurrentQueue<T> _queue = new ConcurrentQueue<T>();
private readonly AutoResetEvent _autoResetEvent = new AutoResetEvent(false);
public void Add(T item)
{
_queue.Enqueue(item);
_autoResetEvent.Set();
}
public T Take()
{
T item;
while (!_queue.TryDequeue(out item))
_autoResetEvent.WaitOne();
return item;
}
public bool TryTake(out T item, TimeSpan patience)
{
if (_queue.TryDequeue(out item))
return true;
var stopwatch = Stopwatch.StartNew();
while (stopwatch.Elapsed < patience)
{
if (_queue.TryDequeue(out item))
return true;
var patienceLeft = (patience - stopwatch.Elapsed);
if (patienceLeft <= TimeSpan.Zero)
break;
else if (patienceLeft < MinWait)
// otherwise the while loop will degenerate into a busy loop,
// for the last millisecond before patience runs out
patienceLeft = MinWait;
_autoResetEvent.WaitOne(patienceLeft);
}
return false;
}
private static readonly TimeSpan MinWait = TimeSpan.FromMilliseconds(1);