none
TPLで全てのタスクの結果を待つには? RRS feed

  • 質問

  • いつもお世話になっております.

    TPLに関する質問です.

     

    あるタスクを並列に実行する場合,あるタスクが終了するたびに,その結果に応じて以下の処理を行うにはどうしたらいいのでしょうか?

    ・処理結果がFalseの場合には,残りのタスクを全てキャンセル

    ・処理結果がTrueの場合には,他のタスクの終了を待つ

     

    Task.WaitAll メソッドを使えば,全てのタスクが終了するのを待つことができ,

    Task.WaitAny メソッドを使えば,いずれか1つのタスクが終了するのを待つことができるのはわかりました.

    ただ,Task.WaitAllだと,全てのタスクが終わるまで,処理結果を検証できず,

    Task.WaitAnyを繰り返し実行するのもコーディング方法がイマイチ分かりません.(ためしに,whileの無限ループで囲んでみたのですが駄目でした.)

    アドバイスよろしくお願いします.

     

     

    2010年11月29日 13:34

回答

  • using System;
    using System.Collections.Generic;
    using System.Threading;
    using System.Threading.Tasks;
    
    public static class Program
    {
      public static void Main(string[] args)
      {
        var R = new Random();
        
        var tasks = new List<Task<bool>>();
        
        for (var i = 0; i < 200; i++)
        {
          tasks.Add(Task<bool>.Factory.StartNew(() =>
          {
            Thread.Sleep(3000 + i);
            return R.Next(1000) > 250;
          }));
        }
        
        while (tasks.Count > 0)
        {
          var index = Task.WaitAny(tasks.ToArray());
          if (index < 0)
          {
            Console.WriteLine("timed out.");
            return;
          }
          
          var task = tasks[index];
          if (!task.Result)
          {
            Console.WriteLine("found failure.");
            return;
          }
          else
          {
            Console.Write("."); // show progress
            
            tasks.RemoveAt(index);
          }
        }
        
        Console.Write("success.");
      }
    }
    
    ざっくり、200のタスクで乱数を生成し、閾値以下の値が出た時点でループを抜けます(キャンセル処理は実装されていません)
    • 回答としてマーク Spring96 2010年12月1日 12:48
    2010年11月30日 4:00

すべての返信

  • while と WaitAny() の組み合わせで問題ないように思いますが、「駄目でした」とは、なにがどうダメだったのでしょうか?

    他の方法として、CancellationTokenSource を生成しておいて、CancellationToken を用いて WaitAll() し、Task 側で Cancel 状態を確認するとともに、処理結果が false になる場合に Cancel() ですべて終了させてしまうという手もあります。

    2010年11月30日 3:46
  • 前回と同様、試行錯誤しながら書きました。
    CancellationToken の使い方等、おかしなところがあるかもしれません。
    よりよい回答が付くまでのつなぎになれば幸いです。(^^;
    変なところはツッコミお願いします。

    // キャンセル用
    var cts = new CancellationTokenSource();
    var ct = cts.Token;

    // 10個のテストタスク
    var tasks = new Task<bool>[10];
    for (var i = 0; i < 10; i++)
    {
        tasks[i] = Task.Factory.StartNew<bool>((state) =>
            {
                // 1つ目のタスクは失敗する
                if ((int)state == 0) return false;

                for (var j = 0; j < 5; j++)
                {
                    System.Threading.Thread.Sleep(1000);
                    // キャンセルが要求されたら終了する
                    if (ct.IsCancellationRequested)
                        return false;
                }
                return true;
            }, i);
    }

    // 1つでもタスクの結果が false の場合は、残りをキャンセルする
    // というタスク
    var taskManager = Task.Factory.StartNew<bool>(() =>
        {
            var taskList = new List<Task<bool>>(tasks);

            // K.Takaoka さんのように taskList.Count で判断した方が
            // だんぜん良いですね。(^^;
            while (!Task.WaitAll(taskList.ToArray(), 10))
            {
                var indexDone = Task.WaitAny(taskList.ToArray());
                if (!taskList[indexDone].Result)
                {
                    // キャンセルの要求
                    cts.Cancel();
                    break;
                }
                taskList.RemoveAt(indexDone);
            }

            // 戻り値は、1つでも失敗があれば false
            // (Resultの参照時にキャンセルの完了が待機される)
            foreach (var task in tasks)
                if (!task.Result) return false;
            return true;
        });

    MessageBox.Show(taskManager.Result ? "完了!" : "失敗!");

    • 編集済み TH01 2010年11月30日 4:14
    2010年11月30日 3:49
  • K.Takaoka さんの返信を読むと、この方法でも大丈夫かなと思いました。(^^;

    K.Takaoka さん
    > 処理結果が false になる場合に Cancel() ですべて終了させてしまうという手もあります。

    私もその方法で最初は書きました。
    シンプルにできたんですが、私の場合、return false する際に Cancel するのを忘れそうなので、別タスクで監視するようにしました。

    追記:
    本当は IsCancellationRequested の監視じゃなくて例外が発生するキャンセルの方法がいいと思ったのですが、まだ扱いが把握できてません。

    • 編集済み TH01 2010年11月30日 4:01 追記
    2010年11月30日 3:56
  • using System;
    using System.Collections.Generic;
    using System.Threading;
    using System.Threading.Tasks;
    
    public static class Program
    {
      public static void Main(string[] args)
      {
        var R = new Random();
        
        var tasks = new List<Task<bool>>();
        
        for (var i = 0; i < 200; i++)
        {
          tasks.Add(Task<bool>.Factory.StartNew(() =>
          {
            Thread.Sleep(3000 + i);
            return R.Next(1000) > 250;
          }));
        }
        
        while (tasks.Count > 0)
        {
          var index = Task.WaitAny(tasks.ToArray());
          if (index < 0)
          {
            Console.WriteLine("timed out.");
            return;
          }
          
          var task = tasks[index];
          if (!task.Result)
          {
            Console.WriteLine("found failure.");
            return;
          }
          else
          {
            Console.Write("."); // show progress
            
            tasks.RemoveAt(index);
          }
        }
        
        Console.Write("success.");
      }
    }
    
    ざっくり、200のタスクで乱数を生成し、閾値以下の値が出た時点でループを抜けます(キャンセル処理は実装されていません)
    • 回答としてマーク Spring96 2010年12月1日 12:48
    2010年11月30日 4:00
  • K. Takaokaさん

    回答ありがとうございます.

     

    配列ではなく,Task<bool>の型のリストにするのが,まず,思いつきませんでした.
    で,処理が終了したタスクをリストから削除していき,リストが空になるまで,Task.WaitAny メソッド・・・・そんなやり方があるのですね.
    ソースコードも大変分かりやすく,勉強になりました.
    ありがとうございました!!

    >while と WaitAny() の組み合わせで問題ないように思いますが、「駄目でした」とは、
    >なにがどうダメだったのでしょうか?
    while(true)
    {
    	Task.WaitAny (tasks);
    }
    と書いていました.

    > 他の方法として、CancellationTokenSource を生成しておいて、
    >CancellationToken を用いて WaitAll() し、Task 側で Cancel 状態を確認するとともに、
    >処理結果が false になる場合に Cancel() ですべて終了させてしまうという手もあります。

     

    Task側で以下のようなコードを書いておき,WaitAll() が書いてあるほうで例外をcatchすればいいですよね?

    if (Task.Factory.CancellationToken.IsCancellationRequested)

          Task.Factory.CancellationToken.ThrowIfCancellationRequested();

     

     

    2010年11月30日 8:18
  • TH01さん

    おひさしぶりです,先日は回答ありがとうございました.

    ソースコードもありがとうございます.

     while (!Task.WaitAll(taskList.ToArray(), 10)){ }

    こういう書き方もあるんですね.

     

     var taskList = new List<Task<bool>>(tasks);と書かれていますが,

    List<Task<bool>> tasks = new List<Task<bool>>();としないのは,C#の流儀だったりするのでしょうか?

    2010年11月30日 8:26
  • > Task側で以下のようなコードを書いておき

    WaitAll() の引数にトークンを与えると、WaitAll() をキャンセルすることもできます。(例外が飛ぶのはいっしょです)

    あるタスクが false を返す場合に、他のタスクをどうしたいかも重要かと思います。例のように、他のタスクを走らせっぱなしでよいのか、すべてのタスクがキャンセルを受けて動作を停止する必要があるのか。

    また、TaskFactory の既定のトークンは、トークンを明示しないすべてのタスクで共有されるので、特定の処理群をキャンセルしたい場合には、その処理群用のトークンを別途用意するのが通常かな、と思います。

    > var taskList = new List<Task<bool>>(tasks);と書かれていますが,

    書かなくても自明なものは書かない。ってのは、C# の方向性の1つですね。そうしなければならないということはありません。(MSDN のサンプルコード類だとすべて型が明記されていますし)

    2010年11月30日 9:41
  • K.Takaoka さん、フォローありがとうございます。
    私は var が好きなので、var s = (string)null; とかもよく書きます。(^^;

    (聞かれてない気もしますけど)配列を元にリストを作り直す点については、taskManager はタスクの配列をもらって処理をする、というなるべく独立した形にしたかったからです。
    例では匿名メソッドの中で外のローカル変数を参照しているので、実際は全然独立はしてませんけど。。

    2010年11月30日 10:21
  • > 本当は IsCancellationRequested の監視じゃなくて

    本当は…というより、タスクはタスクの所属するトークンによる例外でしかキャンセルできないと思います。

    TH01 さんの例だと、Task の IsCanceled が true にならないで IsCompleted が true になって Result を false にして終了していますね。(所属しないトークンを使った例外は、通常のハンドリングされていない例外として扱われて IsFaulted が true になります。)

    2010年11月30日 11:00
  • K.Takaoka さん、ご指摘ありがとうございます!
    CancellationToken の仕組みが全然理解できていないまま、Result で結果を制御する安易な方法をとってしまいました。
    本当はまだ十分には把握できてないので後でちゃんと調べたいと思いますが、書いていただいた内容でなんとなく概要がわかり、勉強になりました。
    2010年12月1日 2:09
  • K.Takaoka さん,TH01さん

    回答ありがとうございました!!

    大変参考になりました!!

    2010年12月1日 12:48