CodeGym /Cursos /C# SELF /Trabajo asíncrono con archivos de texto

Trabajo asíncrono con archivos de texto

C# SELF
Nivel 42 , Lección 2
Disponible

1. Introducción

Recuerda nuestro ejemplo con la carga de una imagen grande. Mientras se carga, la aplicación "se queda congelada". Lo mismo pasa con archivos de texto, especialmente si son grandes (logs de decenas de gigabytes, CSV enormes, backups de bases de datos en formato de texto).

Imagina que estás escribiendo una aplicación que:

Parsea un log enorme para encontrar errores. Si lo lees de forma síncrona, tu interfaz de usuario simplemente se "congelará" por varios segundos o incluso minutos hasta que la operación termine. El usuario pensará que el programa se ha roto.

Escribe datos en un archivo de reporte conforme se generan. Si la escritura bloquea el hilo principal, tanto la generación de datos como la interacción con la UI sufrirán.

Un servidor web que debe atender miles de solicitudes. Cada petición puede requerir lectura o escritura de archivos. Si cada operación I/O es síncrona, los hilos del servidor esperarán al disco y el servidor pronto se "ahogará" por la avalancha de peticiones.

En esos escenarios, el I/O asíncrono deja de ser una "feature agradable" y se vuelve una necesidad vital. Permite que tu aplicación no esté inactiva mientras el disco "piensa", y haga algo útil (por ejemplo, actualizar la UI, procesar otras solicitudes o realizar cálculos).

Conceptos básicos: async/await y tareas

  • La palabra clave async indica que el método puede contener "puntos de espera" (await).
  • El operador await cede temporalmente el control hasta que termine la tarea asíncrona (por ejemplo, la lectura de un archivo).
  • Un método asíncrono realiza I/O sin bloquear el hilo actual: mientras no hay datos — el hilo queda libre.

Todo esto es la base de la "magia" asíncrona al trabajar con archivos.

2. Métodos asíncronos para archivos

En las versiones modernas de .NET casi todas las clases principales para trabajar con archivos tienen equivalentes asíncronos. Para archivos de texto se usan con más frecuencia:

  • StreamReader.ReadLineAsync()
  • StreamReader.ReadToEndAsync()
  • StreamWriter.WriteLineAsync()
  • StreamWriter.WriteAsync()
  • También métodos estáticos: File.ReadAllTextAsync(), File.WriteAllTextAsync() y otros.
Lectura Escritura
ReadLineAsync()
WriteLineAsync()
ReadToEndAsync()
WriteAsync()
File.ReadAllXAsync()
File.WriteAllXAsync()

3. Lectura asíncrona de todo el archivo de texto

Vamos a leer todo el archivo en una sola cadena. Así se hace con archivos pequeños: configs, logs pequeños.

using System;
using System.IO;
using System.Threading.Tasks;

class Program
{
    static async Task Main()
    {
        string path = "input.txt";
        
        // Leemos todo el archivo de forma asíncrona
        string fileContents = await File.ReadAllTextAsync(path);
        
        Console.WriteLine("Contenido del archivo:");
        Console.WriteLine(fileContents);
    }
}

Nota: el método Main ahora está marcado como async Task Main(). Esto es posible desde C# 7.1. ¡Un await y todo funciona de forma asíncrona!

4. Lectura asíncrona línea por línea de un archivo grande

Cuando el archivo es realmente grande, cargarlo entero en memoria no es buena idea. Mejor leerlo línea a línea:

using System;
using System.IO;
using System.Threading.Tasks;

class Program
{
    static async Task Main()
    {
        string path = "biglog.txt";

        // Abrimos StreamReader para lectura asíncrona
        using StreamReader reader = new StreamReader(path);

        string? line;
        while ((line = await reader.ReadLineAsync()) != null)
        {
            // Aquí puedes procesar la línea (por ejemplo, buscar errores)
            Console.WriteLine(line);
        }
    }
}

¿Cómo funciona esto?

Cada llamada a await reader.ReadLineAsync() libera el hilo — especialmente útil si el archivo está en un disco de red o en la nube. El procesamiento asíncrono es crítico con decenas de miles de líneas y trabajo paralelo con usuarios (por ejemplo, en la API de un servidor).

5. Escritura asíncrona de líneas en un archivo

De forma análoga puedes escribir datos en un archivo de forma asíncrona (por ejemplo, al generar reportes):

using System;
using System.IO;
using System.Threading.Tasks;

class Program
{
    static async Task Main()
    {
        string path = "output.txt";

        using StreamWriter writer = new StreamWriter(path);

        for (int i = 0; i < 5; i++)
        {
            await writer.WriteLineAsync($"Línea número {i + 1}");
        }
        
        // Puedes llamar explícitamente FlushAsync para garantizar la escritura
        await writer.FlushAsync();

        Console.WriteLine("¡Datos escritos de forma asíncrona!");
    }
}

La llamada a FlushAsync() no siempre es obligatoria — al cerrar el StreamWriter el buffer se vacía. Pero si necesitas la garantía de "ahora mismo", úsala.

6. Interacción de varias operaciones de archivo asíncronas

Imagina que necesitas leer un archivo de texto y al mismo tiempo escribir una versión transformada en otro:

using System;
using System.IO;
using System.Threading.Tasks;

class Program
{
    static async Task Main()
    {
        string sourcePath = "even_biggerlog.txt";
        string destinationPath = "copy_biggerlog.txt";

        using StreamReader reader = new StreamReader(sourcePath);
        using StreamWriter writer = new StreamWriter(destinationPath);

        string? line;
        int linesProcessed = 0;

        while ((line = await reader.ReadLineAsync()) != null)
        {
            // Un poco de magia: convertimos todas las letras a mayúsculas
            string processed = line.ToUpperInvariant();

            await writer.WriteLineAsync(processed);
            linesProcessed++;
        }

        Console.WriteLine($"Líneas procesadas: {linesProcessed}");
    }
}

Aquí tanto la lectura como la escritura se hacen de forma asíncrona. Cada await cede el control, permitiendo que la aplicación haga otras cosas.

7. Aplicación práctica: ¿dónde se usa esto?

  • Desarrollo web (ASP.NET Core): subir/descargar archivos no bloquea el procesamiento de otras peticiones; el servidor se mantiene responsivo.
  • Aplicaciones de escritorio (WPF, WinForms): al abrir un log o guardar un reporte la UI no "se cuelga".
  • carga asíncrona de recursos (texturas, modelos) permite no interrumpir animaciones y gameplay.
  • Procesamiento de grandes datos: parseo de CSV/JSON/XML enormes línea por línea, procesamiento "on the fly" sin consumo de memoria extra.
  • Servicios en segundo plano y daemons: logging, caching, procesamiento de colas con uso eficiente de hilos y disco.

Conclusión: la asincronía ayuda a crear aplicaciones modernas, responsivas y escalables. "Bloquear" es malo, asincronía es buena idea.

8. Matices y buenas prácticas

¡No te olvides de await! Si llamas a un método con el sufijo Async sin esperarlo, obtendrás un Task, pero el código continuará, lo que causará errores de orden de ejecución.

// MAL: olvidamos await
FileManager.ReadTextFileAsync("nonexistent.txt"); // se lanzará, pero Main seguirá adelante
Console.WriteLine("Me ejecuté inmediatamente, aunque el archivo aún se está leyendo (o ya lanzó una excepción)! ¡Eso es malo!");

El compilador suele advertir sobre un await olvidado, pero no impedirá la compilación.

using para todo lo que implemente IDisposable: todos los streams (FileStream, StreamReader, StreamWriter) deben liberarse correctamente. Usa bloques using o declaraciones using (C# 8+) para garantizar cierre y vaciado de buffers.

Tamaño del buffer (bufferSize): StreamReader/StreamWriter ya están optimizados, pero si tienes requisitos especiales puedes experimentar. Por defecto son valores razonables (en FileStream suele usarse 4096 bytes).

Manejo de errores: los métodos asíncronos también lanzan excepciones. Envuelve las operaciones en try-catch. La excepción "subirá" cuando awaits sobre el Task correspondientemente.

ConfigureAwait: en librerías y escenarios web donde no necesitas el contexto de sincronización (GUI), usa await SomeAsync().ConfigureAwait(false). Esto reduce la sobrecarga por cambio de contexto. En aplicaciones de consola y UI normalmente puedes omitirlo.

Practica — y pronto async Task será tan natural como Console.WriteLine.

9. Errores típicos y matices importantes de operaciones de archivos asíncronas

Si no usas await (y solo llamas al método con sufijo Async), obtendrás un objeto Task, pero el resultado no se esperará automáticamente. Debes esperarlo con await o esperarlo explícitamente (lo que normalmente no es deseable).

No puedes esperar métodos asíncronos desde síncronos sin "subir" el async por la pila de llamadas. Usar .Result o .GetAwaiter().GetResult() puede causar deadlocks — es mejor convertir los métodos llamantes a async.

No leas ni escribas el mismo archivo al mismo tiempo (incluso asíncronamente). Esto puede causar race conditions y corrupción de datos.

La asincronía libera el hilo llamante, pero no hace las operaciones más rápidas: si el disco o la red son lentos, de forma asíncrona seguirá siendo igual de lento — solo que sin bloquear la UI o hilos de trabajo.

Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION