1

ツリーをたどるプロセスを高速化したいと思います。ノードの例を次に示します。

    class Node
    {
        public List<Node> Children { get; set; }
        public int SompeProperty { get; set; }
        public String SomeOtherProperty { get; set; }
    }

試行をトラバースする方法は次のようになります。

    static void TraverseTree(Node ParentNode)
    {
        if (ParentNode.Children == null)
            return;

        foreach (var child in ParentNode.Children)
        {
            TraverseTree(child);               
        }
    }

ParentNode.ChildrenNode はファイルまたはディレクトリを表すため、このメソッドには約 1 ミリ秒かかります。このノードの例を使用して、要点をよりよく説明しました。

したがって、最初のノードに 4 つの子があり、それらの子のそれぞれに 10000000 の子孫がある場合、並列プログラミングを利用して個別のスレッドでこれらの 4 つの子のそれぞれをトラバースする場合、このトラバーサルの速度を上げることができます。それがシナリオだった場合、私はそのアプローチをとったでしょう。しかし、ツリーの構造が事前にわからない場合、どうすればそれを行うことができるでしょうか?

私は考えていました:

1) ツリーのトラバースを開始し、子を持つ最初の 10 ノードをスタックに配置してから、個別のスレッドでそれぞれのトラバースを開始します。

2) 次のようにします。

    static void TraverseTree(Node ParentNode)
    {
        if (ParentNode.Children == null)
            return;

        foreach (var child in ParentNode.Children)
        {
            ThreadPool.QueueUserWorkItem(new WaitCallback((x) =>
            {                    
                TraverseTree(child);   
            }), null);                            
        }
    }

これはしばしば奇妙な結果をもたらしますが、かなり高速です。


結果

task を使用すると、アルゴリズムの速度が約 40% 向上しました。結果は次のとおりです。

C:\ ドライブ全体をスキャンするには、次のアルゴリズムで約5.81秒かかりました。

        //directoryPath  = "C:\"
    var now = DateTime.Now;

        Task<List<ScanItem>> t1 = new Task<List<ScanItem>>(() =>
        {
            return GetAllFilesInDirectory(directoryPath);
        });

        t1.Start();

        t1.Wait();

        var done = DateTime.Now-now;  // done = 5.81 average

C:\ ドライブ全体をスキャンするには、次のアルゴリズムで約3.01秒かかりました。

        //directoryPath  = "C:\"  
        var now = DateTime.Now;


        // get all directories in my c: drive it should only contain directories
        var directories = Directory.GetDirectories(directoryPath);

        // directories = 17 directories:  inetpub, MSOCache, PrefLogs, ProgramFiles, ProgramFiles (x86) etc...

        Task<List<ScanItem>>[] myTasks = new Task<List<ScanItem>>[directories.Length];

        // create a task fore each directory in the c:\ drive
        for (int k = 0; k < myTasks.Length; k++)
        {
            var currentDir = directories[k];
            myTasks[k] = new Task<List<ScanItem>>(() =>
            {
                return GetAllFilesInDirectory(currentDir);
            });                
        }

        // start all the tasks
        for (int k = 0; k < myTasks.Length; k++)
            myTasks[k].Start();


        Task.WaitAll(myTasks); // wait for all tasks to finish

        var done = now - DateTime.Now;  // average about 3.01 seconds

リストをトラバースする場所を指定すると、最初のアルゴリズムは 318,222 個のファイルとディレクトリを返します (これが正しい数です)。2番目のアルゴリズムは318,195を返しますが、これは非常に近いですが、理由はわかりません...

これを 8 コアのコンピューターでテストしています。おそらく、1 つのタスクを使用して 2 つのコアを備えたコンピューターでこれを実行する場所は、この 17 のタスクすべてを作成するよりも高速になる可能性があります。

ファイルを高速で取得するために使用するアルゴリズムを知りたい場合は、https://stackoverflow.com/a/724184/637142をご覧ください

4

4 に答える 4

13

独自の並列処理コードをローリングするのではなく、タスク並列ライブラリを使用してください。この種の問題を解決するのに理想的です。

TPLが機能する方法は、問題にスレッドを割り当てるのではなく、問題を「タスク」に分割し、利用可能なワーカーのプール間で作業を並列化する方法をTPLに任せるだけです。ツリーのサブブランチごとにタスクを作成するだけです。これらのタスクは、サブブランチ用に独自のタスクを生成できます。TPLは、プロセッサが飽和状態になるまで、プールからスレッドを割り当てます。

このため、タスクがCPUまたはI/OのどちらでゲートされるかをTPLに通知することが重要です。

  • タスクがCPUにバインドされている場合、TPLはCPUごとに1つのプールされたスレッドを割り当て、コアが使用可能になるまで他のタスクを待機させます。これにより、スループットが最大化され、すべてのプロセッサが飽和状態になります。それはまさにあなたが望むものです。4つのプロセッサを搭載したマシンを購入し、そのうちの2つがアイドル状態の場合、使用していない2つのコアの料金を支払いました。

  • 単一のタスクがI/Oバウンドの場合、タスクを作成するときにこのオプションを使用して、LongRunningこのタスクがコア全体を消費してはならないことをTPLに示すことができます。他のタスクは、そのコアで順番を与える必要があります。

  • そうであるように、I / Oバウンドのタスクが多数ある場合は、代わりにTaskCompletionSourceの使用を検討する必要があります。これにより、「継続」コールバックをより効率的に使用できるようになります。async/await継続をスケジュールするためにC#5の新機能を使用することも検討してください。非同期コードを書くためのはるかに快適な方法を提供します。

そしてもちろん、問題が実際にマシンのI / O機能を飽和させている場合は、プロセッサーの並列処理の量が低下することはないことを忘れないでください。スイミングプールを埋めている場合、同じ蛇口にホースを追加しても、その蛇口を通る流れは増加しません。

于 2012-04-05T16:38:08.457 に答える
2

マルチスレッドは、アプリケーションが単一のコアで 100% の CPU 時間を使用している場合にのみ役立つことに注意してください。CPU 使用率が低い場合 (ハード ドライブまたはネットワークの後に待機しているため)、コードを並列実行しても何のメリットもありません。

于 2012-04-05T16:29:12.427 に答える
2

ツリーを並行してトラバースする場合は、次のことを行う必要があります。

  • 個別のスレッドがツリーのさまざまな部分で動作することが保証されるようにツリーを分割します (たとえば、ルートから開始して、最大の並列度が達成されるまで子孫ノードを新しいスレッドに割り当てることができます)。
  • 一般に、ツリー構造が複数のスレッドによって安全にトラバースできることを確認してください (つまり、トラバースによって、ツリーの実装で状態変更の副作用が発生することはありません)。
  • トラバーサル中にスレッドがツリーを更新していないことを確認してください。

「奇妙な結果」が得られた場合、上記のいずれかが当てはまらない可能性があります。マルチスレッドの例では、ノードがトラバースされる順序は非決定論的であることに注意してください。結果が「奇妙」であると宣言したときに、それを説明しましたか?

たとえそうであっても:

  • ディレクトリの例では、マルチスレッド アプローチの有効性を制限する IO 競合が発生する可能性があります。
  • メモリ内ノードをトラバースすると、キャッシュから物事が追い出される傾向があり、複数のスレッドを使用することによる投資収益率が低下します (偽共有)。
于 2012-04-05T16:19:46.730 に答える
-1

最近、巨大なツリー構造 (実際にはファイル システムですが、何でもかまいません) を検出し、すべての項目に対して非同期操作を実行できるアルゴリズムを作成する必要がありました。それができる小さなライブラリ (.Net TPL と同時キューを使用して構築) を思いつきました。

  • 並行して巨大な木を発見
  • 親アイテムは常に子の前に処理されます
  • リソースの使用量は、ツリーのサイズではなく、指定された最大並列度に依存します
  • 非同期で動作する

並列非同期 TreeWalker

于 2016-07-24T19:33:09.017 に答える