Безопасна ли итерация через поток общего объекта?

#c #multithreading #parallel-processing #thread-safety #openmp

#c #многопоточность #параллельная обработка #потокобезопасность #openmp

Вопрос:

У меня есть параллельный алгоритм C , который выглядит примерно так:

 void recursiveFunction(int p, std::unordered_set<int> localSet) {
    vector<someStruct> toCall;
    if (p > SOMEINT) {
        return;
    }
    #pragma omp parallel for schedule(dynamic, 1) shared(localSet, someGlobalVector, someOtherGlobalVector)
    for (int i = 0; i < someGlobalVector[p].size();   i) {
        auto someLocalValue = someGlobalVector[p][i];
        auto someOtherLocal = someOtherGlobalVector[p][i];
        std::unordered_set<int> filtered;
        for (auto elem : localSet) {
            if (someLocalValue.count(elem) == 0) {
                filtered.insert(elem);
            }
        }
        if (checkSomeCondition(p 1, filtered)) {
            #pragma omp critical
            {
                tocall.push_back({someOtherLocal, filtered});
            }
        }
    }
    for (auto [elem1, elem2] : toCall) {
        recursiveFunction(p 1, filtered);
    }
}
 

Безопасна ли итерация через общий unordered_set поток?

Ответ №1:

Безопасна ли итерация через общий поток unordered_set?

Нет, an unordered_set не является потокобезопасным по определению. Однако это не обязательно означает, что при использовании таких структур в коде возникает условие гонки; Например, если кто-то только читает структуру данных, даже одновременно, тогда он находится в безопасности. Это причина, по которой действительно неизменяемые структуры данных потокобезопасны по определению, потому что нельзя изменять эти структуры, отсюда и название.

С другой стороны, если unordered_set одновременно изменяется несколькими потоками, то возникает условие гонки. Тем не менее, можно по-прежнему записывать unordered_set в a потокобезопасным способом, если при выполнении записи обеспечивается взаимное исключение.

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

Чтобы избежать условий гонки, можно использовать конструктор OpenMP critical для синхронизации обращений к общей структуре во время insert операции:

 #pragma omp critical
{
  // the code to insure mutual exclusion.
}
 

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

Давайте немного разберем ваш код:

 void recursiveFunction(int p, std::unordered_set<int> localSet) {
    #pragma omp parallel for schedule(dynamic, 1) shared(localSet, someGlobalVector, someOtherGlobalVector)
    for (int i = 0; i < someGlobalVector[p].size();   i) {
        auto someLocalValue = someGlobalVector[p][i];
        auto someOtherLocal = someOtherGlobalVector[p][i];
        std::unordered_set<int> filtered
        for (auto elem : localSet) {
            if (someLocalValue.count(elem) == 0) {
                filtered.insert(elem)
            }
        }
    }
}
 

localSet , someGlobalVector , someOtherGlobalVector являются общими, но они доступны только для чтения. Следовательно, пока нет другого потока вне этого метода (работающего одновременно), изменяющего эти структуры, все в порядке. vector<someStruct> toCall Разделяется и изменяется, но модификация происходит внутри критического конструктора, поэтому там нет условия гонки.

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

 void recursiveFunction(int p, std::unordered_set<int> localSet, int total_threads) {
    vector<someStruct> thread_tocall[total_threads];
    vector<someStruct> toCall;
    if (p > SOMEINT) {
        return;
    }
    #pragma omp parallel shared(localSet, someGlobalVector, someOtherGlobalVector)
    {
        int thread_id = omp_get_thread_num();
        for (int i = 0; i < someGlobalVector[p].size();   i) {
             auto someLocalValue = someGlobalVector[p][i];
             auto someOtherLocal = someOtherGlobalVector[p][i];
             std::unordered_set<int> filtered;
             for (auto elem : localSet) {
                 if (someLocalValue.count(elem) == 0) {
                    filtered.insert(elem);
                 }
             }
           if (checkSomeCondition(p 1, filtered)) {
                thread_tocall[thread_id].push_back({someOtherLocal, filtered});
            }
         }
    }
    for(int i = 0; i < total_threads; i  )
       for (auto [elem1, elem2] : thread_tocall[i]) {
          recursiveFunction(p 1, filtered);
       }
}
 

Ответ №2:

Если глобальный объект предварительно заполнен и читается только тогда — тогда да, он обычно потокобезопасен. Но если общий объект может быть изменен, пока кто-то его читает, тогда чтение также становится небезопасным для потоков.