Странное поведение с многопоточностью и случайным - PullRequest
0 голосов
/ 07 февраля 2019

Edit @ Dublicate: я знаю, что небезопасное использование Thread не рекомендуется.Мой вопрос о том, почему, а не о том, является ли random потокобезопасным.Благодаря ответам, это помогло мне лучше понять это!

Я написал программу «трюк», которая должна отображать случайные символы со случайными цветами (для фона и для фона) в окне консоли с помощью Math.Random () и многопоточность.Для большей случайности я не сделал программу «поточно-безопасной».(Дополнительная информация: Первоначально я хотел, чтобы программа отображала Space-Invader в центре, я добился этого с помощью Thread-Safety, и я знаю, что многопоточность должна быть поточно-ориентированной, но это не тот вопрос, о котором идет речь)

Вывод выглядит так:

The expected Output

Функция программы такая: у меня есть массив, в котором все позиции (X / Y) с цветами и Char хранятся.У меня есть некоторые функции, которые изменяют этот массив, и у меня есть некоторые функции для отображения массива.Я также получил функцию для возврата случайных символов и одну для возврата случайных цветов.

Теперь вот что я не понимаю: иногда все работает, как описано, но иногда программа начинает отображать только! -Chars (восклицательный знак), но сохраняет случайные цвета и позиции:

Random colors with exclamation mark only

В другой раз программа отображает цвета только черный и белый, но символы остаются случайными:

Black and white with random chars

И иногда случается, что программа отображает только символы! -Черки и только черно-белые:

Black and white with exclamation mark only

Что я могу сказать,следующее:

Моя функция «Получить случайный символ» выглядит следующим образом:

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

! - Char - это Ascii-Char 33. Это означает, что если Программа застряла в Random-Char-Функция возвращает только самый низкий случайный символ (== 33 ==!)

Я получил что-то похожее для цветов.Я даю случайное число от 0 до 16 функции, чтобы вернуть 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);

Я знаю, что random.Next (0, 16) вернет максимальное число как 15,и 15 - белый цвет в примере.Если я поменяю 15 на красный, программа будет отображаться красным вместо белого, когда программа застрянет:

If white is changed to red

Это означает: когда программа получилаЗастрявшая функция Random-Char всегда возвращает минимально возможное число (33), а функция Random-Color всегда возвращает максимально возможное число (15).

Вопрос : Почему это поведение?Почему это просто иногда, а не каждый раз?И почему это каждый раз после разного времени бега?Почему одна случайная функция всегда возвращает максимальное число, а другая всегда минимальное число?

Я предполагаю, что это из-за предсказания ЦП, но, как я уже сказал, это всего лишь предположение, и яИнтересно, как это работает, и почему это происходит с разными подходами (мин / макс).

Вот некоторые из моих кодов, при необходимости я мог бы опубликовать больше код:

//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;
            }
        }
    }
}

Ответы [ 2 ]

0 голосов
/ 07 февраля 2019

Многопоточное использование объекта Random нарушает его внутреннее состояние.

Вы можете довольно легко воспроизвести проблему, используя следующую программу.Если вы запустите его, через некоторое время (или иногда сразу) он начнет производить нули вместо случайных чисел.Возможно, вам придется запустить его несколько раз, чтобы увидеть эффект.Кроме того, чаще всего происходит сбой в сборке RELEASE, а не в сборке DEBUG (что не является необычным для проблем с многопоточностью!).

(Примечание: это было проверено с использованием .Net 4.7.2 на 16-ядерном процессоре. Результаты могут отличаться для других систем.)

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());
            }
        }
    }
}
0 голосов
/ 07 февраля 2019

Random.Next() в конечном итоге вызывает этот код (из справочный источник ):

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

Глядя на этот код, очевидно, что многопоточный доступ может сделать некоторые очень плохие вещи,Например, если inext и inextp в конечном итоге содержат одно и то же значение, то SeedArray[locINext]-SeedArray[locINextp]; всегда будет иметь значение 0, а Random.Next() всегда будет возвращать 0.

Я уверен, что вы можете начатьпредставить множество других способов, которыми многопоточный доступ к этому коду может испортить ситуацию.

Концептуально, PRNG внутренне имеют большое количество состояний, где каждое состояние соответствует возможному выходу PRNG (хотя несколько состояний могутимеют одинаковый выход), и они проходят через каждое из этих состояний в детерминированном порядке.Если вы повредите внутреннее состояние PRNG, он может начать проходить через гораздо меньший набор этих состояний, прежде чем вернуться к первому, поэтому вы получите небольшой повторяющийся набор «случайных» выходных чисел.

В другом месте вашего кода вы создаете новый экземпляр Random каждый раз, когда вызывается метод.Как упомянуто в связанном ответе, это приведет ко многим Random экземплярам, ​​которые возвращают один и тот же набор случайных чисел.

Эрик Липперт постоянно публикует серию публикаций в блогах об улучшении класса Random: часть 1 часть 2 (и многое другое).

...