1. Introduction
Rappelez-vous notre exemple avec le téléchargement d'une grande image. Tant qu'elle se charge, l'application "se fige". La même chose arrive avec les fichiers texte, surtout s'ils sont volumineux (logs de dizaines de gigaoctets, énormes rapports CSV, sauvegardes de bases de données en texte).
Imaginez que vous développez une application qui :
Parse un énorme fichier de log pour trouver des erreurs. Si vous le lisez de façon synchrone, votre interface utilisateur va simplement "geler" pendant quelques secondes ou même minutes, jusqu'à la fin de l'opération. L'utilisateur pensera que le programme est planté.
Écrit des données dans un fichier de rapport au fur et à mesure qu'elles sont générées. Si l'écriture bloque le thread principal, la génération des données et l'UI en pâtiront.
Un serveur web qui doit servir des milliers de requêtes. Chaque requête peut nécessiter la lecture ou l'écriture d'un fichier. Si chaque I/O fichier est synchrone, les threads du serveur attendront le disque et le serveur va rapidement "se noyer" sous la charge.
Dans ces scénarios, l'I/O asynchrone devient non seulement une "bonne feature", mais une nécessité vitale. Il permet à votre application de ne pas rester inactive pendant que le disque "réfléchit", et de faire quelque chose d'utile (par ex. mettre à jour l'interface, traiter d'autres requêtes ou effectuer des calculs).
Concepts de base : async/await et tâches
- Le mot-clé async indique que la méthode peut contenir des "points d'attente" (await).
- L'opérateur await rend temporairement le contrôle tant que la tâche asynchrone n'est pas terminée (par ex. la lecture d'un fichier).
- Une méthode asynchrone réalise l'I/O sans bloquer le thread courant : tant que les données ne sont pas prêtes — le thread est libre.
Tout cela — la base de la magie asynchrone pour travailler avec les fichiers.
2. Méthodes asynchrones pour les fichiers
Dans les versions modernes de .NET, presque toutes les classes principales pour travailler avec les fichiers ont des équivalents asynchrones. Pour les fichiers texte on utilise souvent :
- StreamReader.ReadLineAsync()
- StreamReader.ReadToEndAsync()
- StreamWriter.WriteLineAsync()
- StreamWriter.WriteAsync()
- Également des méthodes statiques : File.ReadAllTextAsync(), File.WriteAllTextAsync() etc.
| Lecture | Écriture |
|---|---|
|
|
|
|
|
|
3. Lecture asynchrone de tout un fichier texte
Lisont tout le fichier en une seule chaîne. C'est ce qu'on fait pour des fichiers petits : configs, petits logs.
using System;
using System.IO;
using System.Threading.Tasks;
class Program
{
static async Task Main()
{
string path = "input.txt";
// On lit tout le fichier de façon asynchrone
string fileContents = await File.ReadAllTextAsync(path);
Console.WriteLine("Contenu du fichier:");
Console.WriteLine(fileContents);
}
}
Remarquez : la méthode Main est maintenant marquée async Task Main(). C'est possible depuis C# 7.1. Un seul await — et tout fonctionne de façon asynchrone !
4. Lecture asynchrone ligne par ligne d'un gros fichier
Quand le fichier est vraiment volumineux, tout charger en mémoire n'est pas une bonne idée. Mieux vaut lire ligne par ligne :
using System;
using System.IO;
using System.Threading.Tasks;
class Program
{
static async Task Main()
{
string path = "biglog.txt";
// On ouvre un StreamReader pour la lecture asynchrone
using StreamReader reader = new StreamReader(path);
string? line;
while ((line = await reader.ReadLineAsync()) != null)
{
// Ici on peut traiter la ligne (par ex. chercher des erreurs)
Console.WriteLine(line);
}
}
}
Comment ça marche ?
Chaque appel à await reader.ReadLineAsync() libère le thread — particulièrement utile si le fichier est sur un disque réseau ou dans le cloud. Le traitement asynchrone est critique quand il y a des dizaines de milliers de lignes et un travail parallèle avec les utilisateurs (par ex. dans une API serveur).
5. Écriture asynchrone de lignes dans un fichier
De même on peut écrire des données dans un fichier de façon asynchrone (par ex. lors de la génération de rapports) :
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($"Ligne numéro {i + 1}");
}
// On peut appeler explicitement FlushAsync pour garantir l'écriture
await writer.FlushAsync();
Console.WriteLine("Données écrites de façon asynchrone !");
}
}
L'appel à FlushAsync() n'est pas toujours obligatoire — à la fermeture le StreamWriter videra le buffer. Mais si vous voulez la garantie "tout de suite", utilisez-le.
6. Interaction de plusieurs opérations fichier asynchrones
Supposons qu'il faille lire un fichier texte et simultanément écrire une version transformée dans un autre :
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 peu de magie : on met toutes les lettres en majuscules
string processed = line.ToUpperInvariant();
await writer.WriteLineAsync(processed);
linesProcessed++;
}
Console.WriteLine($"Lignes traitées : {linesProcessed}");
}
}
Ici la lecture et l'écriture s'exécutent de façon asynchrone. Chaque await libère le contrôle, permettant à l'application de faire autre chose.
7. Application pratique : où c'est utilisé ?
- Développement web (ASP.NET Core) : upload/download de fichiers ne bloque pas le traitement des autres requêtes ; le serveur reste réactif.
- Applications desktop (WPF, WinForms) : lors de l'ouverture d'un log ou de la sauvegarde d'un rapport l'UI ne "freeze" pas.
- Moteurs de jeu : chargement asynchrone des ressources (textures, modèles) permet de ne pas interrompre animations et gameplay.
- Traitement de gros volumes : parsing d'énormes CSV/JSON/XML ligne par ligne, traitement "à la volée" sans consommation mémoire excessive.
- Services en arrière-plan et daemons : logging, caching, traitement de queues avec utilisation efficace des threads et du disque.
Conclusion : l'asynchronisme aide à créer des applications modernes, réactives et scalables. Le "blocage" c'est mal, l'asynchronisme c'est bien !
8. Nuances et bonnes pratiques
N'oubliez pas le await ! Si vous appelez une méthode avec le suffixe Async sans l'attendre, vous obtiendrez un Task, mais le code continuera, ce qui provoquera des erreurs d'ordre d'exécution.
// MAL : on a oublié await
FileManager.ReadTextFileAsync("nonexistent.txt"); // ça démarrera, mais Main continuera
Console.WriteLine("Je me suis exécuté tout de suite, alors que le fichier est encore en train d'être lu (ou a déjà jeté une erreur) ! C'est mauvais !");
Le compilateur avertit en général d'un await oublié, mais il ne bloque pas la compilation.
using pour tout ce qui est IDisposable : tous les streams (FileStream, StreamReader, StreamWriter) doivent être correctement libérés. Utilisez des blocs using ou les déclarations using (C# 8+) pour garantir la fermeture et le vidage des buffers.
Taille du buffer (bufferSize) : StreamReader/StreamWriter sont déjà optimisés, mais pour des exigences particulières on peut expérimenter. Par défaut ce sont des valeurs confortables (dans FileStream on voit souvent 4096 octets).
Gestion des erreurs : les méthodes asynchrones lancent aussi des exceptions. Entourez les opérations d'un try-catch. L'exception "remontera" lors de l'exécution du await sur le Task correspondant.
ConfigureAwait : dans les bibliothèques et scénarios web où le contexte de synchronisation (GUI) n'est pas nécessaire, utilisez await SomeAsync().ConfigureAwait(false). Cela réduit le coût du switch de contexte. Dans les applications console et UI on peut généralement l'omettre.
Pratiquez — et bientôt async Task deviendra aussi naturel que Console.WriteLine.
9. Erreurs typiques et nuances importantes des opérations fichier asynchrones
Si vous n'utilisez pas await (et appelez juste la méthode avec le suffixe Async), vous obtiendrez un objet Task, mais le résultat ne sera pas attendu automatiquement. Il faut l'attendre via await ou l'attendre explicitement (ce qui est généralement indésirable).
On ne peut pas attendre des méthodes asynchrones depuis du code synchrone sans "remonter" le async dans la pile. Utiliser .Result ou .GetAwaiter().GetResult() peut provoquer des deadlocks — mieux vaut transformer les méthodes appelantes en async.
Ne lisez pas et n'écrivez pas le même fichier en même temps (même de façon asynchrone). Cela peut causer des races et la corruption des données.
L'asynchronisme libère le thread appelant, mais ne rend pas les opérations plus rapides : si le disque ou le réseau est lent, asynchrone restera lent — simplement sans bloquer l'UI ou les threads de travail.
GO TO FULL VERSION