読者です 読者をやめる 読者になる 読者になる

非同期版読み取り用メソッドを定義する

C# .Net

連載C#でCSVファイル操作ライブラリ の3回目の記事です。

が、以下のご指摘を頂き、追加記事を公開しております。こちらも併せてご参照ください。
非同期版読み取り用メソッドを定義する (改)

おさらい

前回は、XsvReaderにテキストを読み取るためのメソッドReadLine(), ReadXsvLine(), ReadXsvToEnd()を作成しました。
今回は、これらをawait可能にした「非同期版」を作成し、XsvReaderに追加したいと思います。

async/awaitを使用した非同期メソッド

async/awaitキーワードを使用した非同期メソッドを実装します。
基本的に.Net Framework4.5以降が対象となります。

ReadLine()の非同期版ReadLineAsync()メソッド

XsvReaderの内部リーダTextReaderの非同期メソッドReadLineAsync()をそのままラップします。

ポイントは

  • 作成するメソッドにasyncキーワードを付ける
  • 内部リーダのReadLineAsync()メソッドにawaitを付ける
  • 戻り値をTaskにする
public Task<string> ReadLineAsync()
{
    return BaseReader.ReadLineAsync();
}

ReadXsvLine()の非同期版ReadXsvLineAsync()

前回作成したメソッドReadXsvLine()を、Task.Run()メソッドを使用して非同期処理に変換します。

  • Task.Run()Func<TResult>デリゲートを引数に渡すオーバーロードを使用します。このメソッドはTask<TResult>を返します。
  • デリゲート内では同期メソッドReadXsvLine()を実行します。
    ReadXsvLine()IEnumerable<string>を返すのですが、これをToArray()で配列に展開したものを返却しています。 そうしないと、IEnumerable<T>に包み込む部分のみが非同期に実行され、肝心のParse処理自体は、タスク完了後にforeach等でシーケンスを展開した時点で実行されて非同期にした意味が無くなってしまいます。
public async Task<string[]> ReadXsvLineAsync(ICollection<string> delimiters)
{
    return await Task.Run(() => ReadXsvLine(delimiters).ToArray());
}

ReadXsvToEnd()の非同期版ReadXsvToEndAsync()

ReadXsvLineAsync()の場合と同様に、同期版のReadXsvToEnd()Task.Run()で非同期処理に変換します。
ReadXsvToEndメソッドはIEnumerable<string[]> を返却しますが、ここではToList()で一旦Listに展開したものを返却するようにしています。

public async Task<IList<string[]>> ReadXsvToEndAsync(ICollection<string> delimiters)
{
    return await Task.Run(() => ReadXsvToEnd(delimiters).ToList());
}

(メモ) 戻り値でstringの配列となっている部分Task<string[]>Task<IList<string[]>>は、IList<T>インターフェースを使ってTask<IList<string>>Task<IList<IList<string>>>とする方が良かったかも?
この場合に限らず、コレクションを返却したり、プロパティで公開したりする場合のインターフェースに何を選択するかには、結構悩みます...

使用例

上記3つのメソッドを使った、簡単な例を載せておきます。
ReadLineAsync()csvでない先頭行を、ReadXsvLineAsync()csvのヘッダ行を、ReadXsvToEndAcync()でヘッダ以降のすべてのレコードをそれぞれ読み取ります。

using HandyUtil.Text.Xsv;
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;

namespace ConsoleApplication1
{
    class Program
    {
        static void Main(string[] args)
        {
            ReadSample().Wait();
            Console.ReadLine();
        }

        static async Task ReadSample() 
        { 
            var data = @"[sample.csv@20140401]
col1,col2,col3,col4,col5
one,two,three,four,five
aaa,bbb,ccc,ddd,eee
りんご,みかん,バナナ,ぶどう,パイナップル";

            var delimiters = new[]{","};
            
            using(var reader = new XsvReader(new StringReader(data)))
            {
                var comment = await reader.ReadLineAsync();
                Console.WriteLine("comment= " + comment);

                var header = await reader.ReadXsvLineAsync(delimiters);
                Console.WriteLine("header= " + string.Join(",",header));

                var rows = await reader.ReadXsvToEndAsync(delimiters);
                Console.WriteLine("rows= {");
                foreach (var row in rows)
                {
                    Console.WriteLine("  " + string.Join(",", row));
                }
                Console.WriteLine("}");
            }
        } 
    }
}

以下、出力結果です。

> comment= [sample.csv@20140401]
> header= col1,col2,col3,col4,col5
> rows= {
>   one,two,three,four,five
>   aaa,bbb,ccc,ddd,eee
>   りんご,みかん,バナナ,ぶどう,パイナップル
> }

(おまけ).Net4.0の場合は?

async キーワードが無いこと以外、メソッドのシグネチャに変更ありません。

メソッド内では、Task.Run()の代わりにTask.Factory.StartNew()を使用します。

public Task<string> ReadLineAsync()
{
    return Task.Factory.StartNew(() => BaseReader.ReadLine());
}

public Task<string[]> ReadXsvLineAsync(ICollection<string> delimiters)
{
    return Task.Factory.StartNew(() => ReadXsvLine(delimiters).ToArray());
}

public Task<IList<string[]>> ReadXsvToEndAsync(ICollection<string> delimiters)
{
    return Task.Factory.StartNew(() => (IList<string[]>)ReadXsvToEnd(delimiters).ToList());
}

使う側では、これらのメソッドをContinuWith + Unwrap で順に繋げていく感じになりますでしょうか。

using HandyUtil.Text.Xsv;
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;

namespace ConsoleApplication2
{
    class Program
    {
        static void Main(string[] args)
        {
            var data = @"[sample.csv@20140401]
col1,col2,col3,col4,col5
one,two,three,four,five
aaa,bbb,ccc,ddd,eee
りんご,みかん,バナナ,ぶどう,パイナップル";

            using (var reader = new XsvReader(new StringReader(data)))
            {
                ReadSample(reader).Wait();
            }
            Console.ReadLine();
        }

        static Task ReadSample(XsvReader reader)
        {
            var delimiters = new[] { "," };

            return reader.ReadLineAsync().ContinueWith(task =>
            {
                Console.WriteLine("comment= " + task.Result);
                return reader.ReadXsvLineAsync(delimiters);
            }).Unwrap().ContinueWith(task =>
            {
                Console.WriteLine("header= " + string.Join(",", task.Result));
                return reader.ReadXsvToEndAsync(delimiters);
            }).Unwrap().ContinueWith(task =>
            {
                var rows = task.Result;
                Console.WriteLine("rows= {");
                foreach (var row in rows)
                {
                    Console.WriteLine("  " + string.Join(",", row));
                }
                Console.WriteLine("}");
            });

        }
    }
}