1. はじめに
大きな画像を読み込む例を思い出してみて。読み込み中にアプリが「フリーズ」するよね。同じことがテキストファイルでも起きる。特にサイズがでかい場合(数十GBのログ、巨大なCSVレポート、テキスト形式のDBバックアップなど)。
想像してみて、あなたが次のようなアプリを書いているとする:
巨大なログファイルをパースしてエラーを見つける。同期的に読んでいるとUIが数秒〜数分止まって、ユーザーはプログラムが壊れたと思うかも。
データを生成しながらレポートファイルに書き込む。書き込みがメインスレッドをブロックすると、データ生成もUIもひどい目にあう。
何千ものリクエストをさばくWebサーバは、各リクエストでファイルの読み書きが必要になることがある。もしそれが全部同期的なら、サーバのスレッドはディスク待ちで潰れてしまい、すぐにリクエストで溢れる。
こういうケースでは非同期IOは「便利な機能」じゃなくて必須になってくる。ディスクが「考えている」間にアプリが無駄に待たずに済んで、別のこと(UI更新、他のリクエスト処理、計算など)をできるようになるんだ。
基本概念: async/await とタスク
- キーワード async は、そのメソッドが「待ちポイント」(await) を含む可能性があることを示すよ。
- 演算子 await は、非同期タスク(例: ファイル読み取り)が終わるまで一時的に制御を手放す。
- 非同期メソッドは現在のスレッドをブロックせずにIOを行う: データが来るまでスレッドは解放される。
これがファイル操作の非同期マジックの基本だよ。
2. ファイル用の非同期メソッド
最新の.NETでは、ファイル操作の主要クラスにほとんど非同期の対応がある。テキストファイルでは主に次を使うことが多い:
- StreamReader.ReadLineAsync()
- StreamReader.ReadToEndAsync()
- StreamWriter.WriteLineAsync()
- StreamWriter.WriteAsync()
- あとは静的メソッド: File.ReadAllTextAsync(), File.WriteAllTextAsync() など。
| 読み取り | 書き込み |
|---|---|
|
|
|
|
|
|
3. テキストファイルを非同期で全部読む
小さなファイル(設定ファイル、小さめのログなど)は一度に全部読むことが多い。例:
using System;
using System.IO;
using System.Threading.Tasks;
class Program
{
static async Task Main()
{
string path = "input.txt";
// ファイル全体を非同期に読み取る
string fileContents = await File.ReadAllTextAsync(path);
Console.WriteLine("ファイルの内容:");
Console.WriteLine(fileContents);
}
}
注意: メソッド Main は今や async Task Main() としてマークされている。C# 7.1 からこれが可能だ。1つの await で非同期に動くよ!
4. 大きなファイルを行単位で非同期に読む
ファイルが本当に大きいときは、全部をメモリに載せるのは良くない。行単位で読む方がいい:
using System;
using System.IO;
using System.Threading.Tasks;
class Program
{
static async Task Main()
{
string path = "biglog.txt";
// 非同期読み取り用の StreamReader を開く
using StreamReader reader = new StreamReader(path);
string? line;
while ((line = await reader.ReadLineAsync()) != null)
{
// ここで行を処理できる(例: エラーを探す)
Console.WriteLine(line);
}
}
}
どう動くの?
各呼び出し await reader.ReadLineAsync() によりスレッドが解放される—特にファイルがネットワークドライブやクラウド上にある場合に有益だ。何万行もの処理や、ユーザーが並列で操作しているAPIサーバでは非同期処理が重要になる。
5. 非同期でファイルに行を書き込む
同様にデータを非同期でファイルに書き込むこともできる(レポート生成などで):
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($"行番号 {i + 1}");
}
// 明示的に FlushAsync を呼ぶと書き込みを保証できる
await writer.FlushAsync();
Console.WriteLine("データが非同期で書き込まれました!");
}
}
FlushAsync() の呼び出しは必須ではない — StreamWriter を閉じるとバッファはフラッシュされる。ただし「今すぐ確実に」という保証が欲しいなら使おう。
6. 複数の非同期ファイル操作のやり取り
例えば、あるテキストファイルを読みつつ並行して変換したものを書き出す必要があるとする:
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)
{
// 少し魔法: すべての文字を大文字にする
string processed = line.ToUpperInvariant();
await writer.WriteLineAsync(processed);
linesProcessed++;
}
Console.WriteLine($"処理した行数: {linesProcessed}");
}
}
ここでは読み取りも書き込みも非同期で行っている。各 await が制御を解放し、アプリが他のことをできるようにしてくれる。
7. 実践的な用途: どこで使う?
- Web開発 (ASP.NET Core): ファイルのアップ/ダウンロードが他のリクエスト処理をブロックしない、サーバが応答性を保てる。
- デスクトップアプリ (WPF, WinForms): ログを開いたりレポートを保存してもUIが「固まらない」。
- ゲームエンジン: リソース(テクスチャ、モデル)の非同期ロードでアニメーションやゲームプレイを止めない。
- 大規模データ処理: 巨大なCSV/JSON/XMLを行単位でパースして、メモリを無駄にせずにストリーム処理する。
- バックグラウンドサービスやデーモン: ロギング、キャッシュ、キュー処理をスレッドとディスクを効率的に使って行う。
まとめ: 非同期はモダンで応答性が高くスケーラブルなアプリを作るのに役立つ。ブロッキングは悪、非同期は善!
8. 細かい点とベストプラクティス
await を忘れないで! サフィックスが Async のメソッドを待たずに呼ぶと Task が返ってくるけど、コードは先に進んでしまい順序のバグを生むよ。
// ダメな例: await を忘れた
FileManager.ReadTextFileAsync("nonexistent.txt"); // 実行は始まるが Main は先に進む
Console.WriteLine("ファイルがまだ読まれている(あるいは既に例外を出している)のにすぐ実行されちゃった!これは良くない!");
コンパイラは通常 await の忘れを警告するが、ビルドを止めたりはしない。
using は IDisposable なものすべてに: すべてのストリーム(FileStream, StreamReader, StreamWriter)は正しく解放すべきだ。using ブロックや C# 8+ の using 宣言を使って確実にクローズしてバッファをフラッシュしよう。
バッファサイズ (bufferSize): StreamReader/StreamWriter は既に最適化されているが、特別な要件があれば試してみる価値はある。デフォルトは一般的に十分(FileStream ではよく 4096 バイトが使われる)。
エラー処理: 非同期メソッドも例外を投げる。操作は try-catch で囲んでね。例外は対応する await の時点で「発生する」よ。
ConfigureAwait: ライブラリやGUIコンテキストが不要なWebシナリオでは、await SomeAsync().ConfigureAwait(false) を使うとコンテキストスイッチのオーバーヘッドを減らせる。コンソールや一部のUIアプリでは省略して問題ないことが多い。
練習すれば、async Task が Console.WriteLine と同じくらい自然になるよ。
9. 非同期ファイル操作の典型的なミスと重要な注意点
await を使わずに Async サフィックスのメソッドを呼ぶと、Task オブジェクトは得られるが結果は自動的に待たれない。必ず await するか、(ただし通常避けたいが)明示的に待つ必要がある。
同期メソッドの中から非同期メソッドを待つことは、スタック上で async を上げない限りできない。.Result や .GetAwaiter().GetResult() の使用はデッドロックを生むことがある — 呼び出し元メソッドを async に変えるのがベターだ。
同じファイルを同時に読み書きしないで(非同期であっても)。競合やデータ破損の原因になる。
非同期は呼び出しスレッドを解放するが、操作自体を速くするわけではない: ディスクやネットワークが遅ければ非同期でも遅い。ただしUIやワーカースレッドをブロックしない点がメリットだ。
GO TO FULL VERSION