Подтвердить что ты не робот

Случайно генерирует число 1 более 90% раз параллельно

Рассмотрим следующую программу:

public class Program
{
     private static Random _rnd = new Random();
     private static readonly int ITERATIONS = 5000000;
     private static readonly int RANDOM_MAX = 101;

     public static void Main(string[] args)
     {
          ConcurrentDictionary<int,int> dic = new ConcurrentDictionary<int,int>();

          Parallel.For(0, ITERATIONS, _ => dic.AddOrUpdate(_rnd.Next(1, RANDOM_MAX), 1, (k, v) => v + 1));

          foreach(var kv in dic)
             Console.WriteLine("{0} -> {1:0.00}%", kv.Key, ((double)kv.Value / ITERATIONS) * 100);
     }
}

Это напечатает следующий результат:

(Обратите внимание, что вывод будет отличаться при каждом выполнении)

> 1 -> 97,38%
> 2 -> 0,03%
> 3 -> 0,03%
> 4 -> 0,03%
...
> 99 -> 0,03%
> 100 -> 0,03%

Почему число 1 генерируется с такой частотой?

4b9b3361

Ответ 1

Random безопасен не.

Next не делает ничего особенного для обеспечения безопасности потоков.

Не используйте Random, как это. И не учитывайте также длину локального хранилища потоков, иначе вы испортите статистические свойства генератора: вы должны использовать только один экземпляр Random. Один из подходов - использовать lock(_global) и нарисовать число в этой заблокированной области.

Я думаю, что здесь происходит то, что первый поток, который достигнет генератора, получает его случайные числа, сгенерированные правильно, и все последующие потоки получают 0 для каждого чертежа. С пулом потоков "распараллеливания" из 32 потоков, отношения, приведенные выше, приблизительно достигнуты; предполагая, что результаты для 31 потока помещены в первое ведро.

Ответ 2

Отправляя еще один шаг от решения локального хранилища потоков и стараясь избежать статистической проблемы, я предлагаю использовать случайное семя, сгенерированное с помощью RNGCryptoServiceProvider:

using System;
using System.Collections.Concurrent;
using System.Threading;
using System.Threading.Tasks;

namespace ConsoleApplication1
{
    class Program
    {

        private static readonly int ITERATIONS = 5000000;
        private static readonly int RANDOM_MAX = 101;

        private static int GetCriptoRandom()
        {
            using (var rng = new System.Security.Cryptography.RNGCryptoServiceProvider())
            {
                byte[] bytes = new byte[4];
                rng.GetBytes(bytes);
                return BitConverter.ToInt32(bytes, 0);
            }
        }

        private static ThreadLocal<Random> m_rnd = new ThreadLocal<Random>(() => new Random(GetCryptoRandom()));

        private static Random _rnd
        {
            get
            {
                return m_rnd.Value;
            }
        }

        static void Main(string[] args)
        {
            ConcurrentDictionary<int, int> dic = new ConcurrentDictionary<int, int>();
            Parallel.For(1, ITERATIONS, _ => dic.AddOrUpdate(_rnd.Next(1, RANDOM_MAX), 1, (k, v) => v + 1));
            foreach (var kv in dic)
                Console.WriteLine("{0} -> {1:0.00}%", kv.Key, ((double)kv.Value / ITERATIONS) * 100);

        }
    }
}

Кажется статистически правильным, результаты варьируются от 0,99% до 1,01%.

Ответ 3

Ну, класс Random не является потокобезопасным, самый простой выход - сделать его потоком локальным (каждый поток имеет свой собственный экземпляр Random):

private static ThreadLocal<Random> m_rnd = new ThreadLocal<Random>(() => new Random());

private static Random _rnd {
  get {
    return m_rnd.Value;
  }
}

https://msdn.microsoft.com/en-us/library/system.random(v=vs.110).aspx#ThreadSafety

Ответ 4

Random не является потокобезопасным - вы не должны использовать один и тот же экземпляр Random из нескольких потоков без синхронизации.

Почему вы получаете 1 в частности? Ну, способ Random работает (в 4.5.2) - хранить семенной массив, а также два индексатора. Когда вы используете его из нескольких потоков одновременно, ваш семенной массив будет запутан, и вы почти всегда получите одинаковые значения в нескольких слотах. Основная операция делает что-то вроде seed[a] - seed[b], и когда эти значения одинаковы, вы получаете нуль обратно. Поскольку вы просили 1 как минимум, этот ноль смещается на один - и там ваша аномалия. Это происходит очень быстро в многоядерной среде, так как существует довольно много взаимозависимого состояния, которое обновляется при каждом вызове Next.

Есть много способов решить эту проблему. Один из них - синхронизировать доступ к общему экземпляру Random - это имеет смысл только в том случае, если вы делаете относительно немного randoms, но в любом случае вы бы не использовали Parallel в любом случае. Если производительность является проблемой, вам нужно либо добавить некоторую форму предварительной выборки (например, подготовка случайных чисел в пакетах, для потоков или с использованием некоторой параллельной очереди), либо использовать какой-либо другой метод.

Другой способ - сохранить отдельный экземпляр Random для каждого потока. Это требует от вас тщательного выбора семени для каждого из экземпляров, хотя в противном случае ваши случайные числа могут оказаться довольно неслучайными. Подход, используемый в самой .NET(опять же, используя код версии 4.5.2 для ссылки), следует использовать Thread.CurrentThread.ManagedThreadId как семя, которое работает достаточно хорошо. Другим распространенным подходом является использование одного глобального (синхронизированного) экземпляра Random для инициализации семян другого Random s, но в зависимости от ваших требований вам может потребоваться гарантировать, что не будут созданы повторяющиеся семена.

Конечно, вы также можете использовать какой-либо другой генератор случайных чисел. Однако для псевдослучайных генераторов обычно требуются те же подходы, что и Random - они сильно зависят от их состояния; что делает их псевдослучайными в первую очередь. Криптографический генератор может работать лучше, но они, как правило, очень медленные, и в любом случае они могут вернуться к синхронизированному подходу, особенно если нет поддержки аппаратного обеспечения.

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