[WF 4 (9)] アクティビティの非同期実行 – “真に Parallel” な実行

環境 : Visual Studio 2010 Beta 2 (.NET Framework 4)

WF 4

  1. そのアイデアとベースクラスの変更
  2. コードいらずのワークフロー (入門)
  3. ワークフローのコードと XAML の内部
  4. フローチャート (FlowCharts)
  5. アクティビティ (基礎)
  6. アクティビティコンテキスト (context) と変数 (Variable) の意義
  7. アクティビティにおけるさまざまな実行管理
  8. アクティビティのデリゲート
  9. アクティビティの非同期実行
  10. アクティビティデザイナー
  11. ブックマーク
  12. Workflow Extensions と 永続化、トラッキング
  13. デザイナーリホスティングとカスタムアプリケーション

こんにちは。

WF 4 には、非同期実行を支援するための仕組みが提供されています。今日は、この内容について記載します。

ご存じの通り、WF には Parallel アクティビティというものがあり、これを使用すると子アクティビティを「並行に」処理できます。しかし、この「並行」の意味を正しく理解しておいてください。WF のスケジューラの振る舞いを正しく理解していただくため、この Parallel アクティビティを例に、陥りやすい「並列実行」の「誤解」を説明してみましょう。

Windows Workflow Foundation (WF) では、内部で、各アクティビティの処理を実行するための「キュー」が使用されています。このキューを使った動きを理解するため、例えば、下図のようにデザインしたワークフローを例にその動きを確認してみます。

上図のワークフローの場合、Sequence1 と Sequence2 は厳密な意味で並行に実行されるのではなく、内部では、つぎのように動作することになります。

  1. まず、トップの Parallel アクティビティがスケジューラで実行され、実行可能な Sequence1 が実行され、Sequence2 がキューに登録されます。
  2. Sequence1 アクティビティが実行された結果、実行可能な Sequence1 の中の WriteLine1_1 アクティビティが実行されます。
  3. WriteLine1_1 アクティビティの実行が完了 (Completed) すると Sequence1 アクティビティに制御が戻り、引き続き実行可能な Delay アクティビティが実行されます。
  4. Delay アクティビティではタイマーをセットしてアクティビティの状態をいったんアイドル状態にします。このアイドルによって、スケジューラは、つぎにキューの先頭に含まれる Sequence2 アクティビティを取り出して実行します。
  5. Sequence2 アクティビティが実行された結果、継続可能な WriteLine2_1 アクティビティが実行されます。

    . . . (以降は、省略します)

注意 : なお、従来の WF (WF 3.5) では、実行する子アクティビティなど1つ1つのアクティビティがいったんすべてキューに登録され、実行を順番に待機していました。例えば、上記の例では、WF 3.5 の場合、左の Delay アクティビティはいったんキューに登録され、この Delay が評価・実行される前に、WriteLine2_1 が実行されることになります。しかし、WF 4 では、上記の通り、継続可能 (Continue) な状況では、引き続き実行可能なアクティビティが先に評価されて実行されます。

このように、一見「並行」に処理されているように見えるアクティビティですが、内部では、キューやイベントに基づく “逐次的な” 処理がおこなわれています。

この動きをもっとわかりやすく示すため、例えば、つぎのように実行に時間がかかる WriteToFile アクティビティ (カスタムに作成したアクティビティです) を Parallel で実行してみてください。このワークフローの実行をおこなうと、実際には「並列」には実行されず、各処理の順番を待ってアクティビティが実行されるのがわかります。

class Program
{
    static void Main(string[] args)
    {
        Activity workflow1 = new Sequence
        {
            Activities =
            {
                new Parallel
                {
                    Branches =
                    {
                        new WriteToFileActivity
                        {
                            FilePath = @"c:tmptest1.txt"
                        },
                        new WriteToFileActivity
                        {
                            FilePath = @"c:tmptest2.txt"
                        }
                    }
                }
            }
        };

        WorkflowInvoker.Invoke(workflow1);
        Console.ReadLine();
    }
}

public class WriteToFileActivity : CodeActivity
{
    public InArgument<string> FilePath { get; set; }

    protected override void Execute(CodeActivityContext context)
    {
        string filepath = FilePath.Get(context);

        Console.WriteLine("{0} : 書き込み開始", filepath);

        // ファイルのオープン
        TextWriter writer = new StreamWriter(filepath, false);
        // 1 秒に 1 文字ずつ書き込む (わざと、ゆっくりと処理 !!)
        int i;
        for (i = 1; i <= 10; i++)
        {
            System.Threading.Thread.Sleep(1000);
            writer.Write("a");
            Console.WriteLine("{0} : {1} 文字書き込み", filepath, i);
        }
        // ファイルのクローズ
        writer.Close();

        Console.WriteLine("{0} : 書き込み終了", filepath);
    }
}

実行結果 :

c:tmptest1.txt : 書き込み開始
c:tmptest1.txt : 1 文字書き込み
c:tmptest1.txt : 2 文字書き込み
c:tmptest1.txt : 3 文字書き込み
c:tmptest1.txt : 4 文字書き込み
c:tmptest1.txt : 5 文字書き込み
c:tmptest1.txt : 6 文字書き込み
c:tmptest1.txt : 7 文字書き込み
c:tmptest1.txt : 8 文字書き込み
c:tmptest1.txt : 9 文字書き込み
c:tmptest1.txt : 10 文字書き込み
c:tmptest1.txt : 書き込み終了
c:tmptest2.txt : 書き込み開始
c:tmptest2.txt : 1 文字書き込み
c:tmptest2.txt : 2 文字書き込み
c:tmptest2.txt : 3 文字書き込み
c:tmptest2.txt : 4 文字書き込み
c:tmptest2.txt : 5 文字書き込み
c:tmptest2.txt : 6 文字書き込み
c:tmptest2.txt : 7 文字書き込み
c:tmptest2.txt : 8 文字書き込み
c:tmptest2.txt : 9 文字書き込み
c:tmptest2.txt : 10 文字書き込み
c:tmptest2.txt : 書き込み終了

多くの場合、こうした内部を動きを意識せず、見た目は「並行」(Parallel) に動作しますが、この例のようなケースでは、本来の意味で「非同期」にはなっていないことが実感して頂けるでしょう。こうした課題に対処できるのが、WF 4 から用意されている AsyncCodeActivity です。

上記の WriteToFile アクティビティで、CodeActivity ではなく AsyncCodeActivity としてアクティビティを作成すると、実行時に Execute メソッドではなく BeginExecute メソッドが実行され、その中で呼び出される delegate 関数が非同期に処理されることになります。処理終了後のメソッドも、BeginExecute の際に引数で渡されるコールバックをそのまま使用することで、 オーバーライドメソッドとしてあらかじめ提供されている EndExecute メソッドが呼び出されます。

以下は、上記の WriteToFile アクティビティを AsyncCodeActivity として実装した例です

public class WriteToFileActivity : AsyncCodeActivity
{
    public InArgument<string> FilePath { get; set; }

    protected override IAsyncResult BeginExecute(AsyncCodeActivityContext context,
        AsyncCallback callback, object state)
    {
        // delegate の作成
        AsyncWriteToFile d = new AsyncWriteToFile(this.WriteToFile);
        // 複数のコンテキストに対応するため、コンテキストに入れておくこと !
        context.UserState = d;
        // 実行開始 (コールバックとして、引数をそのまま渡す !)
        return d.BeginInvoke(FilePath.Get(context), callback, state);
    }

    protected override void EndExecute(AsyncCodeActivityContext context,
        IAsyncResult result)
    {
        // コンテキストから delegate を取得
        AsyncWriteToFile d = (AsyncWriteToFile)context.UserState;
        // 結果を取得
        int writeByte = d.EndInvoke(result);
    }

    private delegate int AsyncWriteToFile(string filepath);
    private int WriteToFile(string filepath)
    {
        Console.WriteLine("{0} : 書き込み開始", filepath);

        // ファイルのオープン
        TextWriter writer = new StreamWriter(filepath, false);
        // 1 秒に 1 文字ずつ書き込む
        int i;
        for (i = 1; i <= 10; i++)
        {
            System.Threading.Thread.Sleep(1000);
            writer.Write("a");
            Console.WriteLine("{0} : {1} 文字書き込み", filepath, i);
        }
        // ファイルのクローズ
        writer.Close();

        Console.WriteLine("{0} : 書き込み終了", filepath);

        return i;
    }
}

この実行結果は、ご想像の通り、以下の通りになります。

c:tmptest1.txt : 書き込み開始
c:tmptest2.txt : 書き込み開始
c:tmptest2.txt : 1 文字書き込み
c:tmptest1.txt : 1 文字書き込み
c:tmptest2.txt : 2 文字書き込み
c:tmptest1.txt : 2 文字書き込み
c:tmptest2.txt : 3 文字書き込み
c:tmptest1.txt : 3 文字書き込み
c:tmptest2.txt : 4 文字書き込み
c:tmptest1.txt : 4 文字書き込み
c:tmptest2.txt : 5 文字書き込み
c:tmptest1.txt : 5 文字書き込み
c:tmptest2.txt : 6 文字書き込み
c:tmptest1.txt : 6 文字書き込み
c:tmptest2.txt : 7 文字書き込み
c:tmptest1.txt : 7 文字書き込み
c:tmptest2.txt : 8 文字書き込み
c:tmptest1.txt : 8 文字書き込み
c:tmptest2.txt : 9 文字書き込み
c:tmptest1.txt : 9 文字書き込み
c:tmptest2.txt : 10 文字書き込み
c:tmptest1.txt : 10 文字書き込み
c:tmptest2.txt : 書き込み終了
c:tmptest1.txt : 書き込み終了

このように、従来では制御がむずかしかった非同期実行の処理が、WF4 では大変作りやすくなっています。

ここ数回、少しアクティビティ内部に入り込みすぎましたので、次回は、アクティビティ作成の総まとめとして、これらの実用的なアクティビティにデザイナーを適用していきたいと思います。

 

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s