Skip to content

Instantly share code, notes, and snippets.

@jmhdez
Last active December 23, 2015 17:29
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jmhdez/6669478 to your computer and use it in GitHub Desktop.
Save jmhdez/6669478 to your computer and use it in GitHub Desktop.
Enumerators y Dispose
Una consulta básica usando Linq-to-NHibernate (con EF supongo que sería parecido)
var dbQuery = session.Query<Product>();
El IEnumerable que devuelve, internamente tiene un Enumerator parecido a esto:
IEnumerator<Product> GetEnumerator() {
using (var reader = command.ExecuteReader())
{
while (reader.Read())
yield return new Product(reader["name"], reader["price"]);
}
}
El IDataReader se cierra cuando (a) se invoca el dispose del IEnumerator o (b)
se termina de recorrer el enumerator.
En esta implementación (blog.koalite.com/2013/09/mejorando-el-rendimiento-de-un-ienumerable/)
el enumerator que se decora no se puede disposear hasta que se acaba de recorrer, por lo
que el IDataReader quedaría abierto hasta ese momento.
En ésta (gist.github.com/josejuan/6668983), aunque hubiera un Dispose al final del while,
pasaría lo mismo porque hace falta garantizar que el enumerator sigue vivo y el punto en que se
quedó anteriormente para evitar recalcular los valores anteriores.
@josejuan
Copy link

El problema es diferente según que casos se consideren, pero la mejor solución NO DEPENDE (intrínsecamente) del tipo de enumerador utilizado, sino del contexto.

Por ejemplo, es obvio que si tendremos que recorrer completamente y una única vez la enumeración, la versión memoizada no es eficiente, igualmente con otro montón de situaciones, otra de ellas, los casos en los que podemos tener recursos (ej. conexiones) ocupadas ¡pero que merezca la pena tenerlas abiertas no depende (per se) de la enumeración, sino de quien la usa!.

Si pudieramos tener una enumeración que se comportara bien (sea lo que sea eso) en todos los contextos estaría genial, pero podemos ver fácilmente que la elección de cerrar o no las conexiones (en otros casos ya se vería) no está en la mano del tipo de enumeración que hemos construido, sino del gestor de los recursos (o en su defecto, del que hace uso de nuestra enumeración conociendo los recursos consumidos).

Así, es imposible crear una estructura lazy que no deje colgados los recursos subyacentes ¡sin conocer de alguna forma esos mismos recursos subyacentes! (que es lo que quieres hacer tú con "dispose").

¿Y porqué no se puede hacer?, pues porque IEnumerable, que es la única información de que disponemos, no permite controlar la forma en la que se gestionan los recursos subyacentes.

Por ejemplo, si IEnumerable tuviera algo como "LiberarTemporalmenteRecursos" y "VolverATomarLosRecursos", entonces, podrías hacer que su tu versión memoizada si pasa mucho tiempo, liberara los recursos, si después hace falta seguir llenando los lazy, entonces se vuelven a tomar los recursos donde se dejaron.

Algo similar a las típicas paginaciones, tomamos una página y vamos procesando, si la consumimos totalmente, pedimos otra página, etc... es decir, otra estrategia diferente a Liberar/Tomar podría ser que IEnumerable permitiera recuperar "por páginas" ¡sin bloquear los recursos entre página y página!.

Así, quien no quiera bloquear los recursos, NO deberá usar la versión memoizada, sino usar directamente "enum.ToList()", si ésto no es posible porque son muchos datos en memoria o en tiempo (ej. ancho de banda limitado), pues sencillamente debe usar otra cosa que no sea IEnumerable, por ejemplo ¡paginación!.

Algo similar pasa si yo me quejo de que nuestro memoizador no libera los recursos cacheados que ya no voy a leer más ¡como los va a liberar si no tiene información de si los voy a volver a leer o no!. Como antes, una solución que potencialmente soluciona eso es indicar un nº máximo de lecturas (tras las cuales puede borrar), tiempo de caducidad y otras tantas que, en otros contextos, son válidas, pero que IEnumerable no nos permite propagar.

Por supuesto puedes añadir todas estas estrategias en la clase de memoización o como parámetros en la extensión, pero no parecía esa la idea inicial ni parece que tenga sentido meter esas funcionalidades en un único sitio (si necesitas un pool, usa un pool, si necesitas una caché temporal, usa una caché temporal, si necesitas memoizar, asume coste RAM, etc...).

Como sabes, no me gustan los ORM y éste tema tiene mucho que ver de porqué no me gustan.

Si yo defino procedimientos almacenados específicos para cubrir soluciones concretas, me aseguro que la forma en que se procesan los datos es óptima y lo mejor es que puedo hacerlo adhoc y a posteriori (mal vas a cambiar tu código ORM porque cambies de servidor, pero yo si puedo cambiar fácilmente en el servidor la forma en que se generan los datos). Digamos que prefiero que no haya ningún IEnumerable entre mis datos y la forma en que yo las quiero consumir.

Por último, tras el while, puedes tranquilamente liberar los recursos, porque ahí SI TENEMOS la información, por un lado, sabemos que hemos leído toda la secuencia, porque la enum subyacente nos acaba de decir que no hay más datos y por otro, podemos lanzar un dispose para informarle que puede liberar si hay algo.

(puedes poner un if antes del while al estilo de "if no hemos indicado ya un dispose seguir con el while").

@jmhdez
Copy link
Author

jmhdez commented Sep 23, 2013

En realidad cuando estuve dándole vueltas a esto no era tanto por los ORMs sino por otro tipo de expresiones linq en las que acabas haciendo muchas transformaciones (where/select/order-by).

Normalmente cuando accedo a una base de datos intento que ella me devuelva ya las cosas como las necesito (aunque por supuesto no suelo rebajarme a escribir procedimientos almacenados :-P).

Pero vamos, al final llegué a la misma conclusión que tú, no es posible tener una solución genérica sin conocer la naturaleza del IEnumerable que hay por debajo.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment