С# 4 .NET 4.0 Parallel.For проблема с длинными циклами

Я тестирую какое-то решение для распараллеливания в С# 4, .NET 4.0.

У меня есть некоторые странные результаты, поэтому я хотел бы знать, правильно ли я все делаю или нет.

Вот описание моего кода:

//This will count the number of times we pass in the loop
private static double count_method_5 = 0;

//This will generate a MD5 hash
private static void GenerateMD5Hash(double i)
{
    var md5M = MD5.Create();
    byte[] data = Encoding.Unicode.GetBytes(Environment.UserName + i.ToString());
    byte[] result = md5M.ComputeHash(data);
} 

static void Main(string[] args)
{
    //Launch method Parallel for method 2
    var time9 = watch.ElapsedMilliseconds;
    int loop2 = 0;
    int limit2 = 300000;
    Parallel.For(loop2, limit2, new ParallelOptions { MaxDegreeOfParallelism = 8 }, i =>
    {
        GenerateMD5Hash(i);
        count_method_5++;
        loop2++;
    });
    var time10 = watch.ElapsedMilliseconds;
    Console.WriteLine("Parallel For second method  (method 5) Elapsed time :" + (time10 - time9) + "ms");    
    Console.WriteLine("Count method 5 : " + count_method_5);
}

Этот код дает мне такой результат:

Метод подсчета 5: 299250

вместо 300000.

Это неправильное использование параллелизма?


person Christophe    schedule 10.05.2016    source источник


Ответы (3)


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

Вам нужно либо lock код, который обращается к переменной (чтобы только один поток мог получить к ней доступ одновременно), либо использовать Interlocked.Exchange( ref count_method_5, Interlocked.Read(ref count_method_5) + 1) для выполнения потокобезопасного обновления метода.

На самом деле, подумав об этом, также возможно, что один поток считывает значение, а затем другой поток увеличивает его до того, как это сделает первый, и поэтому вы теряете это приращение. lock решит эту проблему, но Interlocked.Exchange сам по себе - нет.

Возможно, лучшим решением было бы использовать оба? Во всяком случае, это должно помочь вам начать.

i.e:

Object lockObject = new Object();
Parallel.For(loop2, limit2, new ParallelOptions { MaxDegreeOfParallelism = 8 }, i =>
    {
        GenerateMD5Hash(i);
        lock(lockObject){
        Interlocked.Exchange( ref count_method_5, Interlocked.Read(ref count_method_5) + 1);}
        loop2++;
    });

Я постараюсь показать пример возможных проблем:

  1. блокировка:

count_method_5: 1 Thread 1: updating count_method_5 Thread 2: tries to update count_method_5 and fails because Thread 1 is accessing it.

  1. потеря приращений:

count_method_5: 1 Thread 1: reads count_method_5 as 1 Thread 2: reads count_method_5 as 1 Thread 1: updates count_method_5 to 2 (1 + 1) Thread 2: updates count_method_5 to 2 (1 + 1)

Поэтому два потока обновили его, но он увеличился только на 1.


Обновление: вместо ужасно сложного Interlocked.Exchange( ref count_method_5, Interlocked.Read(ref count_method_5) + 1); мне напомнили, что вы можете просто использовать Interlocked.Increment(ref count_method_5);

person simonalexander2005    schedule 10.05.2016
comment
ЗАЧЕМ использовать Interlocked внутри lock? Вы можете использовать метод Add без какой-либо блокировки. - person VMAtm; 14.05.2016
comment
разве блокировка не предотвратит доступ только к этому конкретному фрагменту кода (поэтому, если другой фрагмент кода одновременно изменяет переменную, у вас все еще будет проблема, если вы не используете Interlocked.Increment)? - person simonalexander2005; 16.05.2016
comment
Да, если вы делаете изменения не с помощью Interlocked, вы должны заблокировать данные, так как ++ не является атомарной операцией. Но в таких случаях вам, вероятно, следует сделать цикл с Interlocked.CompareExchange, также без блокировок, так как в обычных случаях он будет работать лучше. Но надо мерить, иногда замки работают лучше - person VMAtm; 16.05.2016

Я думаю, что решение от @simonalexander2005 немного сложное. Почему бы не использовать метод Interlocked.Increment? В этом случае вы можете снять блокировку для вашего цикла, что будет работать лучше!

Parallel.For(loop2, limit2, new ParallelOptions { MaxDegreeOfParallelism = 8 }, i =>
{
    GenerateMD5Hash(i);
    Interlocked.Increment(ref count_method_5);
    Interlocked.Increment(ref loop2);
});

Если вам нужно добавить другое значение, а не 1, вы можете использовать Interlocked.Add, например:

Parallel.For(loop2, limit2, new ParallelOptions { MaxDegreeOfParallelism = 8 }, i =>
{
    GenerateMD5Hash(i);
    Interlocked.Add(ref count_method_5, 5);
    Interlocked.Add(ref loop2, 5);
});

Вы можете найти другие замечательные примеры Interlocked здесь. Другой вариант для вас — использовать цикл while с Interlocked.CompareExchange< /a>, но в вашем случае я не думаю, что это очень важно использовать.

person VMAtm    schedule 13.05.2016

Спасибо Simonalexander2005, вы правы!

Я попробовал ваше решение, и оно сработало! Я не думал о параллельном доступе к переменным!

Возможно, ключевое слово ref отсутствовало в вызове Interlocked.Read:

Parallel.For(loop2, limit2, new ParallelOptions { MaxDegreeOfParallelism = 8 }, i =>
{
    GenerateMD5Hash(i);
    lock (lockObject)
    {
    Interlocked.Exchange(ref count_method_5, Interlocked.Read(ref count_method_5) + 1);
    }
    loop2++;
});

Большое тебе спасибо!

Кристоф

person Christophe    schedule 10.05.2016
comment
Хочу отметить, что можно убрать блокировку и добавить цикл while для класса Interlocked, который будет работать лучше блокировок. - person VMAtm; 13.05.2016
comment
Также вы можете просто сделать: Interlocked.Increment для своих переменных и снять блокировки. - person VMAtm; 13.05.2016
comment
Спасибо за улучшение моих знаний о параллельном кодировании! Я ценю это. Я совсем забыл о Interlocked.Increment - person simonalexander2005; 16.05.2016
comment
Спасибо вам всем! Я узнал много нового о Interlocked и его функциях, которых не знал! Спасибо! - person Christophe; 18.05.2016