多线程和随机的奇怪行为

Strange behavior with multithreading and random

编辑@Dublicate:我知道不推荐对 Thread 的不安全使用。我的问题是关于为什么,而不是关于随机是否是线程安全的。感谢您的回答,帮助我更好地理解它!

我编写了一个 "gimmick" 程序,该程序应该使用 Math.Random() 和多线程在控制台中显示具有随机颜色(For- 和背景)的随机字符-Window。为了更多的随机性,我没有制作程序"thread safe"。 (附加信息:我最初希望程序在中心显示一个 Space-Invader,我通过线程安全实现了这一点,而且我知道,多线程应该是线程安全的,但这不是这个问题关于)

输出如下所示:

程序的功能是这样的:我有一个数组,其中存储了所有具有颜色和字符的位置(X/Y)。我有一些函数可以改变这个数组,我有一些函数可以显示数组。我还有一个 return 随机字符和一个 return 随机颜色的函数。

现在我不明白的一点是:有时一切都如描述的那样工作,但有时程序开始只显示 !-Chars(感叹号)但保持随机颜色和位置:

另一次程序只显示黑色和白色,但字符保持随机:

有时会发生这种情况,程序只显示 !-Chars 并且只显示黑色和白色:

我能说的是:

我的 "Get a Random Char" 函数看起来像这样:

public static char GetChar()
{
    return (char)randomChar.Next(33, 150);
}

!-Char 是 Ascii-Char 33。这意味着如果程序卡住,Random-Char-Function 只有 returns 最低的 Random-Char (== 33 == !)

我得到了类似的颜色。我给函数一个 0 到 16 之间的随机数来取回控制台颜色:

private ConsoleColor SetColor(char ColorIndex)
{
    switch (ColorIndex)
    {
        case (char)1:
            return ConsoleColor.Black;
        case (char)2:
            return ConsoleColor.Blue;
        case (char)3:
            return ConsoleColor.Cyan;
        case (char)4:
            return ConsoleColor.DarkBlue;
        case (char)5:
            return ConsoleColor.DarkCyan;
        case (char)6:
            return ConsoleColor.DarkGray;
        case (char)7:
            return ConsoleColor.DarkGreen;
        case (char)8:
            return ConsoleColor.DarkMagenta;
        case (char)9:
            return ConsoleColor.DarkRed;
        case (char)10:
            return ConsoleColor.DarkYellow;
        case (char)11:
            return ConsoleColor.Gray;
        case (char)12:
            return ConsoleColor.Green;
        case (char)13:
            return ConsoleColor.Magenta;
        case (char)14:
            return ConsoleColor.Red;
        case (char)15:
            return ConsoleColor.White;
        case (char)16:
            return ConsoleColor.Yellow;
        default:
            return ConsoleColor.Black;
    }
}

//The call looks like that:
_Data[x, y, 1] = (char)random.Next(0, 16);

我知道 random.Next(0, 16) 将返回 15 作为最大数字,15 是示例中的白色。如果我将 15 更改为红色,当程序卡住时,程序将显示红色而不是白色:

这意味着:当程序卡住时,Random-Char-Function 总是 returns 最低可能的数字 (33) 而 Random-Color-Function 总是 returns 最高可能数 (15)。

问题:为什么会出现这种行为?为什么只是有时而不是每次?为什么每次都在 运行 的不同时间之后?为什么一个随机函数总是 return 最大数而另一个总是最小数?

我的猜测是,他的是因为 CPU 预测,但正如我所说,这只是一个猜测,我想知道它是如何工作的,以及为什么它采用不同的方法(min/max).

这是我的一些代码,如果需要我可以 post 更多代码:

//Program-Start after initialising some stuff:
public AnotherOne()
{
    new Thread(DrawData).Start();
    new Thread(DrawDataLR).Start();
    new Thread(DrawDataRL).Start();
    new Thread(DrawDataTD).Start();
    new Thread(DrawDataDT).Start();
    new Thread(ChangeData).Start();
    ChangeData();
}

//Draw Data example for Left-Right:
private void DrawDataLR()
{
    while (!_done)
    {
        DrawDataLeftRight();
        Thread.Sleep(2);
    }
}

private void DrawDataLeftRight()
{
    for (int x = 0; x < _Width - 1; x++)
    {
        for (int y = 0; y < _Height - 1; y++)
        {
            Console.SetCursorPosition(x, y);
            Console.BackgroundColor = SetColor(_Data[x, y, 1]);
            Console.ForegroundColor = SetColor(_Data[x, y, 2]);
            Console.WriteLine(_Data[x, y, 0]);
        }
    }
}

//Draw Data Function:
private void DrawData()
{
    Random next = new Random();
    int x = 1;

    while (!_done)
    {
        x = next.Next(1, 5);

        switch (x)
        {
            case 1:
                DrawDataLeftRight();
                break;
            case 2:
                DrawDataTopDown();
                break;
            case 3:
                DrawDataRightLeft();
                break;
            case 4:
                DrawDataDownTop();
                break;
        }
    }
}

//Change Data Function with "stripes" as Example:
private void ChangeData()
{
    int x = 100;

    while (!_done)
    {
        FillRandomFeld();
        Thread.Sleep(x);
        ClearChar();
        Thread.Sleep(x);
        SetColor();
        Thread.Sleep(x);
        Stripes();
        Thread.Sleep(x);
        SetChar();
        Thread.Sleep(x);
        OtherStripes();
        Thread.Sleep(x);

        x = randomX.Next(0, 100);
    }
}

private void Stripes()
{
    char colr = (char)random.Next(0, 16);

    for (int x = 0; x < _Width - 1; x += 4)
    {
        for (int y = 0; y < _Height - 1; y++)
        {
            if (_Data[x, y, 3] != (char)1)
            {
                _Data[x, y, 1] = colr;
                _Data[x, y, 2] = colr;
            }
        }
    }
}

Random.Next() 最终调用此代码(来自 the reference source):

  private int InternalSample() {
      int retVal;
      int locINext = inext;
      int locINextp = inextp;

      if (++locINext >=56) locINext=1;
      if (++locINextp>= 56) locINextp = 1;

      retVal = SeedArray[locINext]-SeedArray[locINextp];

      if (retVal == MBIG) retVal--;          
      if (retVal<0) retVal+=MBIG;

      SeedArray[locINext]=retVal;

      inext = locINext;
      inextp = locINextp;

      return retVal;
  }

看这段代码,很明显多线程访问可能会做一些非常糟糕的事情。例如,如果 inextinextp 最终包含相同的值,那么 SeedArray[locINext]-SeedArray[locINextp]; 将始终为 0,而 Random.Next() 将始终为 return 0.

我相信您可以开始想象多线程访问这段代码的许多其他方式可能会把事情搞砸。

从概念上讲,PRNG 内部有大量状态,其中每个状态对应于 PRNG 的一个可能输出(尽管多个状态可以有相同的输出),并且它们以确定性的方式遍历每个状态命令。如果你破坏了 PRNG 的内部状态,它可能会在循环回到第一个状态之前开始遍历一组更小的状态,所以你会得到一组小的重复 "random" 输出数字。

在代码的另一个地方,每次调用该方法时都会创建一个新的 Random 实例。如链接答案中所述,这将导致许多 Random 个实例,其中 return 同一组随机数。

Eric Lippert 有一系列关于改进 Random class 的博客文章:part 1 part 2(还有更多)。

您对 Random 对象的多线程使用正在破坏其内部状态。

您可以使用以下程序很容易地重现该问题。如果你 运行 它,一段时间后(或有时,立即)它会开始产生零而不是随机数。您可能需要 运行 多次才能看到效果。此外,对于 RELEASE 构建而不是 DEBUG 构建,它更容易出错(这对于多线程问题并不罕见!)。

(注意:这是在 16 核处理器上使用 .Net 4.7.2 测试的。结果可能因其他系统而异。)

using System;
using System.Threading.Tasks;

namespace Demo
{
    class Program
    {
        static void Main()
        {
            Console.WriteLine();

            Random rng = new Random(12345);

            Parallel.Invoke(
                () => printRandomNumbers(rng),
                () => printRandomNumbers(rng),
                () => printRandomNumbers(rng));

            Console.ReadLine();
        }

        static void printRandomNumbers(Random rng)
        {
            while (rng.Next() != 0)
            {}

            while (true)
            {
                Console.WriteLine(rng.Next());
            }
        }
    }
}