本文是对cnstream/samples/example/example.cpp代码的学习笔记

git代码 :https://github.com/Cambricon/CNStream

寒武纪开发者论坛: http://forum.cambricon.com/list-47-1.html

1、创建pipeline

MyPipeline pipeline("pipeline");

实例化一个MyPipeline类pipeline,调用了自身的基类Pipeline的构造函数,注意这里新建了一个IdxManager类。

Pipeline::Pipeline(const std::string& name) : name_(name) {
  // stream message handle thread
  exit_msg_loop_ = false;
  smsg_thread_ = std::thread(&Pipeline::StreamMsgHandleFunc, this);   // 创建了一个线程,第一个是成员函数地址,第二个是具体对象的地址。该函数功能就是监听流信息

  event_bus_ = new (std::nothrow) EventBus();
  LOG_IF(FATAL, nullptr == event_bus_) << "Pipeline::Pipeline() failed to alloc EventBus";
  GetEventBus()->AddBusWatch(std::bind(&Pipeline::DefaultBusWatch, this, std::placeholders::_1));   // 绑定成员函数,第一个是成员函数的地址,第二个是对象的地址。把绑定好的函数push到bus_watchers列表中

  idxManager_ = new (std::nothrow) IdxManager();
  LOG_IF(FATAL, nullptr == idxManager_) << "Pipeline::Pipeline() failed to alloc IdxManager";
}

下面展开IdxManager类,它是一个在pipeline里面创建、管理、销毁Moudle和stream的idx的类,也就是说它能保证idx的唯一性:

class IdxManager {
 public:
  IdxManager() = default;
  IdxManager(const IdxManager&) = delete;
  IdxManager& operator=(const IdxManager&) = delete;
  uint32_t GetStreamIndex(const std::string& stream_id);  // 本质的实现是从0到MaxStreamNumber查找,如果没有在stream_bitset里面的就可以分配为index。
  void ReturnStreamIndex(const std::string& stream_id);   // 这里返回的查找对应stream_bitset和stream_idx_map会将查找的复原,这个位置又空出来
  size_t GetModuleIdx();                                  // module的index使用的是掩码的方法
  void ReturnModuleIdx(size_t id_);                       // module_id_mask_ &= ~(1 << id_);把当前为变0
private:
  SpinLock id_lock;
  std::unordered_map<std::string, uint32_t> stream_idx_map;
  std::bitset<MAX_STREAM_NUM> stream_bitset;             // 所有分配stream的index都在这里
  uint64_t module_id_mask_ = 0;                // 所有分配的module的index都在这,根据这个方法,module最大支持64个
};  // class IdxManager

module的index方法:

mask=0000  位移=0001,那么可以分配第一个idx为0。mask=0001

mask=0001  位移=0001,跳过;mask=0001  位移=0010,可以分配第二个idx为1。mask=0011

  for (size_t i = 0; i < GetMaxModuleNumber(); i++) {
    if (!(module_id_mask_ & ((uint64_t)1 << i))) {
      module_id_mask_ |= (uint64_t)1 << i;
      return i;
    }

最后调用了SetStreamMsgObserver(),将pipeline指针赋值给Pipeline类smsg_observer。

pipeline.BuildPipeline({module_a_config, module_b_config, module_c_config, module_d_config});

在调用pipeline的创建管道函数,该函数来自Pipeline类的BuildPipeline()。这里的参数config为一个结构体CNModuleConfig:主要包括模块的名称、一对模块名字—模块的键值对、模块并行性、输入数据的最大size、模块的类名、下游模块名称等

1.1 BuildPipeline()函数:pipeline创建模块并且添加

for (auto& v : configs) {
    this->AddModuleConfig(v);
    std::shared_ptr<Module> instance(creator.Create(v.className, v.name));  // 创建了一个模型className是模型属于哪个类的如人、汽车等等,name是单纯的模型名称如张三、李四、奔驰、凯迪拉克等等
    if (!instance) {
      LOG(ERROR) << "Failed to create module by className(" << v.className << ") and name(" << v.name << ")";
      return -1;
    }
    instance->ShowPerfInfo(v.showPerfInfo);
    this->AddModule(instance);
    this->SetModuleAttribute(instance, v.parallelism, v.maxInputQueueSize);
  }

首先调用Pipeline类的AddModuleConfig()函数,遍历所有模块,把当前模块name、config以键值对形式添加到unordered_map的modules_config_中,把当前模块name、下游模块名称以键值对的形式添加到connections_config_中。

接着调用ModuleCreatorWorker类的Create()函数,创建了一个模块并且返回一个Module指针。命名为instance,ShowPerfInfo()函数只是设置是否显示信息。

然后调用Pipeline类的AddModule()函数,把创建的模块instance添加进去。本质是把模块名字和模块指针以键值对形式添加到Pipeline类的modules_map_中。里面调用SetContainer()函数使得模块本身有指向pipeline的指针(把container_指针指向pipeline),并且获取module的idx

最后调用Pipeline类的SetModuleAttribute()函数,主要是设置Pipeline类的modules_成员,他是 std::unordered_map<std::string, ModuleAssociatedInfo> 其中值是结构体ModuleAssociatedInfo。ModuleAssociatedInfo中的connector是指向一个Cnnector类指针。

展开Cnnector类:

class Connector {
 public:
  explicit Connector(const size_t conveyor_count, size_t conveyor_capacity = 20);
  ~Connector();

  const size_t GetConveyorCount() const;     // 返回conveyor的个数
  Conveyor* GetConveyor(int conveyor_idx) const;   // 在conveyor数组中返回一个
  size_t GetConveyorCapacity() const;        // 返回每个conveyor的buffer queue的大小

  CNFrameInfoPtr PopDataBufferFromConveyor(int conveyor_idx);   // 对特定的conveyor中弹出数据
  void PushDataBufferToConveyor(int conveyor_idx, CNFrameInfoPtr data); // 对特定的conveyor中push数据

  void Start();
  void Stop();
  bool IsStopped();
  void EmptyDataQueue();

 private:
  DISABLE_COPY_AND_ASSIGN(Connector);
  Conveyor* GetConveyorByIdx(int idx) const;

  std::vector<Conveyor*> conveyors_;       // 这里保存了 conveyor_count个Converyor,每个大小 conveyor_capacity
  size_t conveyor_capacity_ = 20;          
  std::atomic<bool> stop_{false};
};  // class Connector

上面Conveyor类的作用是用于两个模型之间传递数据,conveyor有一个buffer queue用于传递数据,上游module可以push数据,下游module可以pop数据。而Conveyor是属于Connector的。

1.2 BuildPipeline()函数:链接模块的上下游

  for (auto& v : connections_config_) {
    for (auto& name : v.second) {
      if (modules_map_.find(v.first) == modules_map_.end() ||
          modules_map_.find(name) == modules_map_.end() ||
          this->LinkModules(modules_map_[v.first], modules_map_[name]).empty()) {
        LOG(ERROR) << "Link [" << v.first << "] with [" << name << "] failed.";
        return -1;
      }
      linked_id_mask |= (uint64_t)1 << modules_map_[name]->GetId();
    }
  }

这一部分是遍历connections_config这个表,每一行的值可能包含多个模块,所以用两个for循环。其中调用了Pipeline类的LinkModules()函数,就是把上游名字和下游名字链接成一个string,作为link_id,  up_name-->down_name。并且把link_id和down_node_info的connector指针作为一个键值对保存在Pipeline类的成员变量links_中。同时设置了调用down_node->SetParentId(up_node->GetId())更新了下游模块中parent_ids_mask_的值。

  if (!down_node_info.connector) {
    LOG(ERROR) << "connector is invalid when linking " << link_id;
    return "";
  }

这里在链接模块的时候必须注意up_module和down_module传输数据都是通过down_module的connector的。如果后面一个模块的buffer为空就发生错误。

 linked_id_mask |= (uint64_t)1 << modules_map_[name]->GetId();

这里将后面一个模块的idx全部记录在 linked_id_mask中,方法为前面内容。

1.3 BuildPipeline()函数:打印没有上游模块的内容

  for (auto& v : configs) {
    if (v.className != "cnstream::DataSource" && v.className != "cnstream::TestDataSource" &&
        v.className != "cnstream::ModuleIPC" &&
        !(((uint64_t)1 << modules_map_[v.name]->GetId()) & linked_id_mask)) {
      LOG(ERROR) << v.name << " not linked to any module.";
      return -1;
    }
  }

这一部分是打印所有模块中没有链接到其他模块的那些。

1.4 .调用GetModule()函数:最后模块设置观察者

  cnstream::Module *sink = pipeline.GetModule(module_d_config.name);
  Observer observer;
  sink->SetObserver(&observer);

这里是遍历Pipeline类的modules_map_这个表,如果找到模型就返回模型的指针,没有就返回nullptr。

同时给module_d这个最后模块设置一个观察者observer用来打印数据的信息。

2 开始pipeline

  if (!pipeline.Start()) {
    LOG(ERROR) << "Pipeline start failed.";
    return EXIT_FAILURE;
  }

调用Pipeline类的Start()函数

2.1 Start()函数先调用SetEOSMask()函数

void Pipeline::SetEOSMask() {
  for (const std::pair<std::string, std::shared_ptr<Module>> module_info : modules_map_) {
    auto instance = module_info.second;
    eos_mask_ |= (uint64_t)1 << instance->GetId();
  }
}

调用它的目的是设置Pipeline中的一个变量eos_mask,记录了所有module的idx。方法是1里面的。(Q:eos_mask和Pipeline->IdxManager->module_id_mask区别?这个函数只在Pipeline启动时调用,调用这个函数之前所有module的idx是否已经分配?,如果已经分配了可以直接使用module_id_mask的值。)

2.2 打开模型

调用modules_map_中的模型的Open()函数,传入当前模型的ModuleParamSet参数(parameters["decoder_type"] = "mlu"; parameters["outptu_type"] = "mlu"; parameters["device_id"] = "0";),然后放入opened_modules数组中。

  if (perf_running_) {
    RwLockReadGuard lg(perf_managers_lock_);
    for (auto it : perf_managers_) {
      it.second->SqlBeginTrans();
    }
    perf_commit_thread_ = std::thread(&Pipeline::PerfSqlCommitLoop, this);
    calculate_perf_thread_ = std::thread(&Pipeline::CalculatePerfStats, this);
    perf_del_data_thread_ = std::thread(&Pipeline::PerfDeleteDataLoop, this);
  }

 

 又生成了三个线程。

2.3 模型开始传输数据

遍历modules_,调用每个模块的modules_ -> second -> connector -> Start()函数,再检查每个模块的参数。

for (auto& it : modules_) {
    const std::string node_name = it.first;
    ModuleAssociatedInfo& module_info = it.second;
    uint32_t parallelism = module_info.parallelism;
    if ((!parallelism && module_info.input_connectors.size()) ||
        (parallelism && !module_info.input_connectors.size())) {
      LOG(ERROR) << "The parallelism of the first module should be 0, and the parallelism of other modules should be "
                    "larger than 0. "
                 << "Please check the config of " << node_name << " module.";
      return false;
    }
    if ((!parallelism && module_info.connector) || (parallelism && !module_info.connector) ||
        (parallelism && module_info.connector && parallelism != module_info.connector->GetConveyorCount())) {
      LOG(ERROR) << "Module parallelism do not equal input Connector's Conveyor number, in module " << node_name;
      return false;
    }
    for (uint32_t conveyor_idx = 0; conveyor_idx < parallelism; ++conveyor_idx) {
      threads_.push_back(std::thread(&Pipeline::TaskLoop, this, node_name, conveyor_idx));  // TaskLoop函数需要两个参数,穿this指针是因为函数是一个类的成员函数。
    }
  }

 上述代码主最要的作用是创建了一系列线程,更新了pipeline里面的成员threads_,它是一个线程数组vector<thread>。遍历每一个的module,如果parallelism(1.1connector类中查看)个数不为零,就生成parallelism个线程。同时每个线程里面跑的是Pipeline::TaskLoop()这个函数。下面展开这个函数。

2.4 Pipeline::TaskLoop()函数:向缓存区中不停获取数据

为线程设置一个唯一的名字,名字格式"cn-" + "模块名称" + "该模块的第几个conveyor缓存区"

  size_t len = node_name.size() > 10 ? 10 : node_name.size();
  std::string thread_name = "cn-" + node_name.substr(0, len) + "-" + NumToFormatStr(conveyor_idx, 2);  
  SetThreadName(thread_name, pthread_self());

往下我们看到pipeline实现多线程数据传输的方法:

首先经过前面的步骤我们生成了 ModuleA × conveyor_count  + ... + ModuleD × conveyor_count 这么多个线程。每个线程执行循环,不停访问自己的conveyor队列,确保上游是否传输数据过来。

|  ModuleA  |  |  ModuleA  |  |  ModuleA  |  ...  |  ModuleD  |  |  ModuleD  |  
|  convery0 |  |  convery1 |  |  convery2 |  ...  |  convery0 |  |  convery1 | 
|           |  |           |  |           |  ...  |           |  |           |  
|           |  |           |  |           |  ...  |           |  |           |  
|           |  |           |  |           |  ...  |           |  |           |

 

  while (has_data) {
    has_data = false;
    std::shared_ptr<CNFrameInfo> data;
    // sync data
    data = connector->PopDataBufferFromConveyor(conveyor_idx);
    if (nullptr == data.get()) {
      /*
         nullptr will be received when connector stops.
         maybe only part of the connectors stopped.
         */
      continue;
    }
    has_data = true;
  ...
}

 

 

 

上面代码不停的获取数据执行,如果获取数据等待时间超过20个单位就放弃这个数据帧,如果pipeline停止了,就结束线程。

在获得数据后执行下面代码,左边data是一个CNFramInfo的结构体,等式左边就是在data的module_mask_map_里面查找数据是否已经流过当前模块,等式右边是返回Module的mask_,返回一个掩码值。如果相等表示这个数据还没有被使用过(?),可以进行下面的步骤;否则发生异常终止这个线程。

下一行代码ClearModuleMask是设置传过来的数据data的module_mask_map_的,module_mask_map标识这个数据传已经流过到哪几个模块。

    has_data = true;
    assert(data->GetModulesMask(instance.get()) == instance->GetModulesMask());
    data->ClearModuleMask(instance.get());

    int ret = instance->DoProcess(data);

最后一行DoProcess()函数本质是将data传递到下一个模块,调用了Pipeline的ProvideData(Module *, FrameInfoPtr)函数,而ProvideData()函数内部又调用了Pipeline类的TransmitData()函数来实现数据的传递。我们展开TransmitData函数。

前面先进行data检查,然后设置了Event e添加到event_bus_里面,然后比较eos_mask(计算得到)跟eos_mask_(设置的),如果两张相等表示没有丢掉中间模块(也就是这一个数据经过了所有的模块)(?),创建一个StreamMsg msg,更新消息。最后是将数据传输到下一个模块:

  for (auto& down_node_name : module_info.down_nodes) {
    ModuleAssociatedInfo& down_node_info = modules_.find(down_node_name)->second;
    assert(down_node_info.connector);
    assert(0 < down_node_info.input_connectors.size());
    Module* down_node = modules_map_[down_node_name].get();
    uint64_t frame_mask = data->SetModuleMask(down_node, module);

    if (processed_by_all_modules) {
      down_node->NotifyObserver(data);
      std::shared_ptr<Connector> connector = down_node_info.connector;
      const uint32_t chn_idx = data->channel_idx;
      int conveyor_idx = chn_idx % connector->GetConveyorCount();
      connector->PushDataBufferToConveyor(conveyor_idx, data);
    }
  }

 上面代码实现的功能就是实现数据流过完整的模块和数据分发到下一个模块connector的策略选择。

首先是数据流过完整的模块:假设有下面这样的一条pipeline包含5个模块并且里面有分支。

                   ----- M_C --                   
                  |             |
M_A -----> M_B ---               ----> M_E 
                  |             |
                  ------ M_D --

 

 A传到B,检查processed_by_all_modules是否成立,显然只有(待续)

 

 

 

获得数据之后进行传输数据,通过调用DoProcess()函数。

3 将数据送到pipeline中

  cnstream::Module *source = pipeline.GetModule(module_a_config.name);
  cnstream::SourceModule *source_ = dynamic_cast<cnstream::SourceModule *>(source);

调用GetModule()函数,在Pipeline类中的成员modules_map_中找到之前创造的module,并且返回一个Module类指针,之后将Module指针转换为SourceModule指针,命名为source_。这里SourceModule是Module的派生类,利用一个基类指针可以实现函数的重载。

  std::shared_ptr<cnstream::SourceHandler> handler0(new (std::nothrow) ExampleSourceHandler(source_, "stream_id_0"));
  source_->AddSource(handler0);
  std::shared_ptr<cnstream::SourceHandler> handler1(new (std::nothrow) ExampleSourceHandler(source_, "stream_id_1"));
  source_->AddSource(handler1);

ExampleSourceHandler继承了SourceHandler,上述代码调用ExampleSourceHandler的构造函数,从而调用了SourceHandler的构造函数。

  explicit SourceHandler(SourceModule *module, const std::string &stream_id) : module_(module), stream_id_(stream_id) {
    if (module_) {
      stream_index_ = module_->GetStreamIndex(stream_id_);
    }
  }

SourceHandler构造函数的作用就是把完成自身module_、stream_id_和stream_index的赋值,将传入的SourceModule指针source_指向本身类里面的SourceModule指针module_,stream_id_直接赋值传入的stream_id,完成后将SourceHandler类成员stream_index赋值(Q:stream_index的作用是什么)。

在项目中具体是FileHandler/RstpHandler/ESMemHandler/ESJpegMemHandler/RawImgMemHandler/UsbHandler,我们根据数据来源去构造一个具体的handler,然后全部返回一个SourceHandler的指针。

source_->AddSource(handler0);得到不同的handler之后将他们链接到source_中,通过source_的source_map_实现,键值对分别是stream_id_和handler。这里的理解是Source_表示所有数据的来源,handler0、handler1表示数据的一种来源:file、jpeg_mem、mem等等。

 4 关闭流水线

  pipeline.WaitForStop();

这里必须清楚几个多线程类,第一个是MyPipeline类里面定义的 std::atomic<int> exit_flag_{0};变量,在它本身构造函数里面赋值为0。

  void WaitForStop() {
    while (!exit_flag_.load()) {
      std::this_thread::sleep_for(std::chrono::milliseconds(100));
    }
    this->Stop();
  }

之后关闭流水线需要不断访问它。

 

posted on 2020-08-12 15:35  wulc++  阅读(720)  评论(0)    收藏  举报