2

私がこの実験を書いたのは、共有データに複数のスレッドで同時にアクセスすることは大したことではないことを誰かに示すためです。驚いたことに、作成したスレッドの数に関係なく、同時実行の問題を作成できず、値は常にバランスの取れた値 0 になりました。インクリメント演算子がスレッドセーフではないことがわかっているため、メソッドが存在します。Interlocked.Increment()と同様ですInterlocked.Decrement()(ここにも記載されていますIs the ++ operator thread safe?)。

インクリメント/デクリメント演算子がスレッド セーフでない場合、以下のコードが問題なく実行され、期待値になるのはなぜですか?

以下のスニペットは、2,000 のスレッドを作成します。データが複数のスレッドによって同時にアクセスされることを保証するために、1,000 が常に増加し、1,000 が常に減少します。さらに悪いことに、通常のプログラムではスレッド数がほとんどないということです。しかし、同時実行の問題を作成するために誇張された数値にもかかわらず、値は常に 0 のバランスのとれた値になります。

static void Main(string[] args)
    {
        Random random = new Random();
        int value = 0;

        for (int x=0; x<1000; x++)
        {
            Thread incThread = new Thread(() =>
            {
                for (int y=0; y<100; y++)
                {
                    Console.WriteLine("Incrementing");
                    value++;
                }
            });

            Thread decThread = new Thread(() =>
            {
                for (int z=0; z<100; z++)
                {
                    Console.WriteLine("Decrementing");
                    value--;
                }
            });

            incThread.Start();
            decThread.Start();
        }

        Thread.Sleep(TimeSpan.FromSeconds(15));
        Console.WriteLine(value);
        Console.ReadLine();
    }

誰かが私に説明を提供してくれることを願っています。そうすれば、スレッドセーフなソフトウェアを作成するための私のすべての努力が無駄ではないこと、またはおそらくこの実験に何らかの欠陥があることがわかります。また、すべてのスレッドをインクリメントして、i++ の代わりに ++i を使用してみました。値は常に期待値になります。

4

3 に答える 3

5

通常、非常に近い時間に増加および減少する 2 つのスレッドがある場合にのみ、問題が発生します。(メモリ モデルの問題もありますが、それらは別のものです。) つまり、操作が衝突する可能性を最大限に高めるために、ほとんどの時間をインクリメントとデクリメントに費やす必要があります。

現在、スレッドはスリープ状態またはコンソールへの書き込みに大部分の時間を費やしています。これにより、衝突の可能性が大幅に減少します。

さらに、証拠の欠如は不在の証拠ではないことに注意してください。特に、強力なメモリモデルと内部アトミックインクリメント/デクリメント命令を備えたCPUで実行している場合は特に、並行性の問題を引き起こすのは難しい場合があります。 JITが使えます。特定のマシンで問題が発生することはないかもしれませんが、別のマシンでは同じプログラムが失敗する可能性があります。

于 2012-11-26T07:43:48.407 に答える
1

IMOこれらのループは短すぎます。2番目のスレッドが開始するまでに、最初のスレッドはすでにループの実行を終了して終了しているに違いありません。各スレッドが実行する反復回数を大幅に増やすようにしてください。この時点で、2つのスレッドだけを生成する(外側のループを削除する)こともでき、間違った値を表示するのに十分なはずです。

たとえば、次のコードを使用すると、システムでまったく間違った結果が得られます。

static void Main(string[] args)
{
    Random random = new Random();
    int value = 0;

    Thread incThread = new Thread(() =>
            {
                for (int y = 0; y < 2000000; y++)
                {
                    value++;
                }
            });

    Thread decThread = new Thread(() =>
            {
                for (int z = 0; z < 2000000; z++)
                {
                    value--;
                }
            });

    incThread.Start();
    decThread.Start();

    incThread.Join();
    decThread.Join();
    Console.WriteLine(value);
}
于 2012-11-26T08:53:42.907 に答える
1

Jon Skeetsの回答に加えて:

少なくとも私の小さなデュアルコアで問題を簡単に示す簡単なテスト:

Sub Main()

    Dim i As Long = 1
    Dim j As Long = 1

    Dim f = Sub()
                While Interlocked.Read(j) < 10 * 1000 * 1000
                    i += 1
                    Interlocked.Increment(j)
                End While
            End Sub

    Dim l As New List(Of Task)
    For n = 1 To 4
        l.Add(Task.Run(f))
    Next

    Task.WaitAll(l.ToArray)
    Console.WriteLine("i={0}   j={1}", i, j)
    Console.ReadLine()


End Sub

i と j は両方とも同じ最終値を持つ必要があります。しかし、彼らは持っていません!

編集

そして、C# は VB よりも賢いと思われるかもしれません。

    static void Main(string[] args)
    {
        long i = 1;
        long j = 1;

        Task[] t = new Task[4];

        for (int k = 0; k < 4; k++)
        {
            t[k] = Task.Run(() => {
                            while (Interlocked.Read(ref j) < (long)(10*1000*1000))
                            {
                                i++;
                                Interlocked.Increment(ref j);
                            }});
        }

        Task.WaitAll(t);
        Console.WriteLine("i = {0}   j = {1}", i, j);
        Console.ReadLine();

    }

そうではありません;)

結果: i は j より約 15% (パーセント!) 低くなります。私のマシンで。8 スレッド マシンを使用すると、probabyl によって結果がさらに差し迫ったものになる可能性さえあります。複数のタスクが真に並行して実行され、単にプリエンプトされるだけではない場合、エラーが発生する可能性が高くなるからです。

もちろん、上記のコードには欠陥があります:(
i++ の直後にタスクが横取りされた場合、他のすべてのタスクは i と j をインクリメントし続けるため、「++」がアトミックであっても、i は j とは異なると予想されます。ただし、簡単な解決策:

    static void Main(string[] args)
    {
        long i = 0;
        int runs = 10*1000*1000;

        Task[] t = new Task[Environment.ProcessorCount];

        Stopwatch stp = Stopwatch.StartNew();

        for (int k = 0; k < t.Length; k++)
        {
            t[k] = Task.Run(() =>
            {
                for (int j = 0; j < runs; j++ )
                {
                    i++;
                }
            });
        }

        Task.WaitAll(t);

        stp.Stop();

        Console.WriteLine("i = {0}   should be = {1}  ms={2}", i, runs * t.Length, stp.ElapsedMilliseconds);
        Console.ReadLine();

    }

これで、タスクはループ ステートメントのどこかでプリエンプトされる可能性があります。しかし、それは効果がありませんi。したがって、効果を確認する唯一の方法は、タスクがステートメントiの時点で横取りされた場合です。i++そして、それが示されるべきものでした: それは起こる可能性があり、実行中のタスクが少なくても実行時間が長い場合に発生する可能性が高くなります. コードの代わりに
書くと、(ロックのため) 実行時間が長くなりますが、まさにそうあるべきです!Interlocked.Increment(ref i);i++i

于 2012-11-26T08:04:13.880 に答える