簡體   English   中英

使用可替換的階段在 C++ 中建模管道

[英]Modeling a pipeline in C++ with replaceable stages

我正在嘗試編造一個 C++ 數據結構來建模一個簡單的 N 階段過程,其中每個階段都可以用不同的函數替換。 一種方法是使用面向對象的方法,並為每個階段創建一個帶有虛方法的抽象基類; 例如:

class Pipeline {
protected:
  virtual void processA(const In& in, BType& B) = 0;
  virtual void processB(const BType& B, BType& C) = 0;
  virtual void processC(const CType& C, BType& D) = 0;
  virtual void processD(const DType& D, Out& out) = 0;
public:
  void process(const In& in, Out& out) {
    Btype B;
    processA(in, B);
    Ctype C;
    processB(B, C);
    Btype D;
    processC(C, D);
    processD(D,out);
  }
};

這種方法的問題是,如果 N 個階段中的每一個都可以與 M 個進程互換,那么您就有 N*M 個可能的子類。

另一個想法是存儲函數對象:

class Pipeline {
public:
  std::function<void(const In& in, BType& B)> processA;
  std::function<void(const In& B, CType& C)> processB;
  std::function<void(const In& C, DType& D)> processC;
  std::function<void(const In& D, Out& out)> processD;
  void process(const In& in, Out& out) {
    Btype B;
    processA(in, B);
    Ctype C;
    processB(B, C);
    Btype D;
    processC(C, D);
    processD(D,out);
  }
};

我用這種方法遇到的問題是階段並不是真正獨立的,在某些情況下,我希望一個對象來存儲有關多個階段的信息。

有沒有人為帶有可更換部件的管道找到好的數據結構? 獎金將能夠允許每個階段同時運行。

指向 std 函數對象的指針是個壞主意。 如果需要,它們已經可以存儲指針。

我建議圖表。

接收sink是消費者:

template<class...Ts>
struct sink : std::function<void(Ts...)> {
  using std::function<void(Ts...)>::function;
};

來源是接受消費者並滿足它的東西:

template<class...Ts>
using source = sink<sink<Ts...>>;

進程是將生產者與消費者聯系起來的東西,可能會改變類型:

template<class In, class Out>
using process = sink< source<In>, sink<Out> >;

然后我們可以定義一個管道操作:

template<class In, class Out>
sink<In> operator|( process< In, Out > a, sink< Out > b ){
  return [a,b]( In in ){
    a( [&in]( sink<In> s )mutable{ s(std::forward<In>(in)); }, b );
  };
}
template<class In, class Out>
source<Out> operator|( source< In > a, process< In, Out > b ){
  return [a,b]( sink<Out> out ){
    b( a, out );
  };
}

template<class In, class Mid, class Out>
process<In, Out> operator|( process<In, Mid> a, process<Mid, Out> b ){
  return [a,b]( source<In> in, sink<Out> out ){
    a( in, b|out ); // or b( in|a, out )
  };
}
template<class...Ts>
sink<> operator|( source<Ts...> a, sink<Ts...> b ){
  return[a,b]{ a(b); };
}

應該這樣做。

我假設組件管道元素的狀態復制起來很便宜,因此共享 ptr 或原始指針等。

如果您想要並發,只需啟動提供值隊列並通過管道傳遞期貨的進程。 但我認為通常最好將元素附加在一起並使管道異步,而不是階段。

將管道元素設為 gsl spans 之類的東西也很有用,它允許階段擁有固定的緩沖區並在不分配的情況下以塊的形式傳遞計算結果。

一個讓你開始的玩具過程:

process<char, char> to_upper = []( source<char> in, sink<char> out ){
  in( [&out]( char c ) { out( std::toupper(c) ); } );
};

和一個來源:

source<char> hello_world = [ptr="hello world"]( sink<char> s ){
  for (auto it = ptr; *it; ++it ){ s(*it); }
};
sink<char> print = [](char c){std::cout<<c;};

int main(){
  auto prog = hello_world|to_upper|print;
  prog();
}

輸出"HELLO WORLD"

現場演示: https : //ideone.com/MC4fDV

請注意,這是一個基於推送的管道。 基於拉動的管道是一種替代方法。 推送管道允許更輕松的作業批處理; pull 管道可以避免制作沒人想要的數據。 Push 使數據傳播自然; pull 使數據收集變得自然。

協程也可以使這更自然。 從某種意義上說,源是一個協程,當它在推送管道中調用接收器時會掛起。 並以相反的方式拉動。 協程可以使推/拉兩個工作 eitb 相同的處理代碼。

為了使您的第一種方法更具可互換性,您可以將抽象基類拆分為多個基類,每個進程一個。 然后基類可以由一個或多個對象實現。 管道將保存一個指向每個基類的引用、指針或智能指針:

struct ProcessA {
  virtual void processA(const In& in, BType& B) = 0;
  virtual ~ProcessA() = default;
};
struct ProcessB {
  virtual void processB(const BType& B, CType& C) = 0;
  virtual ~ProcessB() = default;
};
// ...

struct Pipeline {
  ProcessA* processA;
  ProcessB* processB;
  ProcessC* processC;
  ProcessD* processD;

  void process(const In& in, Out& out) {
    BType B;
    processA->processA(in, B);
    CType C;
    processB->processB(B, C);
    DType D;
    processC->processC(C, D);
    processD->processD(D,out);
  }
};

struct SimpleProcessor : ProcessA, ProcessB, ProcessC, ProcessD {
  void processA(const In& in, BType& B) override;
  void processB(const BType& B, CType& C) override;
  void processC(const CType& C, DType& D) override;
  void processD(const DType& D, Out& out) override;
};

int main() {
  SimpleProcessor processor;
  Pipeline pipeline;
  pipeline.processA = &processor;
  pipeline.processB = &processor; 
  pipeline.processC = &processor; 
  pipeline.processD = &processor; 
  In in;
  Out out;
  pipeline.process(in, out);
}

現場演示

你的第二種方法也可以。 您可以使用 lambda 之類的東西來調整單個對象以適合每個std::function

struct Pipeline {
  std::function<void(const In& in, BType& B)>   processA;
  std::function<void(const BType& B, CType& C)> processB;
  std::function<void(const CType& C, DType& D)> processC;
  std::function<void(const DType& D, Out& out)> processD;

  void process(const In& in, Out& out) {
    BType B;
    processA(in, B);
    CType C;
    processB(B, C);
    DType D;
    processC(C, D);
    processD(D,out);
  }
};

int main() {
  SimpleProcessor proc;
  Pipeline pipeline;
  pipeline.processA = [&proc](const In& in, BType& B){ return proc.processA(in, B); };
  pipeline.processB = [&proc](const BType& B, CType& C){ return proc.processB(B, C); }; 
  pipeline.processC = [&proc](const CType& C, DType& D){ return proc.processC(C, D); }; 
  pipeline.processD = [&proc](const DType& D, Out& out){ return proc.processD(D, out); }; 
  In in;
  Out out;
  pipeline.process(in, out);
}

現場演示

是的,這兩種方法可以讓你同時運行的每個進程,但你的BTypeCTypeDType必須支持的並發訪問,使他們能夠寫入和在同一時間讀取。 例如並發隊列。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM