【问题标题】:Advantages/Disadvantages of Many Nested Callbacks?许多嵌套回调的优点/缺点?
【发布时间】:2017-10-30 09:24:17
【问题描述】:

我有一个工作分配方案,每个单位都进行一些簿记和管理,并将任务按顺序传递给下一个,例如;假设有 3 个类:老板、经理、工人

class Boss
{
    void do_async(Request req, std::function<void(Result)> callback)
    {
        //Find eligible manager etc.
        manager.do_async(Boss_request(req,...),std::bind(&Boss::callback,this,callback,std::placeholders::_1));

    }
    void callback(std::function<void(Result)> main_callback,Boss_result res)
    {
        //some book keeping
        main_callback(res.main_part);
    }
};

class Manager
{
    void do_async(Boss_request req, std::function<void(Boss_result)> boss_callback)
    {
        //Find eligible worker etc. add some data to request
        worker.do_async(Manager_request(req,...),std::bind(&Manager::callback,this,boss_callback,std::placeholders::_1));
    }

    void callback(std::function<void(Boss_result)> boss_callback,Manager_result res)
    {
        //some book keeping
        boss_callback(res.boss_part);
    }
};

class Worker
{
    void do_async(Manager_request req, std::function<void(Manager_result)> manager_callback)
    {
        //Do job async
        work_async(Worker_task(req,...),std::bind(&Worker::callback,this,manager_callback,std::placeholders::_1));
    }

    void callback(std::function<void(Manager_result)> manager_callback,Worker_result res)
    {
        //some book keeping
        manager_callback(res.manager_part);
    }
};

如您所见,我广泛使用 std::bind、std::function 和 std::placeholder。这种方法有什么优点/缺点吗?如果不是最好的,那么更好的方法是什么?在这种状态下使用lambda 函数是否可能/更可取(作为性能或代码质量)?

编辑:为什么我需要在每个级别进行异步访问,而不仅仅是第一级?因为每个类之间存在多对多的关系。我有几层处理单元(老板或经理或工人),它们可以订购下一层的任何人。当一个单位将工作安排给下一个排队的人时。从上面接受新订单必须立即免费。

我没有直接使用lambda,因为回调可能有点大,可能会使其难以阅读。但是,如果存在任何显着的性能损失,则可能会牺牲代码质量。

【问题讨论】:

  • 你应该看看 promise.future 方案,这可能会阻止你编写所有这些样板。
  • 没有理由使用bind。改用 lambda:stackoverflow.com/questions/17363003/…
  • 你似乎在做延续传递风格,但把延续放在不一致的地方。

标签: c++ stl c++14 c++17


【解决方案1】:

您在此处所做的是传递数据。拥抱管道。

namespace chain {
  template<class T, class Base=std::function<void(T)>>
  struct sink:Base{
    using Base::operator();
    using Base::Base;
  };

  template<class T, class F>
  sink<T> make_sink( F&& f ) {
    return {std::forward<F>(f)};
  }

  template<class T>
  using source=sink<sink<T>>;

  template<class T, class F>
  source<T> make_source( F&& f ) {
    return {std::forward<F>(f)};
  }
  template<class T>
  source<std::decay_t<T>> simple_source( T&& t ) {
    return [t=std::forward<T>(t)]( auto&& sink ) {
      return sink( t );
    };
  }
  template<class In, class Out>
  using pipe = std::function< void(source<In>, sink<Out>) >;

  template<class In, class Out>
  sink<In> operator|( pipe<In, Out> p, sink<Out> s ) {
    return [p,s]( In in ) {
      p( [&]( auto&& sink ){ sink(std::forward<In>(in)); }, s );
    };
  }
  template<class In, class Out>
  source<Out> operator|( source<In> s, pipe<Out> p ) {
    return [s,p]( auto&& sink ) {
      p( s, decltype(sink)(sink) );
    };
  }
  template<class T>
  std::function<void()> operator|( source<T> in, sink<T> out ) {
    return [in, out]{ in(out); };
  }
  template<class In, class Mid, class Out>
  pipe<In, Out> operator|( pipe<In, Mid> a, pipe<Mid, Out> b ) {
    return [a,b]( source<In> src, sink<Out> dest ) {
      b( src|a, dest  );
      // or a( src, b|dest );
      // but I find pipe|sink -> sink to be less pleasing an implementation
    };
  }
}//namespace

然后写下这些:

pipe<Request, Result> Boss::work_pipe();

pipe<Boss_request, Boss_result> Manager::work_pipe();
pipe<Boss_request, Manager_request> Manager::process_request();
pipe<Manager_request, Manager_result> Manager::do_request();
pipe<Manager_result, Boss_results> Manager::format_result();

pipe<Manager_request, Manager_result> Worker::work_pipe();

Worker 和 Boss 类似。

pipe<Request, Result> Boss::work_pipe() {
  return process_request() | do_request() | format_result();
}
pipe<Boss_request, Boss_result> Manager::work_pipe() {
  return process_request() | do_request() | format_result();
}
pipe<Manager_request, Manager_result> Worker::work_pipe() {
  return process_request() | do_request() | format_result();
}

然后:

pipe<Manager_request, Manager_result> Manager::do_request() {
  return [this]( source<Manager_request> src, sink<Manager_result> dest ) {
    // find worker
    worker.do_request( src, dest );
  };
}
pipe<Manager_output, Boss_result> Manager::format_result() {
  return [this]( source<Manager_output> src, sink<Boss_result> dest ) {
    src([&]( Manager_output from_worker ) {
      // some book keeping
      dest( from_worker.boss_part );
    });
  };
}

现在,我将源设为“汇为汇”,因为它允许源(或管道)通过一次调用生成 1、0 或多条消息。我发现这在很多情况下很有用,但它确实让编写管道有点陌生。

您也可以在 中编写此代码,而根本不使用std::function,只需将“我是一个接收器”、“我是一个源”和“我是一个管道”标签应用于 lambdas(通过组合,比如override) 然后盲目地将东西与| 挂钩并希望它们的类型兼容。

do_sync,您只需这样做:

void Boss::do_async( Request req, sink<Result> r ) {
  work_async( simple_source(req) | work_pipe() | r );
}

即,整个计算可以捆绑在一起并移动。这会将线程工作移到顶部。

如果您需要将异步线程实现置于底层,您可以将之前的工作通过管道向上传递。

void Boss::do_async( source<Request> req, sink<Result> r ) {
  find_manager().do_async( req|process_request(), format_result()|r );
}
void Manager::do_async( source<Boss_request> req, sink<Boss_result> r ) {
  find_worker().do_async( req|process_request(), format_result()|r );
}
void Worker::do_async( source<Manager_request> req, sink<Manager_result> r ) {
  work_async( req|process_request()|do_request()|format_result()|r );
}

由于 sink/source/pipes 的组成方式,您可以选择传递组合的哪些部分以及传递哪些部分。

std::function-less 版本:

namespace chain {
  struct pipe_tag{};
  struct sink_tag{};
  struct source_tag{};

  template<class T, class=void>
  struct is_source:std::is_base_of<source_tag, T>{};
  template<class T, class=void>
  struct is_sink:std::is_base_of<sink_tag, T>{};
  template<class T, class=void>
  struct is_pipe:std::is_base_of<pipe_tag, T>{};

  template<class F, class Tag>
  struct tagged_func_t: F, Tag {
    using F::operator();
    using F::F;
    tagged_func_t(F&& f):F(std::move(f)) {}
  };
  template<class R, class...Args, class Tag>
  struct tagged_func_t<R(*)(Args...), Tag>: Tag {
    using fptr = R(*)(Args...);
    fptr f;
    R operator()(Args...args)const{
      return f( std::forward<Args>(args)... );
    }
    tagged_func_t(fptr fin):f(fin) {}
  };

  template<class Tag, class F>
  tagged_func_t< std::decay_t<F>, Tag >
  tag_func( F&& f ) { return {std::forward<F>(f)}; }

  template<class F>
  auto as_pipe( F&& f ) { return tag_func<pipe_tag>(std::forward<F>(f)); }
  template<class F>
  auto as_sink( F&& f ) { return tag_func<sink_tag>(std::forward<F>(f)); }
  template<class F>
  auto as_source( F&& f ) { return tag_func<source_tag>(std::forward<F>(f)); }

  template<class T>
  auto simple_source( T&& t ) {
    return as_source([t=std::forward<T>(t)]( auto&& sink ) {
      return sink( t );
    });
  }

  template<class Pipe, class Sink,
    std::enable_if_t< is_pipe<Pipe>{} && is_sink<Sink>{}, bool> = true
  >
  auto operator|( Pipe p, Sink s ) {
    return as_sink([p,s]( auto&& in ) {
      p( [&]( auto&& sink ){ sink(decltype(in)(in)); }, s );
    });
  }
  template<class Source, class Pipe,
    std::enable_if_t< is_pipe<Pipe>{} && is_source<Source>{}, bool> = true
  >
  auto operator|( Source s, Pipe p ) {
    return as_source([s,p]( auto&& sink ) {
      p( s, decltype(sink)(sink) );
    });
  }
  template<class Source, class Sink,
    std::enable_if_t< is_sink<Sink>{} && is_source<Source>{}, bool> = true
  >
  auto operator|( Source in, Sink out ) {
    return [in, out]{ in(out); };
  }
  template<class PipeA, class PipeB,
    std::enable_if_t< is_pipe<PipeA>{} && is_pipe<PipeB>{}, bool> = true
  >
  auto operator|( PipeA a, PipeB b ) {
    return as_pipe([a,b]( auto&& src, auto&& dest ) {
      b( src|a, dest  );
      // or a( src, b|dest );
      // but I find pipe|sink -> sink to be less pleasing an implementation
    });
  }

  template<class T>
  using sink_t = tagged_func_t< std::function<void(T)>, sink_tag >;
  template<class T>
  using source_t = tagged_func_t< std::function<void(sink_t<T>)>, source_tag >;
  template<class In, class Out>
  using pipe_t = tagged_func_t< std::function<void(source_t<In>, sink_t<Out>)>, pipe_tag >;
}

它减少了类型检查,但消除了类型擦除开销。

sink_tsource_tpipe_t 类型定义在您需要对它们进行类型擦除时很有用。

"Hello world" example 使用非类型擦除版本。

【讨论】:

  • @ifyalciner 谢谢。当我对必须为字符串替换和替换编写处理层而烦恼时写了它,并试图弄清楚如何在仍然允许 1 到 {0, infinity} 替换的同时进行内存分配。源是汇的汇(而不是返回 T 的东西)的想法导致了一些简洁的属性。我还在纠结如何让多路复用工作,并且不确定这个模型是否适合它。
猜你喜欢
  • 2010-09-18
  • 1970-01-01
  • 1970-01-01
  • 2014-05-19
  • 2017-05-08
  • 1970-01-01
  • 1970-01-01
  • 2012-01-18
  • 2011-04-05
相关资源
最近更新 更多