Есть ли лучший способ сделать этот код потокобезопасным? Thread_local static кажется тупым инструментом - PullRequest
0 голосов
/ 10 января 2019

Следующий код имитирует большую программу, которая создает экземпляр симуляции и затем распараллеливает его, используя firstprivate, чтобы сделать мои экземпляры приватными. Однако сам экземпляр создает еще два экземпляра в своих методах.

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

Он прекрасно компилируется и при ручном тестировании оказывается поточно-ориентированным и работает.

Но я не уверен, что использую технологию C ++ оптимальным образом, так как подозреваю, что мог бы объявить экземпляры дальше в иерархии памяти и избавить себя от необходимости использовать переменную static, возможно, передавая экземпляр, созданный в parallel регион по ссылке на другой мой экземпляр или что-то в этом роде. Я подозреваю, что это лучше, потому что все в скобках #pragma omp parallel {} является локальным для потока.

Следовательно, моя цель состоит в том, чтобы создать два (или более) локальных для потока независимых экземпляра каждого класса и, в частности, GenNo, поскольку он моделирует генератор случайных чисел, который должен быть засеян один раз на поток, а затем просто вызываться с помощью то же самое семя, хотя здесь я изменяю то, что я называю «семенем» предсказуемым образом, просто чтобы понять поведение программы и выявить нарушения условий безопасности / гонки.

Код, который закомментирован, не работал, но вызвал «ошибку сегментации» при выходе из программы с SIGSEGV 11. Я считаю, что «уникальный» указатель не так уникален при параллельном развертывании. В целом, это решение кажется более элегантным, и я хотел бы, чтобы оно работало, но я рад выслушать ваши комментарии.

Чтобы получить функциональность std::unique_ptr, необходимо закомментировать строку, начинающуюся с thread_local static, а остальные комментарии удалить.

#include <iostream>
#include <omp.h>
//#include <memory>

class GenNo
{
public:

    int num;

    explicit GenNo(int num_)
    {
        num = num_;
    };

    void create(int incr)
    {
        num += incr;
    }
};

class HelpCrunch{
public:
    HelpCrunch() {

    }

    void helper(int number)
    {
        std::cout << "Seed is " << number << " for thread number: " << omp_get_thread_num() << std::endl;
    }
};

class calculate : public HelpCrunch
{
public:

    int specific_seed;
    bool first_run;

    void CrunchManyNos()
    {
        HelpCrunch solver;

        thread_local static GenNo RanNo(specific_seed);
        //std::unique_ptr<GenNo> GenNo_ptr(nullptr);
        /*
        if(first_run == true)
        {
            GenNo_ptr.reset(new GenNo(specific_seed));
            first_run = false;
        }
         solver.helper(GenNo_ptr->num);
*/
        RanNo.create(1);
        solver.helper(RanNo.num);



        //do actual things that I hope are useful.
    };
};




int main()
{

    calculate MyLargeProb;
    MyLargeProb.first_run = true;

#pragma omp parallel firstprivate(MyLargeProb)
    {
        int thread_specific_seed = omp_get_thread_num();
        MyLargeProb.specific_seed = thread_specific_seed;

        #pragma omp for
        for(int i = 0; i < 10; i++)
        {
            MyLargeProb.CrunchManyNos();
            std::cout << "Current iteration is " << i << std::endl;
        }

    }
    return 0;
}

Теперь вывод с ключевым словом thread_local static был:

Seed is 2 for thread number: 1
Current iteration is 5
Seed is 3 for thread number: 1
Current iteration is 6
Seed is 4 for thread number: 1
Current iteration is 7
Seed is 5 for thread number: 1
Current iteration is 8
Seed is 6 for thread number: 1
Current iteration is 9


Seed is 1 for thread number: 0
Current iteration is 0
Seed is 2 for thread number: 0
Current iteration is 1
Seed is 3 for thread number: 0
Current iteration is 2
Seed is 4 for thread number: 0
Current iteration is 3
Seed is 5 for thread number: 0
Current iteration is 4

Пока без использования thread_local, но с сохранением static я получаю:

Seed is 2 for thread number: 1
Current iteration is 5
Seed is 3 for thread number: 1
Current iteration is 6
Seed is 4 for thread number: 1
Current iteration is 7
Seed is 6 for thread number: 1
Current iteration is 8
Seed is 7 for thread number: 1
Current iteration is 9


Seed is 5 for thread number: 0
Current iteration is 0
Seed is 8 for thread number: 0
Current iteration is 1
Seed is 9 for thread number: 0
Current iteration is 2
Seed is 10 for thread number: 0
Current iteration is 3
Seed is 11 for thread number: 0
Current iteration is 4

Если я вообще пропущу ключевое слово static, экземпляр просто будет переназначаться, и, хотя я сильно подозреваю, что он останется закрытым для потока, он малопригоден, так как счетчик застрянет на 1 или 2 для резьбы 0 и 1 на двухъядерной машине. (Реальное приложение должно быть в состоянии «подсчитывать» и не подвергаться воздействию параллельного потока.)

Что мне нужно помочь с

Теперь я смоделировал пример так, чтобы нарушение безопасности потока стало очевидным из-за того, что счетчики мешали друг другу, как мы можем видеть, это тот случай, когда thread_local опущен, а static оставлен в (ни то, ни другое глупо). ). Класс HelpCrunch на самом деле намного сложнее и, скорее всего, ориентирован на многопотоковое исполнение и прекрасно подходит для повторной инициализации при каждом повторении цикла. (На самом деле это лучше, потому что он выбирает кучу переменных из своего дочернего элемента, который является частным экземпляром.) Но как вы думаете, было бы лучше добавить thread_local к созданию solver, без ключевого слова static ? Или я должен объявить экземпляр в другом месте, и в этом случае мне понадобится помощь с передачей по указателю / ссылке и т. Д.

1 Ответ

0 голосов
/ 10 января 2019

Во-первых, ваш пример использует глобальный объект std::cout небезопасным способом, обращаясь к нему одновременно из нескольких потоков. Мне пришлось добавить #pragma omp critical в некоторых местах, чтобы получить читабельный вывод.

Во-вторых, закомментированный код дает сбой, потому что GenNo_ptr имеет автоматическую продолжительность, поэтому он уничтожается каждый раз, когда CrunchManyNos() завершает выполнение. Поэтому, когда first_run равно false, вы разыменовываете указатель nullptr.

Когда дело касается вашего конкретного вопроса, существует огромная разница между RanNo static или static thread_local:

  • Если это static, то будет один экземпляр RanNo инициализируется при первом выполнении CrunchManyNos(). Это к в некоторой степени глобальная переменная, которая будет небезопасно использоваться в Параллельный контекст вашего примера.

  • Если это static thread_local (кстати, используя openmp, вы должны предпочесть threadprivate), это будет быть создан в первый раз, новый поток вызывает CrunchManyNos() и будет длиться в течение всего потока.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...