Многопоточность С#.net

Я экспериментирую с оптимизацией некоторых математических операций с использованием С#.net в пакете под названием Grasshopper (часть Rhino3D). Операция довольно проста, но список, на котором он должен быть выполнен, большой и может стать намного больше.

Я использую Parallel.ForEach и списки в моем С# script, и количество конечных результатов, которые я получаю, ниже ожидаемого. Скорее всего, это связано с тем, что list.add не является потокобезопасным (или не потокобезопасным в программном обеспечении, в котором я его строю).

  private void RunScript(double z, int x, List<double> y, ref object A)
  {
    List<double> temp = new List<double>();
    double r;
    System.Threading.Tasks.Parallel.ForEach(y, numb =>
      {
      r = Math.Pow((numb * x), z);
      temp.Add(r);
      });
    A = temp;

Пожалуйста, помогите мне выяснить простой и эффективный способ выполнения этой простой математической операции на несколько сотен значений с использованием многопоточности процессора (или если у вас есть предложения о GPU CUDA).

Я надеюсь, что неясное и специфическое программное обеспечение не беспокоит вас, поскольку, насколько я знаю, он работает идентично обычным С#.Net/Python/VB.Net.

Ответ 1

Вы правильно поняли, List<T> не является потокобезопасным. Вы должны синхронизировать доступ к любому экземпляру.

Один из вариантов - просто синхронизировать в каждой задаче:

private void RunScript(double z, int x, List<double> y, ref object A)
{
    List<double> temp = new List<double>();
    object l = new object();
    System.Threading.Tasks.Parallel.ForEach(y, numb =>
    {
      double r = Math.Pow((numb * x), z);
      lock (l) temp.Add(r);
    });
    A = temp;
}

Примечание. у вашего кода была и другая ошибка. Вы использовали одну и ту же переменную r среди всех задач, что может привести к тому, что результат добавит два или более раза к результату, в то время как другие значения были опущены. Я исправил ошибку, просто переместив объявление переменной в тело анонимного метода, используемого для вызова ForEach().


Другой вариант - признать, что вы заранее знаете, сколько результатов у вас будет, и поэтому может просто инициализировать массив, достаточно большой, чтобы содержать все результаты:

private void RunScript(double z, int x, List<double> y, ref object A)
{
    double[] results = new double[y.Count];
    System.Threading.Tasks.Parallel.For(0, y.Count, i =>
    {
      // read-only access of `y` is thread-safe:
      results[i] = Math.Pow((y[i] * x), z);
    });
    A = new List<double>(results);
}

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

Вышеприведенное предполагает, что вам действительно нужен List<double> как выходной объект. Конечно, если массив удовлетворительный, вы можете просто назначить results на A вместо передачи его конструктору List<T>, чтобы создать в конце весь новый объект.

Ответ 2

Более простым решением, вероятно, будет использование .AsParallel() и работа над результирующим ParallelEnumerable вместо этого:

private void RunScript(double z, int x, List<double> y, ref object A)
{
    A = y
        .AsParallel().AsOrdered()
        .Select(elem => Math.Pow((elem * x), z))
        .ToList();
}

Ответ 3

Вот еще один вариант:

    private void RunScript(double z, int x, List<double> y, ref object A) {
        var temp = new System.Collections.Concurrent.BlockingCollection<double>();
        System.Threading.Tasks.Parallel.ForEach(y, numb => {
            double r = Math.Pow((numb * x), z);
            temp.Add(r);
        });
        A = temp; // if needed you can A = temp.ToList();
        }

Питер неплохо описал проблемы с вашим кодом, и я думаю, что вторая функция, которую он предлагает, вероятно, является вашим лучшим вариантом. Тем не менее приятно видеть альтернативы и узнавать, что среда .NET содержит совместимые безопасные коллекции.

Ответ 4

Большое спасибо за ваш вклад! Если вы заинтересованы в выходе профилировщика, выполните следующие действия:

Питер Дунихо 1-й вариант: 330 мс

Питер Дунихо 2-й вариант: 207 мс

Вариант DWeeberly: 335 мс

Вариант Маттиаса Буэленса: 376 мс

Это очень странно, возможно, сценарии .net должны запускаться быстрее в кузнечике (потому что это .net), однако ни одно из ваших решений не сравнивает параллельное вычисление python в 129 мс!

В любом случае спасибо всем вам за подробные ответы! Ты прекрасна!

Ответ 5

Я также смотрел на изменение ввода немного. Разделение данных на отдельные ветки, вычисление каждой ветки на отдельном потоке и последующая рекомбинация в конце. Однако он хуже, на 531 мс. Я понимаю, что script плохой, но я думаю, что он хорошо показывает мою идею и если правильно написано, может достичь успеха. Нет?

  private void RunScript(double z, int x, List<double> y, DataTree<double> u, ref object A)
  {
    System.Threading.Tasks.Task<double[]> th1 = System.Threading.Tasks.Task<double[]>.Factory.StartNew(() => mP(u.Branch(0).ToArray(), x, z));
    System.Threading.Tasks.Task<double[]> th2 = System.Threading.Tasks.Task<double[]>.Factory.StartNew(() => mP(u.Branch(1).ToArray(), x, z));
    System.Threading.Tasks.Task<double[]> th3 = System.Threading.Tasks.Task<double[]>.Factory.StartNew(() => mP(u.Branch(2).ToArray(), x, z));
    System.Threading.Tasks.Task<double[]> th4 = System.Threading.Tasks.Task<double[]>.Factory.StartNew(() => mP(u.Branch(3).ToArray(), x, z));
    System.Threading.Tasks.Task<double[]> th5 = System.Threading.Tasks.Task<double[]>.Factory.StartNew(() => mP(u.Branch(4).ToArray(), x, z));
    System.Threading.Tasks.Task<double[]> th6 = System.Threading.Tasks.Task<double[]>.Factory.StartNew(() => mP(u.Branch(5).ToArray(), x, z));
    System.Threading.Tasks.Task<double[]> th7 = System.Threading.Tasks.Task<double[]>.Factory.StartNew(() => mP(u.Branch(6).ToArray(), x, z));
    System.Threading.Tasks.Task<double[]> th8 = System.Threading.Tasks.Task<double[]>.Factory.StartNew(() => mP(u.Branch(7).ToArray(), x, z));

    List<double> list = new List<double>();

    list.AddRange(th1.Result);
    list.AddRange(th2.Result);
    list.AddRange(th3.Result);
    list.AddRange(th4.Result);
    list.AddRange(th5.Result);
    list.AddRange(th6.Result);
    list.AddRange(th7.Result);
    list.AddRange(th8.Result);


    A = list;


  }

Извините, я не могу добавить материал к "использованию"