回调链中使用的对象的C ++内存管理模式

时间:2013-05-19 01:43:43

标签: memory-management c++11

我使用的几个代码库包括以下列模式手动调用newdelete的类:

class Worker {
 public:
  void DoWork(ArgT arg, std::function<void()> done) {
    new Worker(std::move(arg), std::move(done)).Start();
  }

 private:
  Worker(ArgT arg, std::function<void()> done)
      : arg_(std::move(arg)),
        done_(std::move(done)),
        latch_(2) {} // The error-prone Latch interface isn't the point of this question. :)

  void Start() {
    Async1(<args>, [=]() { this->Method1(); });
  }
  void Method1() {
    StartParallel(<args>, [=]() { this->latch_.count_down(); });
    StartParallel(<other_args>, [=]() { this->latch_.count_down(); });
    latch_.then([=]() { this->Finish(); });
  }
  void Finish() {
    done_();
    // Note manual memory management!
    delete this;
  }

  ArgT arg_
  std::function<void()> done_;
  Latch latch_;
};

现在,在现代C ++中,显式delete是一种代码气味,在某种程度上是delete this。但是,我认为这种模式(创建一个表示由回调链管理的大量工作的对象)从根本上说是一个好的,或者至少不是一个坏的想法。

所以我的问题是,我应该如何重写这种模式的实例以封装内存管理?

认为的一个选项是一个好主意是将Worker存储在shared_ptr中:从根本上说,所有权不在此处共享,因此开销很大引用计数是不必要的。此外,为了在回调中保留shared_ptr活着的副本,我需要继承enable_shared_from_this,并记得在之外调用 lambdas并捕获shared_ptr进入回调。如果我直接使用this编写简单代码,或者在回调lambda中调用shared_from_this(),则可以提前删除该对象。

2 个答案:

答案 0 :(得分:3)

我同意delete this是代码气味,并且在较小程度上delete。但我认为这是延续传递风格的自然部分,对我而言,它本身就是一种代码气味。

根本问题是这个API的设计假设无限制的控制流:它确认调用者对调用完成时发生的事情感兴趣,但是通过任意复杂的回调表示完成,而不是简单地从同步通话。最好同步构建它,让调用者确定适当的并行化和内存管理机制:

class Worker {
 public:
  void DoWork(ArgT arg) {
    // Async1 is a mistake; fix it later.  For now, synchronize explicitly.
    Latch async_done(1);
    Async1(<args>, [&]() { async_done.count_down(); });
    async_done.await();

    Latch parallel_done(2);
    RunParallel([&]() { DoStuff(<args>); parallel_done.count_down(); });
    RunParallel([&]() { DoStuff(<other_args>); parallel_done.count_down(); };
    parallel_done.await();
  }
};

在来电者方面,它可能看起来像这样:

Latch latch(tasks.size());
for (auto& task : tasks) {
  RunParallel([=]() { DoWork(<args>); latch.count_down(); });
}
latch.await();

其中RunParallel可以使用std :: thread或您喜欢的任何其他机制来调度并行事件。

这种方法的优点是对象生命周期更简单。 ArgT对象完全适用于DoWork调用的范围。 DoWork的参数与包含它们的闭包完全一样长。这也使得向DoWork调用添加返回值(例如错误代码)变得更加容易:调用者只需从锁存器切换到线程安全队列,并在完成时读取结果。

这种方法的缺点是它需要实际的线程,而不仅仅是boost :: asio :: io_service。 (例如,DoWork()中的RunParallel调用无法阻止等待来自调用方的RunParallel调用返回。)因此,您必须将代码构造为严格分层的线程池,或者您必须允许潜在无限数量的线程。

答案 1 :(得分:0)

一个选项是此处的delete this 代码气味。最多应该将它包装到一个小型库中,该库可以检测是否所有连续回调都被销毁而没有调用done_()