ブログ記事非同期版読み取り用メソッドを定義する に対して頂いたご指摘に対するメモ。
- awaitせずにTaskをじかに返せる場合はasyncにせずにそのまま返した方が良い
- メソッドを asyncにするだけでコストがかかる?
- Task.Runでラップしただけのような偽Asyncメソッドは基本避ける
- 非同期処理メソッドを実装する際は、I/O依存の処理と、CPU依存の処理は分けて考えなければならない
- ライブラリはI/O依存の処理のみを非同期メソッドとして提供すべきであり、CPU依存処理の非同期メソッドは提供すべきでない。
- CPU依存の処理を非同期で行うか否かを決めるのはライブラリを使う側の権限
- スレッド、スレッドプールはアプリケーション開発者側が管理すべきリソースなので、ライブラリ内部でTask.Run()等を使って(勝手に)消費したりしてはいけない。
上記を踏まえてコードを修正してみる
-
ReadLineAsync() は async/await を付けず、内部リーダのReadLineAsync()の結果をそのまま返すだけに修正
-
ReadXsvLineAsync() では、Task.Run()で同期メソッドをラップするのをやめ、内部リーダから1行取得する箇所でのみ非同期版メソッド(ReadLineAsync())を使うように修正
- 読み取った文字列をパースする部分はCPUバウンドだが、分離できていない。これではあまり意味が無い??
- だけど、1行分のテキストを読み取ってパースし、パース後のデータを返すまでの一連の処理は切り離したくない。
- パース処理を含めて、I/Oバウンドとみなしても良いものか?
public Task<string> ReadLineAsync()
{
return BaseReader.ReadLineAsync();
//return await BaseReader.ReadLineAsync(); とするのは無駄
}
public async Task<IList<string>> ReadXsvLineAsync(ICollection<string> delimiters)
{
return await Parse(await ReadLineAsync().ConfigureAwait(false), delimiters, () => ReadLineAsync());
}
public async Task<IList<IList<string>>> ReadXsvToEndAsync(ICollection<string> delimiters)
{
var result = new List<IList<string>>();
while (!EndOfData)
{
var line = await ReadXsvLineAsync(delimiters).ConfigureAwait(false);
result.Add(line);
}
return result;
}
public async static Task<IList<string>> Parse(string line, ICollection<string> delimiters,
Func<Task<string>> followingLineSelector)
{
//引数 line に渡された1行分のテキストをパースする
//...
// 次の行を続けて処理したい場合、もう1行読み込んでParse()を再帰
var followingLine = await followingLineSelector().ConfigureAwait(false);
var next = await Parse(token + Environment.NewLine + followingLine,
delimiters, followingLineSelector);
//...
}