2010-06-14 14 views
20

Durante un tiempo en mi empresa hemos utilizado una implementación de fabricación propia ObjectPool<T> que proporciona acceso de bloqueo a sus contenidos. Es bastante sencillo: un Queue<T>, un object para bloquear, y un AutoResetEvent para señalizar un hilo "prestado" cuando se agrega un artículo.BlockingCollection (T) rendimiento

La carne de la clase es en realidad estos dos métodos:

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(); 
} 

Hemos estado utilizando esto y algunas otras clases de colección en lugar de los proporcionados por System.Collections.Concurrent porque estamos utilizando .NET 3.5, no 4.0. Pero recientemente descubrimos que, dado que estamos usando Reactive Extensions, en realidad tenemos con el espacio de nombres Concurrent disponible (en System.Threading.dll).

Naturalmente, pensé que como BlockingCollection<T> es una de las clases principales en el espacio de nombres Concurrent, probablemente ofrecería un mejor rendimiento que cualquier cosa que yo o mis compañeros de equipo escribieron.

así que traté de escribir una nueva aplicación que funciona de manera muy sencilla:

public T Borrow() { 
    return _blockingCollection.Take(); 
} 

public void Return(T obj) { 
    _blockingCollection.Add(obj); 
} 

Para mi sorpresa, según algunas pruebas simples (préstamos/volver a la piscina unas cuantas miles de veces desde múltiples hilos), nuestra original la implementación gana significativamente BlockingCollection<T> en términos de rendimiento. Ambos parecen funcionar correctamente; es solo que nuestra implementación original parece ser mucho más rápida.

Mi pregunta:

  1. ¿Por qué sería? ¿Es quizás porque BlockingCollection<T> ofrece una mayor flexibilidad (entiendo que funciona al envolver un IProducerConsumerCollection<T>), lo que necesariamente introduce una sobrecarga de rendimiento?
  2. ¿Es esto simplemente un uso desacertado de la clase BlockingCollection<T>?
  3. Si este es un uso apropiado de BlockingCollection<T>, ¿no lo estoy utilizando correctamente? Por ejemplo, ¿es el enfoque Take/Add demasiado simplista, y hay una forma mucho mejor de lograr la misma funcionalidad?

A menos que alguien tenga alguna idea que ofrecer en respuesta a esa tercera pregunta, parece que por el momento nos mantendremos con nuestra implementación original.

+0

Sin ver su índice de referencia, que es difícil hacer comentarios. Tal vez BlockingCollection está optimizado para scenarii que son diferentes de su punto de referencia? – Joe

+0

@Joe: scenarii? Jaja bueno. De todos modos, punto tomado - Proporcionaré un punto de referencia en breve. –

+2

¿Ha visto el documento técnico de Microsoft sobre las características de rendimiento de la colección .net 4? Podría ser un escenario no óptimo: http://blogs.msdn.com/b/pfxteam/archive/2010/04/26/9997562.aspx – piers7

Respuesta

23

Aquí hay un par de posibles posibilidades.

En primer lugar, BlockingCollection<T> en las extensiones reactivas es un backport, y no es exactamente igual a la versión final de .NET 4. No me sorprendería si el rendimiento de este backport difiere de .NET 4 RTM (aunque no he perfilado esta colección, específicamente). Gran parte del TPL tiene un mejor rendimiento en .NET 4 que en .NET 3.5 backport.

Dicho esto, sospecho que su implementación superará a BlockingCollection<T> si tiene un solo hilo de productor y un único hilo de consumidor. Con un productor y un consumidor, su bloqueo tendrá un impacto menor en el rendimiento total, y el evento de reinicio es un medio muy eficaz de esperar del lado del consumidor.

Sin embargo, BlockingCollection<T> está diseñado para permitir que muchos subprocesos del productor "encueteen" datos muy bien. Esto no funcionará bien con su implementación, ya que la disputa de bloqueo comenzará a ser problemática con bastante rapidez.

Dicho esto, también me gustaría señalar una idea equivocada aquí:

... es probablemente ofrecería un mejor rendimiento que cualquier cosa que yo o mis compañeros de equipo escribí.

Esto a menudo no es cierto. Las clases de recopilación de marcos normalmente realizan muy bien, pero a menudo no son la opción más eficaz para un escenario determinado. Dicho esto, tienden a tener un buen rendimiento, siendo muy flexibles y muy robustos. A menudo tienden a escalar muy bien. Las clases de colecciones "escritas en el hogar" a menudo superan a las colecciones de marcos en escenarios específicos, pero tienden a ser problemáticas cuando se usan en escenarios distintos de aquel para el que fueron diseñados específicamente. Sospecho que esta es una de esas situaciones.

+0

No veo cómo el código podría diferir de dos hilos frente a muchos hilos. Aún usarías las mismas construcciones de bloqueo. Presumiblemente él está probando ambos usando la misma cantidad de hilos. –

+5

@BC: BlockingCollection es en realidad una colección sin cerradura. Esto hace que se escale de forma bastante diferente a la implementación del OP. –

9

me trataron BlockingCollection contra un combinado ConurrentQueue/AutoResetEvent (similar a la solución de OP, pero lockless) en .Net 4, y el segundo combo era tan mucho más rápido para mi caso de uso, que Dejé BlockingCollection. Lamentablemente, esto fue hace casi un año y no pude encontrar los resultados de referencia.

El uso de un AutoResetEvent por separado no complica mucho más las cosas. De hecho, uno podría incluso abstracta a la basura, una vez por todas, en un BlockingCollection BlockingCollectionSlim ....

internamente se basa en una ConcurrentQueue también, pero does malabarismos adicional semáforos delgados y fichas cancelación , que produce características adicionales, pero a un costo, incluso cuando no se utiliza. También se debe tener en cuenta que BlockingCollection no está casado con ConcurrentQueue, pero también se puede usar con otros implementadores de IProducerConsumerCollection.


A (probado, pero aún no aguerrido) no acotados esqueleto aplicación BlockingCollectionSlim:

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); 
Cuestiones relacionadas