¿Qué tiene de malo Lazy I / O?

89

En general, he escuchado que el código de producción debería evitar el uso de E / S diferidas. Mi pregunta es, ¿por qué? ¿Alguna vez está bien usar Lazy I / O además de jugar? ¿Y qué hace que las alternativas (por ejemplo, los enumeradores) sean mejores?

Dan Burton
fuente

Respuestas:

81

Lazy IO tiene el problema de que liberar cualquier recurso que haya adquirido es algo impredecible, ya que depende de cómo su programa consume los datos: su "patrón de demanda". Una vez que su programa descarta la última referencia al recurso, el GC eventualmente se ejecutará y liberará ese recurso.

Los flujos perezosos son un estilo muy conveniente para programar. Es por eso que las tuberías de shell son tan divertidas y populares.

Sin embargo, si los recursos están limitados (como en escenarios de alto rendimiento o entornos de producción que esperan escalar hasta los límites de la máquina), confiar en el GC para limpiar puede ser una garantía insuficiente.

A veces es necesario liberar recursos con entusiasmo para mejorar la escalabilidad.

Entonces, ¿cuáles son las alternativas a la IO perezosa que no significan renunciar al procesamiento incremental (que a su vez consumiría demasiados recursos)? Bueno, hemos foldlbasado el procesamiento, también conocido como iterados o enumeradores, introducido por Oleg Kiselyov a finales de la década de 2000 y, desde entonces, popularizado por una serie de proyectos basados ​​en redes.

En lugar de procesar los datos como flujos diferidos, o en un lote enorme, abstraemos el procesamiento estricto basado en fragmentos, con la finalización garantizada del recurso una vez que se lee el último fragmento. Esa es la esencia de la programación basada en iteratee y ofrece limitaciones de recursos muy agradables.

La desventaja de la E / S basada en iteratee es que tiene un modelo de programación algo incómodo (más o menos análogo a la programación basada en eventos, frente al buen control basado en subprocesos). Definitivamente es una técnica avanzada, en cualquier lenguaje de programación. Y para la gran mayoría de los problemas de programación, la IO perezosa es completamente satisfactoria. Sin embargo, si va a abrir muchos archivos, o hablará en muchos sockets, o si va a utilizar muchos recursos simultáneos, un enfoque de iteración (o enumerador) podría tener sentido.

Don Stewart
fuente
22
Como acabo de seguir un enlace a esta vieja pregunta de una discusión sobre E / S perezosa, pensé en agregar una nota que, desde entonces, gran parte de la incomodidad de los iterados ha sido reemplazada por nuevas bibliotecas de transmisión como tuberías y conductos .
Ørjan Johansen
40

Dons ha proporcionado una muy buena respuesta, pero ha dejado fuera lo que es (para mí) una de las características más atractivas de los iterados: hacen que sea más fácil razonar sobre la gestión del espacio porque los datos antiguos deben retenerse explícitamente. Considerar:

average :: [Float] -> Float
average xs = sum xs / length xs

Esta es una pérdida de espacio bien conocida, porque toda la lista xsdebe conservarse en la memoria para calcular tanto sumy length. Es posible hacer un consumidor eficiente creando un pliegue:

average2 :: [Float] -> Float
average2 xs = uncurry (/) <$> foldl (\(sumT, n) x -> (sumT+x, n+1)) (0,0) xs
-- N.B. this will build up thunks as written, use a strict pair and foldl'

Pero es algo inconveniente tener que hacer esto para cada procesador de flujo. Hay algunas generalizaciones ( Conal Elliott - Beautiful Fold Zipping ), pero no parece que hayan tenido éxito . Sin embargo, los iterados pueden brindarle un nivel de expresión similar.

aveIter = uncurry (/) <$> I.zip I.sum I.length

Esto no es tan eficiente como un pliegue porque la lista todavía se repite varias veces; sin embargo, se recopila en fragmentos para que los datos antiguos se puedan recolectar de manera eficiente. Para romper esa propiedad, es necesario retener explícitamente toda la entrada, como con stream2list:

badAveIter = (\xs -> sum xs / length xs) <$> I.stream2list

El estado de los iterados como modelo de programación es un trabajo en progreso, sin embargo, es mucho mejor que hace un año. Estamos aprendiendo lo combinadores son útiles (por ejemplo zip, breakE, enumWith) y que son por lo menos, con el resultado de que incorporados iteratees y combinadores proporcionan continuamente más expresividad.

Dicho esto, Dons tiene razón en que son una técnica avanzada; Ciertamente no los usaría para cada problema de E / S.

Juan L
fuente
25

Utilizo E / S diferidas en el código de producción todo el tiempo. Es solo un problema en ciertas circunstancias, como mencionó Don. Pero con solo leer algunos archivos, funciona bien.

augusts
fuente
Yo también uso E / S perezoso. Recurro a los iterados cuando quiero más control sobre la gestión de recursos.
John L
20

Actualización: Recientemente en haskell-cafe, Oleg Kiseljov demostró que unsafeInterleaveST(que se usa para implementar IO perezoso dentro de la mónada ST) es muy inseguro: rompe el razonamiento ecuacional. Demuestra que permite construir de bad_ctx :: ((Bool,Bool) -> Bool) -> Bool tal manera que

> bad_ctx (\(x,y) -> x == y)
True
> bad_ctx (\(x,y) -> y == x)
False

aunque ==es conmutativo.


Otro problema con la E / S diferida: la operación de E / S real puede aplazarse hasta que sea demasiado tarde, por ejemplo, después de cerrar el archivo. Citando de Haskell Wiki - Problemas con IO perezoso :

Por ejemplo, un error común para principiantes es cerrar un archivo antes de que uno haya terminado de leerlo:

wrong = do
    fileData <- withFile "test.txt" ReadMode hGetContents
    putStr fileData

El problema es que withFile cierra el identificador antes de que se fuerce fileData. La forma correcta es pasar todo el código a withFile:

right = withFile "test.txt" ReadMode $ \handle -> do
    fileData <- hGetContents handle
    putStr fileData

Aquí, los datos se consumen antes de que finalice withFile.

A menudo, esto es inesperado y un error fácil de cometer.


Consulte también: Tres ejemplos de problemas con E / S diferida .

Petr
fuente
En realidad, combinar hGetContentsy no withFiletiene sentido porque el primero pone el identificador en un estado "pseudo-cerrado" y manejará el cierre por usted (perezosamente) por lo que el código es exactamente equivalente a readFile, o incluso openFilesin hClose. Eso es básicamente lo que es la E / S perezosa . Si no lo usa readFile, getContentso hGetContentsno usa E / S diferida. Por ejemplo, line <- withFile "test.txt" ReadMode hGetLinefunciona bien.
Dag
1
@Dag: aunque hGetContentsse encargará de cerrar el archivo por usted, también está permitido cerrarlo usted mismo "antes" y ayuda a garantizar que los recursos se liberen de manera predecible.
Ben Millwood
17

Otro problema con IO perezoso que no se ha mencionado hasta ahora es que tiene un comportamiento sorprendente. En un programa Haskell normal, a veces puede ser difícil predecir cuándo se evalúa cada parte de su programa, pero afortunadamente, debido a la pureza, realmente no importa a menos que tenga problemas de rendimiento. Cuando se introduce la IO perezosa, el orden de evaluación de su código en realidad tiene un efecto en su significado, por lo que los cambios que está acostumbrado a considerar como inofensivos pueden causarle problemas genuinos.

Como ejemplo, aquí hay una pregunta sobre el código que parece razonable pero se vuelve más confuso por IO diferido: withFile vs. openFile

Estos problemas no son invariablemente fatales, pero es otra cosa en la que pensar, y un dolor de cabeza lo suficientemente severo que personalmente evito IO perezoso a menos que haya un problema real con hacer todo el trabajo por adelantado.

Ben Millwood
fuente