0

文字列内の文字を数える非常に単純なプログラムがあります。整数threadnumはスレッドの数を設定し、それに応じてデータthreadnumを各スレッドが処理するチャンクに分割します。

各スレッドは、共有ディクショナリに含まれる値をインクリメントし、文字ヒストグラムを作成します。

private Dictionary<UInt32, int> dict = new Dictionary<UInt32, int>();
  • すべてのスレッドが終了するのを待ってメイン プロセスを続行するために、Thread.Join
  • 最初は、後でマージされる各スレッドのローカル ディクショナリがありましたが、共有ディクショナリはロックせずに正常に機能しました。
  • メソッドBuildDictionaryでは参照はロックされませんが、ディクショナリのロックはスレッド実行時間に大きな影響を与えませんでした。
  • 各スレッドの時間が計測され、結果の辞書が比較されます。
  • ディクショナリの内容は、スレッドが 1 つであろうと複数であろうと、同じであるべきです
  • 各スレッドが完了するには、threadnum によって決定される部分が必要です

問題:

合計時間はおおよそ の倍数ですthreadnum。つまり、実行時間は増加します。

(残念ながら、現時点では C# プロファイラーを実行できません。さらに、C# 3 コードとの互換性を希望します。)

他の人も同様に苦労している可能性があります。VS 2010 Express Edition の vshostプロセスがスタックし、スレッドが順番に実行されるようにスケジュールしている可能性がありますか?

別の MT パフォーマンスの問題が最近投稿され、「Visual Studio C# 2010 Express Debug running Faster than Release」として投稿されました

コード:

public int threadnum = 8;
Thread[] threads = new Thread[threadnum];
Stopwatch stpwtch = new Stopwatch();
stpwtch.Start();
for (var threadidx = 0; threadidx < threadnum; threadidx++)
{
    threads[threadidx] = new Thread(BuildDictionary);
    threads[threadidx].Start(threadidx);
    threads[threadidx].Join(); //Blocks the calling thread, till thread completion
}
WriteLine("Total - time: {0} msec", stpwtch.ElapsedMilliseconds);

助けていただけますか?

更新

スレッド数の増加に伴うほぼ直線的な速度低下の奇妙な動作は、IDE のデバッガーの多数のフックによるアーティファクトのようです。

開発者環境の外でプロセスを実行すると、実際に 2 つの論理/物理コア マシンで 30% の速度向上が得られます。デバッグ中、私はすでに CPU 使用率の上限に達しているため、アイドル コアを追加することで、開発中にある程度の余裕を持たせることが賢明であると思われます。

最初と同じように、各スレッドに独自のローカル データ チャンクで計算させます。このローカル データ チャンクはロックされ、共有リストに書き戻され、すべてのスレッドが終了した後に集計されます。

結論

プロセスが実行されている環境に注意してください。

4

6 に答える 6

3

Tony the Lion が彼の回答で言及している辞書の同期の問題は、しばらく脇に置くことができます。現在の実装では、実際には何も並行して実行していないからです。

ループで現在何をしているかを見てみましょう。

  • スレッドを開始します。
  • スレッドが完了するまで待ちます。
  • 次のスレッドを開始します。

Joinつまり、ループ内で呼び出すべきではありません。

代わりに、現在行っているようにすべてのスレッドを開始する必要がありますが、AutoResetEventすべてのスレッドがいつ完了したかを判断するには、 などの信号構造を使用してください。

サンプルプログラムを参照してください:

class Program
{
    static EventWaitHandle _waitHandle = new AutoResetEvent(false);

    static void Main(string[] args)
    {
        int numThreads = 5;
        for (int i = 0; i < numThreads; i++)
        {
            new Thread(DoWork).Start(i);
        }
        for (int i = 0; i < numThreads; i++)
        {
            _waitHandle.WaitOne();
        }
        Console.WriteLine("All threads finished");
    }

    static void DoWork(object id)
    {
        Thread.Sleep(1000);
        Console.WriteLine(String.Format("Thread {0} completed", (int)id));
        _waitHandle.Set();
    }
}

Joinまたは、使用可能なスレッドへの参照がある場合は、2 番目のループで呼び出すこともできます。

これを行った後は、辞書の同期の問題について心配することができますし、心配する必要があります。

于 2012-09-06T08:14:09.393 に答える
2

コレクションが変更されていない限り、ディクショナリは複数のリーダーを同時にサポートできます。MSDNから

あなたは言う:

しかし、共有辞書はロックせずに正常に機能しました。

各スレッドは、共有ディクショナリに含まれる値をインクリメントします

プログラムは定義上壊れています。適切にロックせずに辞書のデータを変更すると、バグが発生します。これ以上言う必要はありません。

于 2012-09-06T08:09:34.447 に答える
1

私はいくつかの共有を使用しませんstatic Dictionary。各スレッドがローカルコピーで機能する場合、すべてのスレッドが完了を通知したら、結果を統合できます。

WaitHandle.WaitAllのデッドロックを回避しますAutoResetEvent

class Program
{
    static void Main()
    {
        char[] text = "Some String".ToCharArray();
        int numThreads = 5;

        // I leave the implementation of the next line to the OP.
        Partition[] partitions = PartitionWork(text, numThreads);

        completions = new WaitHandle[numThreads];
        results = IDictionary<char, int>[numThreads];

        for (int i = 0; i < numThreads; i++)
        {
            results[i] = new IDictionary<char, int>();
            completions[i] = new ManualResetEvent(false);
            new Thread(DoWork).Start(
                text,
                partitions[i].Start,
                partitions[i].End,
                results[i],
                completions[i]);
        }

        if (WaitHandle.WaitAll(completions, new TimeSpan(366, 0, 0, 0))
        {
            Console.WriteLine("All threads finished");
        }
        else
        {
            Console.WriteLine("Timed out after a year and a day");
        }

        // Merge the results
        IDictionary<char, int> result = results[0];
        for (int i = 1; i < numThreads - 1; i ++)
        {
            foreach(KeyValuePair<char, int> item in results[i])
            {
                if (result.ContainsKey(item.Key)
                {
                    result[item.Key] += item.Value;
                }
                else
                {
                   result.Add(item.Key, item.Value);
                }
            }
        }
    }

    static void BuildDictionary(
        char[] text, 
        int start, 
        int finish,
        IDictionary<char, int> result,
        WaitHandle completed)
    {
        for (int i = start; i <= finish; i++)
        {
            if (result.ContainsKey(text[i])
            {
                result[text[i]]++;
            }
            else
            {
               result.Add(text[i], 1);
            }
        }
        completed.Set();
    }
}

この実装では、これまでに共有される唯一の変数はのchar[]であり、text常に読み取り専用です。

最後に辞書をマージする負担はありますが、それは同時発生の問題を回避するための小さな代償です。フレームワークの新しいバージョンでは、TPLConcurrentDictionaryとおそらくを使用していPartitioner<TSource>ました。

于 2012-09-06T09:29:14.153 に答える
0

私は TonyTheLion などに完全に同意します。間違った場所で参加するという実際の問題を修正しても、ロック (なし) と共有辞書の更新にはまだ問題があります。簡単な回避策を紹介したいと思います。整数値をオブジェクトにラップするだけです。

それ以外の:

Dictionary<uint, int> dict = new Dictionary<uint, int>();

使用する:

class Entry { public int value; }
Dictionary<uint, Entry> dict = new Dictionary<uint, Entry>();

代わりに Entry::value をインクリメントするようになりました。そうすれば、ディクショナリは変更に気付かず、ディクショナリをロックすることなく安全になります。

注: ただし、これは、1 つのスレッドが独自の 1 つのエントリのみを使用することが保証されている場合にのみ機能します。あなたが「文字のヒストグラム」と言ったように、これは真実ではないことに気づきました。インクリメント中に各エントリをロックする必要があります。そうしないと、一部のインクリメントが失われる可能性があります。それでも、エントリ層でのロックは、辞書全体でのロックと比較して大幅に高速化されます

于 2012-09-06T08:17:07.190 に答える
0

Rotem が指摘しているように、ループに参加することで、各スレッドが完了するのを待ってから続行します。

これがなぜなのかについてのヒントは、MSDN の Thread.Join ドキュメントにあります。

スレッドが終了するまで呼び出しスレッドをブロックします

そのため、その 1 つのスレッドが作業を完了するまで、ループは続行されません。すべてのスレッドを開始し、それらが完了するのを待つには、ループの外でそれらを結合します。

public int threadnum = 8;
Thread[] threads = new Thread[threadnum];
Stopwatch stpwtch = new Stopwatch();
stpwtch.Start();

// Start all the threads doing their work
for (var threadidx = 0; threadidx < threadnum; threadidx++) 
{
     threads[threadidx] = new Thread(BuildDictionary);
     threads[threadidx].Start(threadidx);
}
// Join to all the threads to wait for them to complete
for (var threadidx = 0; threadidx < threadnum; threadidx++) 
{
    threads[threadidx].Join();
}

System.Diagnostics.Debug.WriteLine("Total - time: {0} msec", stpwtch.ElapsedMilliseconds);

BuildDictionary 関数を投稿する必要があります。複数のスレッドを使用しても操作が高速化されず、スレッド化のオーバーヘッドによって実際に実行時間が長くなる可能性が非常に高くなります。

于 2012-09-06T08:30:23.397 に答える