Использование интеллектуальных указателей в многопоточном приложении

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

class A
{
public:
    double step1() { return 2.5; }
};

class B
{
public:
    double step2() { return 1.2; }
};

class Result
{
public:
    Result(std::shared_ptr<A> _a, std::shared_ptr<B> _b) : a(_a), b(_b) {};

    double getResult() { return a->step1() + b->step2(); }

private:
    std::shared_ptr<A> a;
    std::shared_ptr<B> b;
};

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

Теперь окончательный расчет в getResult() также требует полиморфного поведения, поэтому я создаю (уникальный) указатель на Result, создаю лямбду, вызывающую getResult(), и передаю эту лямбду в свои потоки следующим образом:

void run_multi_threaded_calculation()
{
    auto result = create_result_unique_ptr();

    const int nThreads = 4;
    std::vector<double> save(nThreads);

    auto task = [&](int n) {
        // Preprocessing before getResult()
        save[n] = n * result->getResult();
    };

    std::vector<std::thread> threads;
    threads.reserve(nThreads);
    for (size_t i = 0; i < nThreads; ++i)
    {
        threads.push_back(std::thread(task, i));
    }

    for (auto& th : threads)
        th.join();

    for (const auto& s : save)
        std::cout << s << '\n';
}

Вопрос 1. Использую ли я правильную конфигурацию интеллектуальных указателей и лямбда-захвата, например unique_ptr к Result и shared_ptr к A и B? После некоторых предположений и проверки изменения типов интеллектуальных указателей вышеуказанное компилируется (но не компилируется, если a и b в Result являются unique_ptr), но я не уверен, что это лучший способ подойти к этому.

Вопрос 2: Если я заменю лямбду эквивалентным (или так я думал) функциональным объектом, мой код не скомпилируется (ошибка C2661: 'std::tuple‹ResultFunctor,unsigned int›:: tuple': ни одна перегруженная функция не принимает 2 аргумента). Есть ли что-то, чего мне не хватает в интеллектуальных указателях, или, может быть, в том, как работают потоки, или, возможно, какая-то проблема с определением моего функционального объекта?

Вот соответствующие изменения:

class ResultFunctor
{
public:
    ResultFunctor(std::unique_ptr<Result> _result, std::vector<double>& _save) : result(std::move(_result)), save(_save) {};

    void operator() (int n) { save[n] = n * result->getResult(); }

private:
    std::unique_ptr<Result> result;
    std::vector<double>& save;
};

и замените следующую строку:

void run_multi_threaded_calculation()
{
    // Other stuff is unchaged...

    /*auto task = [&](int n) {
        // Preprocessing before getResult()
        save[n] = n * result->getResult();
    };*/

    auto task = ResultFunctor(std::move(result), save);

    // other stuff is unchanged...
}

person Charlie H.    schedule 20.07.2020    source источник
comment
Почему result должен быть указателем?   -  person Paul Sanders    schedule 20.07.2020
comment
Это просто самый простой пример, который демонстрирует мой вопрос. Моя реальная программа будет Result вести себя полиморфно, производная от чего-то вроде IResult базового класса. Затем getResult будет использовать шаги 1 и 2, но с некоторыми другими вычислениями.   -  person Charlie H.    schedule 20.07.2020
comment
Это может быть хорошим вариантом использования сопрограмм, особенно если важен порядок шагов. В вашем примере я не думаю, что вы можете быть уверены, что шаг 1 будет оцениваться до шага 2.   -  person ttemple    schedule 21.07.2020
comment
Я рассмотрю сопрограммы, но ваш комментарий заставил меня понять, что мой пример немного вводит в заблуждение. На самом деле шаги не будут оцениваться в одной строке, а скорее: step1() /*... Do some stuff with the result...*/ step2().   -  person Charlie H.    schedule 22.07.2020


Ответы (1)


Часть проблемы, с которой вы сталкиваетесь, заключается в том, что вы передаете unique_ptr. Но вы пропустили один move для того, который находится в вашем классе ResultFunctor при попытке передать его в std::thread здесь:

threads.push_back(std::thread(task, i));

Если вам нужно использовать unique_ptr, вам, скорее всего, понадобится перемещение c'tor в ResultFunctor:

class ResultFunctor
{
public:
    ResultFunctor(std::unique_ptr<Result> _result, std::vector<double>& _save) : result(std::move(_result)), save(_save) {};

    void operator() (int n) { save[n] = n * result->getResult(); }

    // Move c'tor rough un-tested example
    ResultFunctor(ResultFunctor&& rf) :
        result(std::move(rf.result)),
        save(rf.save)
    {};

private:
    std::unique_ptr<Result> result;
    std::vector<double>& save;
};

Чтобы вы могли затем переместить его в c'tor std::thread, который принимает ссылку на r-значение

threads.push_back(std::thread(std::move(task), i));

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

Примечание: ваша лямбда сильно отличается от вашего эквивалентного функторного класса. Ваша лямбда захватывается по ссылке. Однако на самом деле это не так безопасно, поскольку, когда вы передаете это потоку, НАСТОЯЩИЙ unique_ptr может выйти за рамки и уничтожить его!

Так что это не очень хорошее решение. Я считаю, что в С++ 14 вы можете захватить unique_ptr с помощью перемещения: выражение">Как преобразовать unique_ptr в лямбда-выражение? и https://isocpp.org/wiki/faq/cpp14-language#lambda-captures

person code_fodder    schedule 20.07.2020
comment
Это помогло ответить на мой вопрос. Я внес предложенные вами изменения, и все скомпилировалось нормально, однако я получил исключение (нарушение доступа для чтения из-за nullptr), я думаю, потому что unique_ptr<Result> был удален одним из потоков до того, как другие могли завершиться, поэтому я думаю, что это должно быть shared_ptr. Однако в итоге я использовал функтор с необработанным указателем на результат, поскольку владение Result управлялось функцией, вызывающей функтор, а не самим функтором. - person Charlie H.; 21.07.2020
comment
@ЧарлиХ. Хорошее место, да, я думаю, превратив общий доступ в уникальный, вы потеряли совместность. Поэтому, когда счетчик ссылок общего указателя становится равным нулю, он будет уничтожен: o - person code_fodder; 21.07.2020