2012年10月15日月曜日

[C#] 非同期処理

[共通] 当 Blog の投稿に関する基本事項について
→ http://fuunyan.blogspot.jp/2012/09/blog.html


ファイルの比較処理をテストで作ったのだが、処理が長くなると UI が固まるため、対策として非同期処理を導入することにしました。
今回は web 上で公開されているサンプルなども充実していたので苦労しませんでした。
っと言いたいのですが VisualStudio (っと言うか .NET のバージョンですかね)の違いなどにより使える機能が限定されているため最新の解説ページの情報が役に立たず。
何だかんだと調べてみた結果 VS2008 + C# の組み合わせでは最適と思われる方法で作ってみました。
環境制限などで必要に迫られているようでしたら参考にどうぞ。

今回のテストフォーム上は少しコントロール多めです。
TextBox x 2
Label x 2
Button x 1
ListBox x 1
TextBox に比較対象となるファイルのフルパスを入力しておきます(実行時に入力してもいいですがめんどくさい)
そしてボタンを押すと処理開始となります。
処理結果は ListBox に出力、途中経過は Label に出力します。

とりあえず using 宣言。
using System.IO;            // ファイル操作で使用
using System.Threading;     // スレッド系で使用

次にデリゲートの宣言。
スレッド対象のメソッド(って言う言い方が正しいのか?)を宣言します。
ちなみに宣言位置はクラス内です。
// 非同期処理に利用するデリゲートの定義宣言
private delegate int checkDlgt(string file1, string file2);

// 非同期側スレッド内からフォーム上のコントロールを操作するために利用するデリゲートの定義宣言
private delegate void addStrLog3dlgt(int tgt, string str);

そして非同期側からフォームコントロールを操作するためのメソッド
フォームコントロールは別スレッドからは操作できないため、デリゲート経由で操作することになります。
今回は途中経過を表示したいので、作成しました。
// 非同期側(子プロセス)から listBox3 に追記するためのメソッド
private void addStrLog3(int tgt, string str)
{
    switch (tgt)
    {
        case 1:
            label1.Text = str;      // 処理位置
            break;
        case 2:
            label2.Text = str;      // ファイルサイズ
            break;
        case 3:
        default:
            listBox3.Items.Add(str);
            break;
    }
}

やっとサブスレッドの本体です。
この中でファイルの比較を行います。
// 非同期処理の本体
private int binaryChecking(string file1, string file2)
{
    // もろもろメッセージ出力のための処理
    if (!this.InvokeRequired)
        return 0;

    addStrLog3dlgt asl3 = new addStrLog3dlgt(addStrLog3);

    if (File.Exists(file1) && File.Exists(file2))
    {
        // 指定された2ファイルをバイナリ形式で開く
        byte[] f1 = File.ReadAllBytes(file1);
        byte[] f2 = File.ReadAllBytes(file2);
        // 進捗率表示用
        int iProgress = f1.Count() / 10;

        // 比較元のファイルサイズを表示
        this.Invoke(asl3, 2, f1.Count().ToString());
        this.Invoke(asl3, 3, "start   :" + DateTime.Now.ToLongDateString() + " " + DateTime.Now.ToLongTimeString());
        for (int i = 0; f1.Count() > i; i++)
        {
            if (!(f1[i] == f2[i]))
                break;

            // 進捗状況表示
            if (i % iProgress == 0)
                this.Invoke(asl3, 3, i.ToString("D8") + ":" + DateTime.Now.ToLongDateString() + " " + DateTime.Now.ToLongTimeString());

            // 処理状況表示(非常に処理速度に影響を与えるので下記値を参考に変更)
            //   1=トイレ休憩可能、10=遅い、1000=まぁ待てる、10000=実用範囲
            if( i % 1000 == 0)
                this.Invoke(asl3, 1, i.ToString());
        }
    }
    else
    {
        return 0;
    }

    return 1;
}

そしてコールバックメソッドです。
呼び出したのはいいですが、非同期なのでスレッドいつ終わるかわかりません。
メインスレッドでサブスレッドの終了監視を行うことも可能ですが、それでは結局 UI が固まり本末転倒。
そんなわけで、終わったら自分から申告してもらう必要があります。
そのために利用します。
// コールバックメソッド
private void CallBackMethod(IAsyncResult iAR)
{
    addStrLog3dlgt asl3 = new addStrLog3dlgt(addStrLog3);
    checkDlgt cDlgt = (checkDlgt)iAR.AsyncState;

    string strWork;
    int ret = cDlgt.EndInvoke(iAR);

    if (ret == 1)
        strWork = "finish  :";
    else
        strWork = "abort   :";
    
    this.Invoke(asl3, 3, strWork + DateTime.Now.ToLongDateString() + " " + DateTime.Now.ToLongTimeString());
}

最後にイベント起動部分。
ボタン押下イベントです。
// バイナリファイルの比較
private void button4_Click(object sender, EventArgs e)
{
    // 非同期で動作させるメソッドを指定
    checkDlgt cDlgt = new checkDlgt(binaryChecking);

    // 非同期処理の起動(先方で利用する引数値もここで指定)
    IAsyncResult iAR = cDlgt.BeginInvoke(textBox1.Text, textBox2.Text, new AsyncCallback(CallBackMethod), cDlgt);
}

本当は別途テストプログラムを作成するつもりだったのですが、なんとなく勢いで考えていた処理をそのまま作ってしまいました。
サブスレッドの処理状況表示 if 文の数値を小さくすると格段に処理が遅くなるため、メインスレッド(画面側)の UI が死んでいる様、もしくは死なない様の比較がしやすいかと思います。

余談ですが VS2012 など投稿時点での最新バージョンでは atwait などの処理を利用することで、もっと簡単に高機能な処理を実現できるようです。
バージョン制約が無い方は、その辺も調べられてみてはどうでしょうか。

今回はこれまで。

0 件のコメント:

コメントを投稿