У меня есть распараллеленная процедура для выполнения серии вычислений для каждого объекта в большом массиве указателей, где вычисления требуют, чтобы каждый поток мог читать все другие объекты, но когда-либо будет писать только в один объект. Я настроил его так, как показано ниже

#include <atomic>
#include <thread>

void threadFunction(Object** objects, int n);

std::atomic<int> idx;
int nobjects = 10000;

int main() {
  int nthreads = 4;
  Object** objects = new Object*[nobjects];

  idx = 0;
  std::thread threads[nthreads];
  for (int ii = 0; ii < nthreads; ii ++) {
    threads[ii] = std::thread(threadFunction, objects, ii);
  }

  while (idx < nobjects - 1) {}    // Wait until all the calculations have been done

  for (int ii = 0; ii < nthreads; ii ++) {
    threads[ii].join();
  }
}

void threadFunction(Object** objects, int n) {
  Object* current = NULL;
  while (idx < nobjects - 1) {
    current = objects[idx++];
    // do calculation
  }
}

Где Object - это настраиваемый класс, но для этих целей его можно заменить примитивом. У меня вопрос, насколько «безопасно» делать это таким образом? Я понимаю, что типы atomic защищены от частичной записи, но могу ли я быть уверен, что это будет работать каждый раз даже для больших массивов?

Это может быть слишком конкретным, но я буду признателен за любую помощь, которую смогу получить. Спасибо

2
user10713417 27 Ноя 2018 в 23:18

1 ответ

Лучший ответ

Как отмечали другие в комментариях, у вас есть состояние гонки между проверкой условия цикла и использованием значения idx. Это может привести к тому, что вы прочитаете за пределами конца массива. Ваша функция потока просто нуждается в небольшой настройке:

void threadFunction(Object** objects, int n) {
  Object* current = NULL;
  while (true) {
    int next = idx++;
    if (next < nobjects - 1) {
      current = objects[next];
      // do calculation
    } else {
      break;
    }
  }
}

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

Использование стандартной библиотеки

Хотя об этом специально не спрашивали, вероятно, стоит отметить, что все это можно сделать с помощью стандартной библиотеки (которая позволяет избежать сложных вопросов безопасности и решает такие проблемы, как разбиение на разделы). Примерно так:

void DoCalculations(Object& obj)
{
  // details...
}

// later...

std::vector<std::unique_ptr<Object>> objects = CreateObjects();
std::for_each(
  std::execution::par,
  objects.begin(),
  objects.end(),
  [] (std::unique_ptr<Object> const& p) { DoCalculations(*p); });
3
Peter Ruderman 27 Ноя 2018 в 21:02