Acceso asincrónico a archivos (C#)

Mediante el uso de la funcionalidad asincrónica para acceder a los archivos, puede llamar a métodos asincrónicos sin usar callbacks ni dividir el código entre varios métodos o expresiones lambda. Para que el código sincrónico sea asincrónico, llame a un método asincrónico en lugar de un método sincrónico y agregue algunas palabras clave al código.

Considere la posibilidad de agregar asincronía a las llamadas de acceso a archivos por estos motivos:

  • 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), la interfaz de usuario podría inmovilizarse hasta que se complete la E/S y el subproceso de la interfaz de usuario puede volver a procesar la entrada del teclado y el mouse y otros eventos.
  • La asincronía mejora la escalabilidad de ASP.NET y otras aplicaciones basadas en servidor al reducir la necesidad de subprocesos. Si la aplicación usa un subproceso dedicado por respuesta y se administran mil solicitudes simultáneamente, se necesitan mil subprocesos. Las operaciones asincrónicas a menudo no necesitan usar un hilo durante el periodo de espera. Usan el subproceso de finalización de E/S existente brevemente al final.
  • La latencia de una operación de acceso a archivos podría ser muy baja en condiciones actuales, pero la latencia podría aumentar considerablemente en el futuro. Por ejemplo, un archivo se puede mover a un servidor que se encuentra en todo el mundo.
  • La sobrecarga agregada del uso de la característica asincrónica es pequeña.
  • Se pueden ejecutar varias operaciones de E/S asincrónicas sin bloquear el subproceso que realiza la llamada.

Usar las clases adecuadas

En los ejemplos sencillos de este tema se muestra File.WriteAllTextAsync y File.ReadAllTextAsync. Para controlar correctamente las operaciones de E/S de archivos, use la FileStream clase , que tiene una opción que hace que la E/S asincrónica se produzca en el nivel de sistema operativo. Con esta opción, puede evitar el bloqueo de un hilo del grupo de hilos en muchos casos. Para habilitar esta opción, especifique el useAsync=true argumento o options=FileOptions.Asynchronous en la llamada al constructor.

No puede usar esta opción con StreamReader y StreamWriter si las abre directamente especificando una ruta de acceso de archivo. Sin embargo, puede usar esta opción si les proporciona un Stream que la clase FileStream abrió. Las llamadas asincrónicas son más rápidas en las aplicaciones de UI incluso si se bloquea un subproceso de grupo, ya que el subproceso de UI no se bloquea durante la espera.

Escribir texto

En los ejemplos siguientes se escribe texto en un archivo. En cada instrucción await, el método termina inmediatamente. Una vez completada la entrada/salida de archivos, el método se reanuda en la instrucción que sigue al await. El modificador asincrónico está 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 tiene 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 genera una tarea y provoca que se inicie el procesamiento de archivos. La segunda instrucción con await hace que el método salga inmediatamente y devuelva una tarea diferente. Cuando el procesamiento de archivos se complete más adelante, la ejecución vuelve a la instrucción que sigue al método 'await'.

Leer 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 el búfer y, en este caso, se coloca en un StringBuilder. A diferencia de en el ejemplo anterior, la evaluación de await genera un valor. El ReadAsync método devuelve un Task<Int32>, por lo que la evaluación de await genera un Int32 valor numRead una vez completada la operación. Para obtener más información, vea Tipos de valor devuelto 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();
}

Varias operaciones de E/S asincrónicas

En los ejemplos siguientes se inician varias operaciones de escritura asincrónicas. El tiempo de ejecución pone en cola estas operaciones y la implementación subyacente podría usar subprocesos de E/S asincrónicos del sistema operativo (SO) o de grupo de subprocesos en función de la plataforma y la configuración, por lo que la simultaneidad real depende del sistema operativo y del hardware.

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 WriteAsync método devuelve una tarea que se agrega a una lista de tareas. La await Task.WhenAll(tasks); instrucción sale del método y se reanuda dentro del método cuando se completa el procesamiento de archivos para todas las tareas.

En el ejemplo se cierran todas las FileStream instancias de un finally bloque una vez completadas las tareas. Si cada FileStream se creara en una instrucción using, FileStream podría ser eliminado antes de que se completara la tarea.

El enfoque asincrónico evita bloquear el subproceso de llamada mientras la E/S está pendiente. En muchos casos, las mejoras de rendimiento dependen del sistema operativo, el hardware y, en algunas plataformas, el comportamiento del entorno de ejecución de .NET, como los límites y la programación del grupo de subprocesos.

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 WriteAsync métodos y ReadAsync , puede especificar un CancellationToken para cancelar la operación a mitad del flujo. Para más información, consulte Cancelación de subprocesos administrados.

Consulte también