7

次のコードで n が 0 にならないのはなぜですか。毎回 1000000 未満の大きさの乱数であり、場合によっては負の数になることもあります。

static void Main(string[] args)
{
    int n = 0;

    var up = new Thread(() =>
        {
            for (int i = 0; i < 1000000; i++)
            {
                n++;
            }
        });

    up.Start();

    for (int i = 0; i < 1000000; i++)
    {
        n--;
    }

    up.Join();

    Console.WriteLine(n);

    Console.ReadLine();
}

up.Join() は、WriteLine が呼び出される前に両方の for ループを終了させませんか?

ローカル変数は実際には舞台裏のクラスの一部であることを理解しています(クロージャーと呼ばれていると思います)が、ローカル変数 n は実際にはヒープが割り当てられているため、n が毎回 0 ではないことに影響しますか?

4

4 に答える 4

7

n++and操作は、n--アトミックであるとは限りません。各操作には次の 3 つのフェーズがあります。

  1. メモリから現在の値を読み取る
  2. 値の変更 (インクリメント/デクリメント)
  3. 値をメモリに書き込む

両方のスレッドがこれを繰り返し実行しており、スレッドのスケジューリングを制御できないため、次のような状況が発生します。

  • スレッド 1: 取得n(値 = 0)
  • スレッド 1: インクリメント (値 = 1)
  • スレッド 2: 取得n(値 = 0)
  • スレッド 1: 書き込みn(n == 1)
  • Thread2: デクリメント (値 = -1)
  • スレッド 1: 取得n(値 = 1)
  • スレッド 2: 書き込みn(n == -1)

等々。

これが、共有データへのアクセスをロックすることが常に重要である理由です。

-- コード:

static void Main(string[] args)
{

    int n = 0;
    object lck = new object();

    var up = new Thread(() => 
        {
            for (int i = 0; i < 1000000; i++)
            {
                lock (lck)
                    n++;
            }
        });

    up.Start();

    for (int i = 0; i < 1000000; i++)
    {
        lock (lck)
            n--;
    }

    up.Join();

    Console.WriteLine(n);

    Console.ReadLine();
}

-- 編集:lock仕組みの詳細...

ステートメントを使用すると、指定したオブジェクト (上記のコードlockのオブジェクト) のロックを取得しようとします。lckそのオブジェクトが既にロックされている場合、このlockステートメントにより、コードは続行する前にロックが解除されるまで待機します。

C#ステートメントは実質的にCritical Sectionlockと同じです。実際には、次の C++ コードに似ています。

// declare and initialize the critical section (analog to 'object lck' in code above)
CRITICAL_SECTION lck;
InitializeCriticalSection(&lck);

// Lock critical section (same as 'lock (lck) { ...code... }')
EnterCriticalSection(&lck);
__try
{
    // '...code...' goes here
    n++;
}
__finally
{
    LeaveCriticalSection(&lck);
}

C#lockステートメントでは、そのほとんどが抽象化されています。つまり、クリティカル セクションに入って (ロックを取得して)、そこから出るのを忘れるということがずっと難しくなっています。

ただし重要なことは、ロックしているオブジェクトのみが影響を受け、同じオブジェクトのロックを取得しようとしている他のスレッドに関してのみ影響を受けるということです。コードを記述してロック オブジェクト自体を変更したり、他のオブジェクトにアクセスしたりすることを妨げるものは何もありません。 あなたは、あなたのコードがロックを尊重し、共有オブジェクトへの書き込み時に常にロックを取得することを確認する責任があります。

そうしないと、このコードで見たような非決定論的な結果、または仕様作成者が「未定義の動作」と呼びたいものになります。Here Be Dragons (無限のトラブルが発生するバグの形で)。

于 2013-08-09T05:15:07.193 に答える
6

はい、呼び出されるup.Join()前に両方のループが確実に終了するようにします。WriteLine

ただし、両方のループが同時に実行され、それぞれが独自のスレッドで実行されています。

2 つのスレッド間の切り替えは常にオペレーティング システムによって行われ、プログラムの実行ごとに異なる切り替えタイミング セットが表示されます。

n--また、 andn++はアトミック操作ではなく、実際には 3 つのサブ操作にコンパイルされていることに注意する必要があります。

Take value from memory
Increase it by one
Put value in memory

パズルの最後のピースは、上記の 3 つの操作のいずれかの間で、 n++or内でスレッド コンテキストの切り替えが発生する可能性があることです。n--

そのため、最終的な値は非決定論的です。

于 2013-08-09T05:13:38.447 に答える
-2

以前にスレッドに参加する必要があります。

static void Main(string[] args)
    {
        int n = 0;

        var up = new Thread(() =>
        {
            for (int i = 0; i < 1000000; i++)
            {
                n++;
            }
        });

        up.Start();
        up.Join();

        for (int i = 0; i < 1000000; i++)
        {
            n--;
        }


        Console.WriteLine(n);

        Console.ReadLine();
    }
于 2013-08-09T05:16:05.183 に答える