0

Edit @ Dublicate: I know that an unsafe use of Thread is not recomented. My question is about the why, and not about if random is thread-safe. Thanks to the answers, helped me understandig it better!

I wrote a "gimmick" program that is supposed to display random chars with random colors (For- and Background) in a Console-Window using Math.Random() and Multithreading. For more randomness I didn't made the program "thread safe". (Additional Information: I originally wanted the program to display an Space-Invader in the center, I achieved that with Thread-Safety, and I know, multithreading is supposed to be thread safe but this is not what this question is about)

The output looks like this:

The expected Output

The function of the program is like that: I have an Array in which all the Positions (X/Y) with colors and Char are stored. I have some Functions that change this array and I have some functions to display the array. I also got a function to return random chars and one to return random colors.

Now the point that I don't get: Sometimes everything works as described, but sometimes the program starts to display only !-Chars (exclamation mark) but keeps the random colors and positions:

Random colors with exclamation mark only

Another time the program only shows the colors black and white but the chars keep being random:

Black and white with random chars

And sometimes it happens, that the program only displays !-Chars and only black and white:

Black and white with exclamation mark only

What I could say is the following:

My "Get a Random Char" function looks like that:

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

!-Char is Ascii-Char 33. That means if the Program get stucked the Random-Char-Function only returns the lowest Random-Char (== 33 == !)

I got something similar for the colors. I give an random-number between 0 and 16 to a function to get back a Console-Color:

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);

I know that random.Next(0, 16) will give a 15 as maximum number back, and 15 is the color white in the example. If I change 15 to red, the Program will display red instead of white, when the program gets stucked:

If white is changed to red

This means: When the program got stucked the Random-Char-Function always returns the lowest possible number (33) and the Random-Color-Function always returns the highest possible number (15).

The question: Why is this behaviour? Why is it just sometimes and not every time? And why it is every time after a different time of running? Why is one random function always returning max-number and the other one always the min-number?

My guess is, that his is because of the CPU predicting, but as I said, it's just a guess and I wonder how this works, and why it goes to different approaches (min/max).

Here are some of my Code, if needed I could post more Code:

//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;
            }
        }
    }
}
Kiwimanshare
  • 130
  • 9
  • 5
    Random is not thread-safe. If two threads access it at the same time, its internal state may end up corrupted, possibly to the point that it always returns the same number or the same sequence of numbers. Accessing a non-thread-safe object in a multithreaded way does not give you more "randomness", it can give you exceptions, crashes, and strange and unpredictable behaviour. Don't do it. – canton7 Feb 07 '19 at 10:41
  • 1
    Possible duplicate of [Is C# Random Number Generator thread safe?](https://stackoverflow.com/questions/3049467/is-c-sharp-random-number-generator-thread-safe) – Chris Pickford Feb 07 '19 at 10:45
  • See marked dupe answer, it's likely because you are creating new instances of `Random` at the same time on each thread, effectively using the same seed. – Chris Pickford Feb 07 '19 at 10:46
  • it isn't clear what the scope of `randomX` is, since you don't show it; but if it is shared between threads: frankly, I'd just lock over it, so: `lock (randomX) { x = randomX.Next(0, 100); }`; similarly `random` – Marc Gravell Feb 07 '19 at 10:53
  • @MarcGravell It's used in ChangeData(), code on the bottom. Thanks all you for the replays, I will check that out. As mentioned, I got a Thread-Safe solution, I was just wondering why it is soooo random and not just crashing. – Kiwimanshare Feb 07 '19 at 10:56
  • All of your questions boil down to: "I wrote a program that has undefined behavior; why does it exhibit a particular behavior?" Well, undefined behavior means that **any behavior is permitted to happen**. You saw some behaviors. They are permitted to happen. Be glad that you got weird stuff displayed, and did not get all your files deleted. There's little point in trying to understand "why" a particular undefined behavior happened; you should follow the rules and prevent it. – Eric Lippert Feb 07 '19 at 23:19

2 Answers2

2

Random.Next() ends up calling this code (from 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;
  }

Looking at this code, it is obvious that multithreaded access could do some very bad things. For example, if inext and inextp end up containing the same value, then SeedArray[locINext]-SeedArray[locINextp]; will always result in 0, and Random.Next() will always return 0.

I'm sure you can start to imagine lots of other ways that multithreaded access to this code could mess things up.

Conceptually, PRNGs internally have a large number of states, where each state corresponds to a possible output of the PRNG (although multiple states can have the same output), and they walk through each one of those states in a deterministic order. If you corrupt the internal state of the PRNG, it may start walking through a much smaller set of those states before looping back to the first one, so you get a small repeating set of "random" output numbers.

In another place in your code, you are creating a new Random instance each time the method is called. As mentioned in the linked answer, this will result in many Random instances which return the same set of random numbers.

Eric Lippert has an ongoing series of blog posts about improving the Random class: part 1 part 2 (and more to come).

canton7
  • 37,633
  • 3
  • 64
  • 77
1

Your multithreaded use of the Random object is corrupting its internal state.

You can reproduce the issue fairly easily using the following program. If you run it, after a while (or sometimes, immediately) it will start producing zeroes instead of random numbers. You may have to run it several times to see the effect. Also, it goes wrong more often for a RELEASE build rather than a DEBUG build (which is not unusual for multithreading problems!).

(Note: This was tested using .Net 4.7.2 on a 16 core processor. Results may vary for other systems.)

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());
            }
        }
    }
}
Matthew Watson
  • 104,400
  • 10
  • 158
  • 276
  • Thank you! I'm gonna check this out! – Kiwimanshare Feb 07 '19 at 11:19
  • I tested your code, and after a while I only get zeros back, just as you said. Perfect :) In my code I also got only zeros back but I also have a PRNG that always gives back the highest number (random.next(0, 16)), I understand why there is always zero, thanks to canton7, but how is it possible to always get a 15 (max value)? – Kiwimanshare Feb 08 '19 at 08:43