jeudi 19 septembre 2019

Does constructing a struct with default move constructor and wrap it in a shared_ptr is safe to pass to a lambda function executed in a tbb::task?

Context: On one of my C++11 application, object serialization and publish of message is time consuming. Therefore I want to do it in a separate thread using Intel TBB library (more specifically using a tbb::task_group)

Issue: the object to serialize is a struct where some of the properties are std::vector<std::unique_ptr<SomeObject>>, making it impossible to pass by copy to the lambda executed in a task

Approximately it look like

struct MockedDC {
  MockedDC(int x, std::vector<std::unique_ptr<SomeObject>> v) : x(x),
                                                                v(std::move(v)) {};

  int                                      x;
  std::vector<std::unique_ptr<SomeObject>> v;
};

The "solution" I found, is to reconstruct on the heap with the move-constructor my instance and wrap it in a shared_ptr<MockedDC> which is copyable. In the end the function which invoke the tbb::task_group::run look like

// function called like this `executeInThread(taskGroup, std::move(mockedDC))`

void  executeInThread(tbb::task_group& taskGroup, MockedDC mockedDC) {
  const std::shared_ptr<MockedDC> sharedMockedDC(new MockedDC(std::move(mockedDC)));
  auto f = [sharedMockedDC] {
    const auto serialized(serializer(*sharedMockedDC)); // pass by reference
    publish(serialized);
  };

  taskGroup.run(f);
};


it compile and run fine, but I can't put it under pressure as it will be in real life condition so my question is is it safe/sane to do this ?

I found on another stackoverflow question an alternative, but the implementation looks difficult to maintain given my C++ knowledge :) that's why I want to stick with the shared_ptr approach as suggested somewhere else

What I tried so far: I wrote a dummy code to test the thing, but I think its not enough to validate this approach. I also wanted to compile with some sanitization flags, but tbb fail to link with a bunch of errors like undefined reference to __ubsan_handle_pointer_overflow

Here is the dummy example if that help to answer (it compile and run without issues (except some int overflow but that not an issue I guess))

#include <cstdio>
#include <iostream>
#include <memory>
#include <vector>
#include <numeric>
#include "tbb/task_scheduler_init.h"
#include "tbb/task_group.h" 

struct MockedDC {
  MockedDC(int seed, size_t baseLen) : seed(seed), baseLen(baseLen) {
    this->a_MDC.reserve(baseLen);
    for (size_t i = 0; i < baseLen; ++i)
      this->a_MDC.emplace_back(new int((seed + i) / (seed + 1)));
    };

    int                               seed;
    size_t                            baseLen;
    std::vector<std::unique_ptr<int>> a_MDC;
  };

void  executeInThread(tbb::task_group& taskGroup, MockedDC mockedDC) {
  const std::shared_ptr<MockedDC> sharedMockedDC(new MockedDC(std::move(mockedDC)));
  auto f = [sharedMockedDC] {
    std::cout <<
      std::accumulate(sharedMockedDC->a_MDC.begin(), sharedMockedDC->a_MDC.end(), 0, [](int acc, const std::unique_ptr<int>& rhs) {
          return acc + *rhs;
      })
      << std::endl << std::flush;
  };
  taskGroup.run(f);
};


void  triggerTest(tbb::task_group& taskGroup) {
  for (size_t i = 0; i < 1000000; ++i) {
    MockedDC  mdc(i, 10000000);
    executeInThread(taskGroup, std::move(mdc));
  }
  return ;
};

int main() {
  tbb::task_scheduler_init  tbbInit(tbb::task_scheduler_init::automatic);
  //tbb::task_scheduler_init  tbbInit(8);
  tbb::task_group           taskGroup;

  triggerTest(taskGroup);
  taskGroup.wait();
  return (0);
};

PS: using C++14 new capture by move doesn't work because of TBB library :/

Aucun commentaire:

Enregistrer un commentaire