Accès asynchrone aux fichiers (C#)

Vous pouvez utiliser la fonctionnalité Async pour accéder à des fichiers. La fonctionnalité async vous permet d’appeler des méthodes asynchrones sans utiliser de rappels ni fractionner votre code entre plusieurs méthodes ou expressions lambda. Pour rendre le code synchrone asynchrone, il vous suffit d’appeler une méthode asynchrone au lieu d’une méthode synchrone, puis d’ajouter quelques mots clés au code.

Vous pouvez considérer les raisons suivantes pour ajouter de l'asynchrone aux appels d'accès aux fichiers :

  • L'asynchronisme rend les applications d'interface utilisateur plus réactives parce que le thread d'interface utilisateur qui lance l'exécution peut effectuer d'autres tâches. Si le thread d’interface utilisateur doit exécuter du code qui prend du temps (par exemple, plus de 50 millisecondes), l’interface utilisateur peut figer jusqu’à ce que l’E/S soit terminée et que le thread d’interface utilisateur puisse traiter à nouveau des entrées au clavier et à la souris et d’autres événements.
  • L'asynchronisme améliore l'extensibilité d'ASP.NET et d'autres applications serveur en réduisant le besoin de threads. Si l'application utilise un thread dédié par réponse et que mille demandes sont traitées simultanément, mille threads sont nécessaires. Les opérations asynchrones n'ont souvent pas besoin d'utiliser un thread pendant l'attente. Elles utilisent le thread de terminaison d’E/S existant brièvement à la fin.
  • La latence d'une opération d'accès à un fichier peut être très basse sous les conditions actuelles, mais la latence peut considérablement augmenter à l'avenir. Par exemple, un fichier peut être déplacé vers un serveur qui se trouve à travers le monde.
  • La charge mémoire supplémentaire pour l'utilisation de la fonctionnalité Async est faible.
  • Les tâches asynchrones peuvent facilement être exécutées en parallèle.

Utiliser les classes appropriées

Les exemples simples de cette rubrique illustrent File.WriteAllTextAsync et File.ReadAllTextAsync. Pour un contrôle précis des opérations d’E/S de fichier, utilisez la classe FileStream, qui a une option qui entraîne l’exécution d’E/S asynchrones au niveau du système d’exploitation. En utilisant cette option, vous pouvez éviter de bloquer un thread de pool de threads dans de nombreux cas. Pour l’activer, spécifiez l’argument useAsync=true ou options=FileOptions.Asynchronous dans l’appel de constructeur.

Vous ne pouvez pas utiliser cette option avec StreamReader et StreamWriter si vous les ouvrez directement en spécifiant un chemin d’accès au fichier. Toutefois, vous pouvez utiliser cette option si vous leur fournissez un Stream ouvert par la classe FileStream. Les appels asynchrones sont plus rapides dans les applications d’interface utilisateur même si un thread de pool de threads est bloqué, car le thread d’interface utilisateur n’est pas bloqué pendant l’attente.

Écrire du texte

Les exemples suivants écrivent du texte dans un fichier. À chaque instruction await, la méthode s'arrête immédiatement. Quand l’E/S de fichier est terminée, la méthode reprend à l’instruction qui suit l’instruction await. Le modificateur asynchrone se trouve dans la définition de méthodes qui utilisent l’instruction await.

Exemple simple

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

    await File.WriteAllTextAsync(filePath, text);
}

Exemple de contrôle fini

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);
}

L’exemple d’origine comprend l’instruction await sourceStream.WriteAsync(encodedText, 0, encodedText.Length);, qui est une contraction des deux instructions suivantes :

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

La première instruction retourne une tâche et provoque le début du traitement du fichier. La deuxième instruction avec await provoque la fin immédiate de la méthode et retourne une tâche différente. Quand le traitement du fichier se termine plus loin, l’exécution retourne à l’instruction qui suit l’attente.

Lire le texte

Les exemples suivants lisent du texte dans un fichier.

Exemple simple

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

    Console.WriteLine(text);
}

Exemple de contrôle fini

Le texte est mis en mémoire tampon et, dans cet exemple, est placé dans un StringBuilder. Contrairement à l’exemple précédent, l’évaluation de l’instruction await génère une valeur. La méthode ReadAsync retourne un Task<Int32>, de sorte que l’évaluation de l’expression await génère une valeur Int32numRead une fois l’opération effectuée. Pour plus d’informations, consultez Types de retour Async (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 asynchrones en parallèle

Les exemples suivants illustrent le traitement parallèle en écrivant 10 fichiers texte.

Exemple simple

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);
}

Exemple de contrôle fini

Pour chaque fichier, la méthode WriteAsync retourne une tâche qui est ensuite ajoutée à une liste des tâches. L’instruction await Task.WhenAll(tasks); quitte la méthode et reprend dans cette dernière quand le traitement du fichier est terminé pour toutes les tâches.

L’exemple ferme toutes les instances de FileStream dans un bloc finally une fois les tâches effectuées. Si chaque FileStream était plutôt créé dans une instruction using, FileStream pourrait être libéré avant que la tâche ne soit terminée.

Toute amélioration des performances provient presque entièrement du traitement parallèle et non du traitement asynchrone. Les avantages du mode asynchrone sont qu'il n'attache pas plusieurs threads et qu'il ne bloque pas le thread d'interface utilisateur.

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();
        }
    }
}

Quand vous utilisez les méthodes WriteAsync et ReadAsync, vous pouvez spécifier un CancellationToken, qui vous permet d’annuler l’opération en cours de route. Pour plus d’informations, consultez Annulation dans les threads managés.

Voir aussi