Compartir vía


Acceso asincrónico a archivos (C#)

Puede usar la característica async para acceder a archivos. Con la característica async, se puede llamar a métodos asincrónicos sin usar devoluciones de llamada ni dividir el código en varios métodos o expresiones lambda. Para convertir código sincrónico en asincrónico, basta con llamar a un método asincrónico y no a un método sincrónico y agregar algunas palabras clave al código.

Podrían considerarse los siguientes motivos para agregar asincronía a las llamadas de acceso a archivos:

  • La asincronía hace que las aplicaciones de interfaz de usuario tengan mayor capacidad de respuesta porque el subproceso de interfaz de usuario que inicia la operación puede realizar otro trabajo. Si el subproceso de interfaz de usuario debe ejecutar código que tarda mucho tiempo (por ejemplo, más de 50 milisegundos), puede inmovilizar la interfaz de usuario hasta que la E/S se complete y el subproceso de interfaz de usuario pueda volver a procesar la entrada de teclado y de mouse y otros eventos.
  • La asincronía mejora la escalabilidad de ASP.NET y otras aplicaciones basadas en servidor reduciendo la necesidad de subproceso. Si la aplicación usa un subproceso dedicado por respuesta y se procesa un millar de solicitudes simultáneamente, se necesitan mil subprocesos. Las operaciones asincrónicas no suelen necesitar un subproceso durante la espera. Usan el subproceso existente de finalización de E/S brevemente al final.
  • Puede que la latencia de una operación de acceso a archivos sea muy baja en las condiciones actuales, pero puede aumentar mucho en el futuro. Por ejemplo, se puede mover un archivo a un servidor que está a escala mundial.
  • La sobrecarga resultante de usar la característica Async es pequeña.
  • Las tareas asincrónicas se pueden ejecutar fácilmente en paralelo.

Uso de las clases adecuadas

Los ejemplos sencillos de este tema muestran cómo usar los métodos File.WriteAllTextAsync y File.ReadAllTextAsync. Para tener control finito sobre las operaciones de E/S de archivos, use la clase FileStream, que tiene una opción que hace que la E/S asincrónica se produzca en el nivel del sistema operativo. Si usa esta opción, puede evitar bloquear un subproceso del grupo de subprocesos en muchos casos. Para habilitar esta opción, especifique el argumento useAsync=true o options=FileOptions.Asynchronous en la llamada al constructor.

No puede usar esta opción con StreamReader y StreamWriter si los abre directamente al especificar una ruta de acceso de archivo. En cambio, puede usar esta opción si les proporciona un Stream que ha abierto la clase FileStream. Las llamadas asincrónicas son más rápidas en aplicaciones de interfaz de usuario aunque un subproceso del grupo de subprocesos se bloquee, porque el subproceso de interfaz de usuario no se bloquea durante la espera.

Escritura de texto

En los siguientes ejemplos se escribe texto en un archivo. En cada instrucción await, el método finaliza inmediatamente. Cuando se complete la E/S de archivo, el método se reanuda en la instrucción que sigue a la instrucción await. El modificador async se encuentra en la definición de métodos que usan la instrucción await.

Ejemplo sencillo

public async Task SimpleWriteAsync()
{
    string filePath = "simple.txt";
    string text = $"Hello World";

    await File.WriteAllTextAsync(filePath, text);
}

Ejemplo de control finito

public async Task ProcessWriteAsync()
{
    string filePath = "temp.txt";
    string text = $"Hello World{Environment.NewLine}";

    await WriteTextAsync(filePath, text);
}

async Task WriteTextAsync(string filePath, string text)
{
    byte[] encodedText = Encoding.Unicode.GetBytes(text);

    using var sourceStream =
        new FileStream(
            filePath,
            FileMode.Create, FileAccess.Write, FileShare.None,
            bufferSize: 4096, useAsync: true);

    await sourceStream.WriteAsync(encodedText, 0, encodedText.Length);
}

El ejemplo original incluye la instrucción await sourceStream.WriteAsync(encodedText, 0, encodedText.Length);, que es una contracción de las dos instrucciones siguientes:

Task theTask = sourceStream.WriteAsync(encodedText, 0, encodedText.Length);
await theTask;

La primera instrucción devuelve una tarea e inicia el procesamiento de archivos. La segunda instrucción con await finaliza el método inmediatamente y devuelve otra tarea. Después, cuando se complete el procesamiento de archivos, la ejecución vuelve a la instrucción que sigue a la instrucción await.

Lectura de texto

En los ejemplos siguientes se lee texto de un archivo.

Ejemplo sencillo

public async Task SimpleReadAsync()
{
    string filePath = "simple.txt";
    string text = await File.ReadAllTextAsync(filePath);

    Console.WriteLine(text);
}

Ejemplo de control finito

El texto se almacena en búfer y, en este caso, se coloca en un StringBuilder. A diferencia del ejemplo anterior, la evaluación de la instrucción await genera un valor. El método ReadAsync devuelve Task<Int32>, por lo que la evaluación de await genera un valor Int32numRead una vez completada la operación. Para obtener más información, consulte Tipos de valor devueltos asincrónicos (C#).

public async Task ProcessReadAsync()
{
    try
    {
        string filePath = "temp.txt";
        if (File.Exists(filePath) != false)
        {
            string text = await ReadTextAsync(filePath);
            Console.WriteLine(text);
        }
        else
        {
            Console.WriteLine($"file not found: {filePath}");
        }
    }
    catch (Exception ex)
    {
        Console.WriteLine(ex.Message);
    }
}

async Task<string> ReadTextAsync(string filePath)
{
    using var sourceStream =
        new FileStream(
            filePath,
            FileMode.Open, FileAccess.Read, FileShare.Read,
            bufferSize: 4096, useAsync: true);

    var sb = new StringBuilder();

    byte[] buffer = new byte[0x1000];
    int numRead;
    while ((numRead = await sourceStream.ReadAsync(buffer, 0, buffer.Length)) != 0)
    {
        string text = Encoding.Unicode.GetString(buffer, 0, numRead);
        sb.Append(text);
    }

    return sb.ToString();
}

E/S asincrónica en paralelo

En los siguientes ejemplos se muestra el procesamiento paralelo escribiendo 10 archivos de texto.

Ejemplo sencillo

public async Task SimpleParallelWriteAsync()
{
    string folder = Directory.CreateDirectory("tempfolder").Name;
    IList<Task> writeTaskList = new List<Task>();

    for (int index = 11; index <= 20; ++ index)
    {
        string fileName = $"file-{index:00}.txt";
        string filePath = $"{folder}/{fileName}";
        string text = $"In file {index}{Environment.NewLine}";

        writeTaskList.Add(File.WriteAllTextAsync(filePath, text));
    }

    await Task.WhenAll(writeTaskList);
}

Ejemplo de control finito

Para cada archivo, el método WriteAsync devuelve una tarea que luego se agrega a una lista de tareas. La instrucción await Task.WhenAll(tasks); finaliza el método y se reanuda en el método cuando el procesamiento de archivos se completa para todas las tareas.

Tras completar las tareas, el ejemplo cierra todas las instancias de FileStream de un bloque finally. Si en lugar de ello, cada FileStream se ha creado en una instrucción using, la FileStream se podría desechar antes de completarse la tarea.

Cualquier aumento del rendimiento se debe casi por completo al procesamiento en paralelo y no al procesamiento asincrónico. Las ventajas de la asincronía radican en que no inmoviliza varios subprocesos ni el subproceso de interfaz de usuario.

public async Task ProcessMultipleWritesAsync()
{
    IList<FileStream> sourceStreams = new List<FileStream>();

    try
    {
        string folder = Directory.CreateDirectory("tempfolder").Name;
        IList<Task> writeTaskList = new List<Task>();

        for (int index = 1; index <= 10; ++ index)
        {
            string fileName = $"file-{index:00}.txt";
            string filePath = $"{folder}/{fileName}";

            string text = $"In file {index}{Environment.NewLine}";
            byte[] encodedText = Encoding.Unicode.GetBytes(text);

            var sourceStream =
                new FileStream(
                    filePath,
                    FileMode.Create, FileAccess.Write, FileShare.None,
                    bufferSize: 4096, useAsync: true);

            Task writeTask = sourceStream.WriteAsync(encodedText, 0, encodedText.Length);
            sourceStreams.Add(sourceStream);

            writeTaskList.Add(writeTask);
        }

        await Task.WhenAll(writeTaskList);
    }
    finally
    {
        foreach (FileStream sourceStream in sourceStreams)
        {
            sourceStream.Close();
        }
    }
}

Al usar los métodos WriteAsync y ReadAsync, puede especificar un CancellationToken, que puede usar para cancelar la operación en mitad de la secuencia. Para más información, consulte Cancelación de subprocesos administrados.

Vea también