Accès asynchrone aux fichiers (C#)

En utilisant la fonctionnalité asynchrone pour accéder aux fichiers, vous pouvez appeler des méthodes asynchrones sans utiliser de rappels ou fractionner votre code entre plusieurs méthodes ou expressions lambda. Pour rendre le code synchrone asynchrone, appelez une méthode asynchrone au lieu d’une méthode synchrone et ajoutez quelques mots clés au code.

Envisagez d’ajouter une synchronisation aux appels d’accès aux fichiers pour les raisons suivantes :

  • Asynchrony rend les applications d’interface utilisateur plus réactives, car le thread d’interface utilisateur qui lance l’opération peut effectuer d’autres tâches. Si le thread d’interface utilisateur doit exécuter du code qui prend beaucoup de temps (par exemple, plus de 50 millisecondes), l’interface utilisateur peut se figer jusqu’à ce que l’E/S soit terminée et que le thread d’interface utilisateur puisse à nouveau traiter l’entrée clavier et la souris et d’autres événements.
  • Asynchrony améliore la scalabilité des ASP.NET et d’autres applications basées sur le serveur en réduisant le besoin de threads. Si l’application utilise un thread dédié par réponse et qu’un millier de requêtes sont traitées simultanément, un millier de threads sont nécessaires. Les opérations asynchrones n’ont souvent pas besoin d’utiliser un thread pendant l’attente. À la fin, ils utilisent brièvement le thread d'achèvement d'E/S existant.
  • La latence d’une opération d’accès aux fichiers peut être très faible dans les conditions actuelles, mais la latence peut augmenter considérablement à l’avenir. Par exemple, un fichier peut être déplacé vers un serveur qui se trouve dans le monde entier.
  • La surcharge ajoutée de l’utilisation de la fonctionnalité asynchrone est petite.
  • Plusieurs opérations d’E/S asynchrones peuvent s’exécuter sans bloquer le thread appelant.

Utiliser les classes appropriées

Les exemples simples de cette rubrique illustrent File.WriteAllTextAsync et File.ReadAllTextAsync. Pour un contrôle précis sur les opérations d’E/S de fichier, utilisez la FileStream classe, qui a une option qui entraîne l’exécution d’E/S asynchrones au niveau du système d’exploitation. En activant cette option, vous pouvez éviter de bloquer un fil d'un pool de threads dans de nombreux cas. Pour activer cette option, spécifiez l’argument useAsync=true ou l’argument options=FileOptions.Asynchronous dans l’appel du 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 une Stream que la classe FileStream a ouverte. Les appels asynchrones sont plus rapides dans les applications d'interface utilisateur, même si un fil d'exécution du pool est bloqué, car le fil d'exécution de l'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 se ferme immédiatement. Une fois l’E/S de fichier 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 a 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émarrage du traitement des fichiers. La deuxième instruction avec await entraîne la sortie immédiate de la méthode et le retour d’une autre tâche. Une fois le traitement du fichier terminé, l'exécution retourne à l'instruction qui suit l'await.

Lire du texte

Les exemples suivants lisent du texte à partir d’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 ce cas, placé dans un StringBuilder. Contrairement à l’exemple précédent, l’évaluation de la fonction 'await' produit une valeur. La ReadAsync méthode retourne un Task<Int32>, de sorte que l’évaluation de l’await produit une Int32 valeur numRead une fois l’opération terminée. Pour plus d’informations, consultez Types de retour asynchrones (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();
}

Opérations d’E/S asynchrones multiples

Les exemples suivants démarrent plusieurs opérations d’écriture asynchrones. Le runtime met en file d’attente ces opérations et l’implémentation sous-jacente peut utiliser des threads d’E/S asynchrones ou de pool de threads en fonction de la plateforme et de la configuration, de sorte que la concurrence réelle dépend du système d’exploitation et du matériel.

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 WriteAsync méthode retourne une tâche ajoutée à une liste de tâches. L’instruction await Task.WhenAll(tasks); quitte la méthode et reprend dans la méthode lorsque le traitement du fichier est terminé pour toutes les tâches.

L’exemple ferme toutes les FileStream instances d’un finally bloc une fois les tâches terminées. Si chaque FileStream était plutôt créé dans une instruction using, le FileStream pourrait être supprimé avant la fin de la tâche.

L’approche asynchrone évite de bloquer le thread appelant pendant que les E/S sont en attente. Dans de nombreux cas, les améliorations du débit dépendent du système d’exploitation, du matériel et, sur certaines plateformes, du comportement du runtime .NET, comme les limites du pool de threads et la planification.

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

Lorsque vous utilisez les méthodes WriteAsync et ReadAsync, vous pouvez spécifier une option CancellationToken pour annuler l’opération en cours. Pour plus d’informations, consultez Annulation dans les threads managés.

Voir également