Состояние гонки с использованием AsParallel - PullRequest
0 голосов
/ 10 апреля 2020

Итак, у меня нет большого опыта работы с параллельными проблемами, поэтому я решил задать вопрос, касающийся фрагмента кода.

У меня есть класс:

public class SimpleClass
{
    public int OwnNumber { get; set; }
    public int ActualNumber { get; set; }
}

есть два метода расширения, работающих с этим классом:

public static class SimpleClassHelper
{
    public static int Apply(this SimpleClass sc)
    {
        int z = 1;
        for (var i = 0; i < 1000; i++)
        {
            z += i;
        }
        return sc.ActualNumber;
    }

    public static IEnumerable<SimpleClass> Test(this IEnumerable<SimpleClass> sc) =>
        sc.AsParallel().Select(s => new SimpleClass
        {
            OwnNumber = s.OwnNumber,
            ActualNumber = s.Apply()
        });
}

и в моем Program.cs у меня есть это:

    static void Main(string[] args)
    {
        var s = new SimpleClass { OwnNumber = 0, ActualNumber = 0 };
        var s1 = new SimpleClass { OwnNumber = 1, ActualNumber = 1 };
        var s2 = new SimpleClass { OwnNumber = 2, ActualNumber = 2 };
        var s3 = new SimpleClass { OwnNumber = 3, ActualNumber = 3 };
        var s4 = new SimpleClass { OwnNumber = 4, ActualNumber = 4 };
        var s5 = new SimpleClass { OwnNumber = 5, ActualNumber = 5 };
        var s6 = new SimpleClass { OwnNumber = 6, ActualNumber = 6 };
        var s7 = new SimpleClass { OwnNumber = 7, ActualNumber = 7 };

        List<SimpleClass> seq = new List<SimpleClass>();
        seq.Add(s);
        seq.Add(s1);
        seq.Add(s2);
        seq.Add(s3);
        seq.Add(s4);
        seq.Add(s5);
        seq.Add(s6);
        seq.Add(s7);

        for (var i = 0; i < 10; i++)
        {
            var res = seq.Test();
            foreach (var item in res)
            {
                Console.WriteLine($"{item.OwnNumber} : {item.ActualNumber}");
            }
        }
    }

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

Мой ход мыслей таков:

  • Я создаю коллекцию seq
  • Я вызываю 10 раз Test() для этой коллекции
  • Каждый раз, когда я звоню Test, коллекция разделяется на несколько кусков, выполняемых параллельно
  • В какой-то момент, когда я применяю значение из s.Apply() Я ожидаю, что по крайней мере в некоторый процент времени ссылка в методе Apply() будет заменена ссылкой из некоторых параллельных потоков, и я получу различные значения OwnNumber и ActualNumber.

Однако я сделал несколько прогонов, и все вроде бы в порядке, но с параллелизмом вы никогда не узнаете. Это ожидаемое поведение AsParalle()? Поскольку все объекты вызывают один и тот же метод Apply(), почему я никогда не вижу описанное выше поведение? Если, скажем, 3 потока вызывают Apply() одновременно, я ожидаю, что по крайней мере в нескольких точках получим значение от какого-то другого объекта, тем более что у меня есть некоторая задержка с параметром for l oop?

Что ж, похоже, я ошибаюсь в своих предположениях, но есть ли что-то, что препятствует этому коду в условиях гонки?

1 Ответ

1 голос
/ 10 апреля 2020

Подумав немного, я думаю, что знаю, в чем смущение.

Давайте посмотрим на ваш Test метод?

public static IEnumerable<SimpleClass> Test(this IEnumerable<SimpleClass> sc) =>
    sc.AsParallel().Select(s => new SimpleClass
    {
        OwnNumber = s.OwnNumber,
        ActualNumber = s.Apply()
    });

.Select(s => {}) <- это «трансформирует» каждую локальную переменную <code>s

И это делает ваш текущий поток кода безопасным без каких-либо проблем с параллелизмом, поскольку s является local variable, нет никакого способа, которым другой поток может изменить ссылка на local variable.

Чтобы ваш код потерпел неудачу, вам понадобится shared variable, к которому одновременно могут обращаться несколько потоков в любой данный момент времени.

Например, если ваш код выглядел следующим образом:

public static class SimpleClassHelper
{
    private static SimpleClass _sharedSimpleClassHelper;

    public static int Apply()
    {
        int z = 1;
        for (var i = 0; i < 1000; i++)
        {
            z += i;
        }

        return _sharedSimpleClassHelper.ActualNumber; //use the shared variable
    }

    public static IEnumerable<SimpleClass> Test(this IEnumerable<SimpleClass> sc) =>
        sc.AsParallel().Select(s => 
        {
            _sharedSimpleClassHelper = s; //pass the local variable to a shared variable, that can be referenced from multiple threads
            return new SimpleClass
            {
                OwnNumber = s.OwnNumber,
                ActualNumber = Apply()
            };
        });
}

Вы можете увидеть его в действии здесь: https://dotnetfiddle.net/rF68e2


Редактировать:

Основываясь на вашем комментарии

, но все еще есть некоторая путаница, как метод stati c, который в theroy (по крайней мере, я так думаю) может быть вызван из разных потоков, гарантирует, что если вы вызовете метод с одним объектом, объект останется тем же самым до конца метода. В параллельном программировании обычно типы stati c имеют совершенно противоположную цель - делиться данными между потоками, а не инкапсулировать.

Да, метод stati c, как и любой метод, может быть вызван из разных потоков в одно и то же время, но что решает, является ли метод потокобезопасным, не так, если метод объявлен как stati c или нет, так это если потоки имеют общее состояние или нет.

Давайте еще раз посмотрите на ваш метод применения stati c (убрал для l oop, чтобы было легче смотреть)

public static int Apply(this SimpleClass sc)
{
    return sc.ActualNumber;
}

Что заставляет вас верить, что референт c sc может

sc - это local variable, который имеет только область действия этого метода, ничто за пределами этого метода не может дать этой локальной переменной другую ссылку.

Так что даже если Apply будет вызван x раз в то же время, каждый раз, когда все эти local variable будут иметь указатель на переданный в классе.

Конечно, если для примера ваш метод выглядел так:

public static int Apply(this SimpleClass sc)
{
    var localVariable_ActualNumber = sc.ActualNumber;
    localVariable_ActualNumber += 1;
    return localVariable_ActualNumber;
}

Этот код я Потокобезопасен, поскольку все потоки обращаются к общей памяти и управляют ею ActualNumber.

Например:

  1. Поток 1 читает ActualNumber (1)
  2. Тема 2 читает ActualNumber (1)
  3. Тема 1 + = 1
  4. Тема 1 пишет ActualNumer (2)
  5. Тема 3 читает ActualNumber (2)
  6. Тема 3 + = 1
  7. Тема 3 пишет ActualNumer (3)
  8. Тема 2 + = 1
  9. Тема 2 пишет ActualNumer (2)

Vs без потоков

  1. читает ActualNumber (1)
  2. + = 1
  3. пишет ActualNumer (2)
  4. читает ActualNumber (2)
  5. + = 1
  6. пишет ActualNumer (3)
  7. читает ActualNumber (3)
  8. + = 1
  9. пишет ActualNumer (4 )

Таким образом, даже если метод был введен 3 раза, ActualNumber будет только 2 вместо 4.

Но вы можете спросить, почему theads теперь делиться памятью?

Потому что local variable это всего лишь pointer для один SimpleClass, если вы обращаетесь к значению внутри SimpleClass и изменяете его, оно действительно для всех потоков.

Вы также можете спросить себя, считаете ли вы, что любой поток может получить доступ localVariable_ActualNumber?

Нет, они не могут.

То же самое относится к sc, к local variable нельзя получить доступ из другого потока.

Но если вы куда манипулировать sc.ActualNumber это может вызвать проблемы с безопасностью потоков, поскольку каждый sc указывает на один и тот же объект в памяти, а ActualNumber распределяется между потоками.

...