源码看CAF的线程调度框架

news2024/12/23 19:28:41

本篇文章带着大家来看下CAF(C++ Actor Framwwork)的调度框架,也是算现阶段C++比较成熟的调度框架,大家如果自己完成一个比较大型的项目,任务调度也可以参照CAF。

鉴于篇幅较长,大家如果学习使用如何使用CAF可以参照 http://purecpp.cn/detail?id=2237 内容,如果想要直接学习调度框架,可以直接跳转到任务调度小节。

关于CAF用法

简单说一下CAF的用法或者说一个调用模型,它里边比较关键的概念是Actor,翻译过来就是角色,每个Actor中有beavior(行为)这个概念,beavior里边就是这个角色会做的事情(也就是一个函数)。Actor是基本的计算单元和通信的单元,两个actor相互通信,actor执行自己的behavior。如图所示:

再来看下官方的使用例子,我删了下注释:

using namespace caf;

behavior mirror(event_based_actor* self) {
  return {
    [=](const std::string& what) -> std::string {
      aout(self) << what << std::endl;
      return std::string{what.rbegin(), what.rend()};
    },
  };
}

void hello_world(event_based_actor* self, const actor& buddy) {
  self->request(buddy, std::chrono::seconds(10), "Hello World!")
    .then(
      [=](const std::string& what) {
        aout(self) << what << std::endl;
      });
}

void caf_main(actor_system& sys) {
  // create a new actor that calls 'mirror()'
  auto mirror_actor = sys.spawn(mirror);
  // create another actor that calls 'hello_world(mirror_actor)';
  sys.spawn(hello_world, mirror_actor);
  // the system will wait until both actors are done before exiting the program
}

CAF_MAIN()

我这里帮着大家顺一下这段代码:

  • 函数入口在CAF_MAIN宏中,主要是做一些初始化工作,和我们主题相关的是会创建actor_system对象并调用caf_main函数。
  • 然后caf_main中调用sys.spawn会创建出来一个actor,我们看mirror_actor的behavior,是在mirror函数中return了一个behavior对象,也就是spawn会调用mirror函数并获得了behavior,然后创建了actor对象。
  • 再下一个spawn函数调用hello_world,传入mirror_actor对象,对应于buddy参数,并执行request函数,request传入buddy参数,表示会调用buddy的behavior函数,根据函数参数去找,,request参数的第二个表示该函数的超时时间,第三个之后才是要调用buddy这个actor的behavior的函数参数,那么这里正好可以调用到mirror_actor的behavior,当behavior返回时会自动执行then这个函数。
  • 最后整体的执行顺序就是,sys调用spawn创建拥有behavior的actor对象mirror_actor, 第二个spawn会创建出来第二个actor对象,并调用自己的request函数向另外一个actor发送调用信息,也即会调用buddy的bahavior,调用behavior后执行then函数,整体结束。

调度器启动(初始化)

首先调度器的构造或者说初始化是在actor_system的构造函数进行:

// actor_system.cpp

actor_system::actor_system(actor_system_config& cfg) {
  // ...

  auto& sched = modules_[module::scheduler];
  using namespace scheduler;
  using policy::work_sharing;
  using policy::work_stealing;
  using share = coordinator<work_sharing>;
  using steal = coordinator<work_stealing>;
  if (!sched) {
    enum sched_conf {
      stealing = 0x0001,
      sharing = 0x0002,
      testing = 0x0003,
    };
    sched_conf sc = stealing;
    namespace sr = defaults::scheduler;
    auto sr_policy = get_or(cfg, "caf.scheduler.policy", sr::policy);
    if (sr_policy == "sharing")
      sc = sharing;
    else if (sr_policy == "testing")
      sc = testing;
    else if (sr_policy != "stealing")
      std::cerr << "[WARNING] " << deep_to_string(sr_policy)
                << " is an unrecognized scheduler pollicy, "
                   "falling back to 'stealing' (i.e. work-stealing)"
                << std::endl;
    switch (sc) {
      default: // any invalid configuration falls back to work stealing
        sched.reset(new steal(*this));
        break;
      case sharing:
        sched.reset(new share(*this));
        break;
      case testing:
        sched.reset(new test_coordinator(*this));
    }
  }

  // ...
}

调度器的对象是从modules_这个数组中取出,我们看下module是什么:

// actor_system.hpp
class CAF_CORE_EXPORT actor_system {
  // ...

  class CAF_CORE_EXPORT module {
  public:
    enum id_t {
      scheduler,
      middleman,
      openssl_manager,
      network_manager,
      num_ids
    };

    virtual ~module();
    const char* name() const noexcept;
    virtual void start() = 0;
    virtual void stop() = 0;
    virtual void init(actor_system_config&) = 0;
    virtual id_t id() const = 0;
    virtual void* subtype_ptr() = 0;
  };

  // ...
};

module会提供一些公共接口,init,start,stop等等之类的。我们也看到调度器是属于module的。

然后再回到创建调度器那里,最开始调度器是没有的,然后可以看的出来有三种调度配置,steal(偷取任务),share(共享任务),test(测试模式),我们就忽略测试模式。截止到switch那里,我们知道从配置文件中读取到调度的配置是什么,就去创建相应的调度器。那么调度器的类就是coordinator,work_sharing和work_stealing分别是不同调度配置的实现类,coordinator传入模版参数为调度的策略类(work_sharing和work_stealing)来实现调度器的全部功能。

然后继续往下继续看,目前已经有了调度器的创建,我们大致看下实现调度器共的类大体继承关系:

// coordinator.hpp
template <class Policy>
class coordinator : public abstract_coordinator {
  // ...
};

// abstract_coordinator.hpp
class CAF_CORE_EXPORT abstract_coordinator : public actor_system::module {
  // ...
};

coordinator继承abstract_coordinator,abstract_coordinator然后继承actor_system::module,这就和我们上边说的调度器属于module联系上了。

然后我们继续回到actor_system继续往下走:

// actor_system.cpp
actor_system::actor_system(actor_system_config& cfg) {
  // ...

  for (auto& mod : modules_)
    if (mod)
      mod->init(cfg);

  // ...
}

这里我们看到会调用调度器实现类的init函数,init函数是由abstract_coordinator类实现:

// abstract_coordinator.cpp
void abstract_coordinator::init(actor_system_config& cfg) {
  namespace sr = defaults::scheduler;
  max_throughput_ = get_or(cfg, "caf.scheduler.max-throughput",
                           sr::max_throughput);
  num_workers_ = get_or(cfg, "caf.scheduler.max-threads",
                        default_thread_count());
}

这里实现比较简单,就是从配置获取最大吞吐量(max_throughput_)和worker的数量,worker的数量默认是线程个数。

继续看actor_system构造函数:

// actor_system.cpp

actor_system::actor_system(actor_system_config& cfg) {
  // ...

  for (auto& mod : modules_)
    if (mod)
      mod->start();

  // ...
}

这里就是会调用到coordinator的start函数:

// coordinator.hpp
template <class Policy>
class coordinator : public abstract_coordinator {
  // ...

using worker_type = worker<Policy>;
void start() override {
    // Create initial state for all workers.
    typename worker_type::policy_data init{this};
    // Prepare workers vector.
    auto num = num_workers();
    workers_.reserve(num);
    // Create worker instances.
    for (size_t i = 0; i < num; ++i)
      workers_.emplace_back(
        std::make_unique<worker_type>(i, this, init, max_throughput_));
    // Start all workers.
    for (auto& w : workers_)
      w->start();

    // ...
  }

  // ...
}

首先获取到worker个数,然后来构造worker放置到workers_的数据结构中。然后调用worker的start函数。我们进入到worker类中去看看顺便看下它的start函数:

// worker.hpp

template <class Policy>
class worker : public execution_unit {
public:
  using coordinator_ptr = coordinator<Policy>*;
  using policy_data = typename Policy::worker_data;
  worker(size_t worker_id, coordinator_ptr worker_parent,
         const policy_data& init, size_t throughput)
    : execution_unit(&worker_parent->system()),
      max_throughput_(throughput),
      id_(worker_id),
      parent_(worker_parent),
      data_(init) {
    // nop
  }

  void start() {
    CAF_ASSERT(this_thread_.get_id() == std::thread::id{});
    this_thread_ = system().launch_thread("caf.worker", thread_owner::scheduler,
                                          [this] { run(); });
  }

  // ...
};

首先构造函数是传入吞吐量,id,coordinator和policy_data对象。然后当调用start函数时其实就是开启了一个线程执行run函数,那也就是说每个worker其实就是开启一个线程来执行任务而已。再去看下run函数:

// worker.hpp

template <class Policy>
class worker : public execution_unit {
  // ...

private:
  void run() {
    // scheduling loop
    for (;;) {
      auto job = policy_.dequeue(this);
      policy_.before_resume(this, job);
      auto res = job->resume(this, max_throughput_);
      policy_.after_resume(this, job);

      switch (res) {
        case resumable::resume_later: {
          policy_.resume_job_later(this, job);
          break;
        }
        case resumable::done: {
          policy_.after_completion(this, job);
          intrusive_ptr_release(job);
          break;
        }
        case resumable::awaiting_message: {
          intrusive_ptr_release(job);
          break;
        }
        case resumable::shutdown_execution_unit: {
          policy_.after_completion(this, job);
          policy_.before_shutdown(this);
          return;
        }
      }
    }
  }

  Policy policy_;

  // ...
};

我省略了下代码,后边还会回来看。run函数主要就是做几件事,从Policy中获取一个job,执行job(job->resume), 然后判断结果做出不同的行为,这里要说一下的是当执行完job,返回结果时resumable::resume_later时,会继续将这个job丢入到队列中,之后某个时刻会再次调用到这个job

以上就是整个调度器的初始化过程,首先actor_system的构造函数中会根据不同调度策略,创建调度器类(coordinator<Policy>),并调用coordinator的init函数和start函数。其中start函数会构造worker并调用worker的start函数。一个worker<Policy>在start时会开一个线程调用run函数,run函数负责执行被调度过来的任务。

actor的创建

我们的例子中CAF创建actor通过actor_system的spawn函数来创建一个actor,简单看下创建的actor的过程:

// actor_system.hpp
infer_handle_from_fun_t<F> spawn(F fun, Ts&&... xs) {
  using impl = infer_impl_from_fun_t<F>;
  check_invariants<impl>();
  static constexpr bool spawnable = detail::spawnable<F, impl, Ts...>();
  static_assert(spawnable,
                "cannot spawn function-based actor with given arguments");
  actor_config cfg;
  return spawn_functor<Os>(detail::bool_token<spawnable>{}, cfg, fun,
                          std::forward<Ts>(xs)...);
}

infer_handle_from_fun_t这个我们跟踪进去会发现是actor这个类型,函数内容的第一行的impl是event_based_actor,spawn_functor函数就会创建actor并把相应的的behavior保存下来。然后太多这里先不管关注了,这里大家需要理解的是有两个点,第一个我们这里会创建一个event_based_actor对象,然后使用actor对象来接收并返回,那也就是说CAF的actor中使用actor_control_block对象来持有创建出来的对象,因为创建出来有可能是别的actor类型,那看下所有类型的actor的类图:

第二个点是event_based_actor创建behavior是会调用到event_based_actor的make_behavior函数:

// event_based_actor.cpp
void event_based_actor::initialize() {
  // ...
  auto bhvr = make_behavior();
  if (bhvr) {
    become(std::move(bhvr));
  }
}


behavior event_based_actor::make_behavior() {
  behavior res;
  if (initial_behavior_fac_) {
    res = initial_behavior_fac_(this);
    initial_behavior_fac_ = nullptr;
  }
  return res;
}

后调用become函数,最终会调用到他的父类scheduled_actor的do_become函数;

// scheduled_actor.cpp
void scheduled_actor::do_become(behavior bhvr, bool discard_old) {
  // ...
  if (bhvr)
    bhvr_stack_.push_back(std::move(bhvr));
  // ...
}

这里可以看到会讲behavior放置到scheduled_actor的bhvr_stack_这个成员变量里。

任务调度

创建一个任务

我们上边也看到event_based_actor是继承scheduled_actor这个类,但是实际的继承看起来还有有点绕的:

// event_based_actor.hpp
class CAF_CORE_EXPORT event_based_actor
  : public extend<scheduled_actor, event_based_actor>::
           with<mixin::sender,
                mixin::requester,
                mixin::subscriber,
                mixin::behavior_changer> {

                  // ...
};

这里看到实际上event_based_actor继承extend的with类型,最后大家会发现实际是继承mixin::sender,mixin::requester,mixin::subscriber,mixin::behavior_changer,这四个类型,这四个类型又继承scheduled_actor,这段代码还是有点意思,大家可以去实际去看下。

然后我们回到我们最初的例子,会通过event_based_actor的request函数调用到另外一个actor的behavior里。正好我们request函数是在继承的mixin::requester类里:

// requester.hpp
auto request(const Handle& dest, timeout, Ts&&... xs) {
  // ...
  auto self = static_cast<Subtype*>(this);
  auto req_id = self->new_request_id(P);
  auto pending_msg = disposable{};
  if (dest) {
    detail::profiled_send(self, self->ctrl(), dest, req_id, {},
                          self->context(), std::forward<Ts>(xs)...);
    pending_msg = self->request_response_timeout(timeout, req_id);
  } else {
    //...
  }
  using response_type
    = response_type_t<typename Handle::signatures,
                      detail::implicit_conversions_t<detail::decay_t<Ts>>...>;
  using handle_type
    = response_handle<Subtype, policy::single_response<response_type>>;
  return handle_type{self, req_id.response_id(), std::move(pending_msg)};
}

生成一个req_id,通过profiled_send发送消息到另外一个actor,并返回response_type。我们后边会调用then函数,也即调用response_type的then,最终会调用到scheduled_actor的add_multiplexed_response_handler函数中:

// scheduled_actor.cpp
void scheduled_actor::add_multiplexed_response_handler(message_id response_id, behavior bhvr) {
  if (bhvr.timeout() != infinite)
    request_response_timeout(bhvr.timeout(), response_id);
  multiplexed_responses_.emplace(response_id, std::move(bhvr));
}

可以看到会把then后边的函数存放在multiplexed_responses_,与response_id相对应。那这里大家应该可以猜到,是发送消息的actor收到回应就会调用到这个函数。


让我们回过头看下profiled_send函数会做什么:

// profiled_send.hpp
void profiled_send(Self* self, SelfHandle&& src, const Handle& dst,
                   message_id msg_id, std::vector<strong_actor_ptr> stages,
                   execution_unit* context, Ts&&... xs) {
  if (dst) {
    auto element = make_mailbox_element(std::forward<SelfHandle>(src), msg_id, std::move(stages), std::forward<Ts>(xs)...);
    dst->enqueue(std::move(element), context);
  } else {
    // ...
  }
}

首先通过make_mailbox_element构造一个message,然后就是直接放到dst的enqueue函数,也把消息放置在目的actor的队列里。

同样这个函数依然是调用到scheduled_actor的enqueue函数。

// scheduled_actor.cpp
bool scheduled_actor::enqueue(mailbox_element_ptr ptr, execution_unit* eu) {
  // ...
  auto mid = ptr->mid;
  auto sender = ptr->sender;
  // ...
  switch (mailbox().push_back(std::move(ptr))) {
    case intrusive::inbox_result::unblocked_reader: {
      // ...
      if (private_thread_)
        private_thread_->resume(this);
      else if (eu != nullptr)
        eu->exec_later(this);
      else
        // ...
      return true;
    }
    case intrusive::inbox_result::success:
      return true;
    default: { // intrusive::inbox_result::queue_closed
      // ...
      return false;
    }
  }
}

这里稍微有点复杂,首先就是需要把message放到scheduled_actor的信箱里,这个信箱也是scheduled_actor的成员,然后会返回结果。

需要比对下返回结果,unblocked_reader的意思是actor未被调度执行behavior,最开始actor没有任务执行那肯定是这样的。success就是表明actor现在正在执行其他的behavior然后直接返回true就可以了。

那为什么要这样区分呢,我们看下没有actor没有调度执行behavior这条分支后边会做什么。首先如果scheduled_actor有自己的私有的线程,一般都是没有的,这里不是重点。然后看下eu是execution_unit对象,那么这个execution_unit是什么呢,大家看代码就会发现execution_unit是被worker继承的。篇幅原因我省略了一些代码讲解,我直接说下原理,当actor在创建的时候会设定下他的上下文(ctx)是某个worker,而这里eu其实是发送方actor的上下文(ctx),也就是worker。这里eu就是worker,我们直接定位到函数:

  void exec_later(job_ptr job) override {
    policy_.internal_enqueue(this, job);
  }

这里其实很关键,我们能发现worker最终消费的job其实是actor,然后把job也就是actor会放到policy的队列中。

现在我们知道为什么消息放到信箱中后需要区分现在actor的状态了,如果actor现在没有执行behavior,需要将actor放到队列中被调度执行,如果现在actor正在执行behavior那么不用管,把message放到信箱里,actor执行完当前的behavior,自然会从信箱拿出新的message从而去执行相应的behavior。

任务调度执行

这一小节算是这篇文章的重点了,限于一些CAF的概念不得不讲了前边很大的一坨。

到目前为止我们明白了actor是被worker调度执行的单元,且actor是存放在policy的队列中。然后被worker消费并执行actor的behavior。
我们钱包讲到了policy其实是有两种,share和steal,顾名思义就是一种是各个policy的任务是共享的,另外一种是一个policy会去别的policy去偷任务执行。具体代码是怎样的呢?我们来看下两个policy的入队列和出队列的函数;
先看worker_stealing这个类的函数:

// worker_stealing.hpp
template <class Worker>
void internal_enqueue(Worker* self, resumable* job) {
  d(self).queue.prepend(job);
}

入队列的函数很简单,仅仅是将job放到self(worker)的worker_data中的队列中。

// worker_stealing.hpp
template <class Worker>
resumable* dequeue(Worker* self) {
  // ...
  auto* job = d(self).queue.try_take_head();
  if (job)
    return job;
  for (size_t k = 0; k < 2; ++k) { // iterate over the first two strategies
    for (size_t i = 0; i < strategies[k].attempts;
          i += strategies[k].step_size) {
      // try to steal every X poll attempts
      if ((i % strategies[k].steal_interval) == 0) {
        job = try_steal(self);
        if (job)
          return job;
      }
      // wait for some work to appear
      job = d(self).queue.try_take_head(strategies[k].sleep_duration);
      if (job)
        return job;
    }
  }
  // ...
}

template <class Worker>
resumable* try_steal(Worker* self) {
  auto p = self->parent();
  if (p->num_workers() < 2) {
    return nullptr;
  }
  // roll the dice to pick a victim other than ourselves
  auto victim = d(self).uniform(d(self).rengine);
  if (victim == self->id())
    victim = p->num_workers() - 1;

  return d(p->worker_by_id(victim)).queue.try_take_tail();
}

简单理解下,当self(worker调用policy的dequeue函数取数据传入worker对象指针)的worker_data的队列中有数据就会从该队列中取得数据,当自己的队列中没有后且满足某些策略后调用try_steal去偷取别的worker下的worker_data的任务。利用worker的parent获取到其他的worker,然后再从其worker_data的队尾取出数据,worker的parent就是调度器的类coodinator,coodinator会持有所有的worker的对象。

接下来看work_sharing的相关函数:

//work_sharing.hpp
template <class Coordinator>
bool enqueue(Coordinator* self, resumable* job) {
  queue_type l;
  l.push_back(job);
  std::unique_lock<std::mutex> guard(d(self).lock);
  d(self).queue.splice(d(self).queue.end(), l);
  d(self).cv.notify_one();
  return true;
}

template <class Worker>
resumable* dequeue(Worker* self) {
  auto& parent_data = d(self->parent());
  std::unique_lock<std::mutex> guard(parent_data.lock);
  parent_data.cv.wait(guard, [&] { return !parent_data.queue.empty(); });
  resumable* job = parent_data.queue.front();
  parent_data.queue.pop_front();
  return job;
}

share模式的policy就比较简单,入队列就是将数据放置在coordinator的队列中,然后出队列取数据时,多个worker从其parent也就是coordinator的队列获取数据并返回。

让我们再次回到worker的执行体中:

// worker.hpp
void run() {
    CAF_SET_LOGGER_SYS(&system());
    // scheduling loop
    for (;;) {
      auto job = policy_.dequeue(this);
      policy_.before_resume(this, job);
      auto res = job->resume(this, max_throughput_);
      policy_.after_resume(this, job);

      //...
    }
  }

关键点是我们会从policy调用dequeue函数,传入参数this,然后获取到job,也就是actor,然后调用actor的resume就可以执行相应的behavior了。

我们也简单过一下resume函数:

// scheduled_actor.cpp
resumable::resume_result scheduled_actor::resume(execution_unit* ctx, size_t max_throughput) {
  // ...
  auto handle_async = [this, max_throughput, &consumed](mailbox_element& x) {
    return run_with_metrics(x, [this, max_throughput, &consumed, &x] {
      switch (reactivate(x)) {
        // ...
      }
    });
  };

  while (consumed < max_throughput) {
    mailbox_.fetch_more();
  
    auto& hq = get_urgent_queue();
    auto& nq = get_normal_queue();
    if (hq.new_round(quantum * 3, handle_async).consumed_items > 0) {
      
      nq.flush_cache();
    }
    if (nq.new_round(quantum, handle_async).consumed_items > 0) {
      hq.flush_cache();
    }

    auto delta = consumed - prev;
    if (delta > 0) {
      // ...
    } else {
      reset_timeouts_if_needed();
      if (mailbox().try_block())
        return resumable::awaiting_message;
    }
  }
  // ...
}

我只列了下重要的代码,handle_async是实际上会执行behavior的地方,首先mailbox_执行fetch_more会标注mailbox_中可以被消费的消息,然后hq和nq会相继取出消息去调用handle_async执行behavior,最后消费完全后,会使得mailbox设定其状态为block,也就是我们前边说到的actor会认为时当前没有behavior正在执行。

总结

这里来总结下CAF这个是如何调度任务的:

  1. 首先调度器(coordinator)会被初始化,调度器初始化多个worker,每个worker则为单独运行在一个线程上,然后worker获取任务会有两个policy来选择,共享模式和偷窃模式,共享模式是多个worker共享coordinator中的队列,偷窃模式是当一个worker使用完自己的队列就会去偷取别的worker的队列中的任务。
  2. 关于CAF特有的则是,创建任务时将消息直接放到actor的信箱里,将actor放到worker的队列中,当worker消费actor并调用其resume函数,actor就可以执行所有的从信箱中获取所有的消息所匹配的behavior。

ref

https://actor-framework.readthedocs.io/en/stable/index.html
http://purecpp.cn/detail?id=2237
https://github.com/actor-framework/actor-framework

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/164969.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

修改jupyter notebook默认路径

修改jupyter notebook默认路径jupyter notebook默认打开C:\Users\你的用户名&#xff0c;用户名是你的电脑用户名&#xff0c;upload文件又会在C盘生成一堆文件&#xff0c;很乱&#xff0c;用notebook打开文件还要跳转到目录&#xff0c;很麻烦&#xff0c;那有没有办法呢&…

【PYTHON】如何配置集成开发环境Geany

&#x1f482;作者简介&#xff1a; THUNDER王&#xff0c;一名热爱财税和SAP ABAP编程以及热爱分享的博主。目前于江西师范大学会计学专业大二本科在读&#xff0c;同时任汉硕云&#xff08;广东&#xff09;科技有限公司ABAP开发顾问。在学习工作中&#xff0c;我通常使用偏后…

【笔记】大话设计模式17-20

【笔记】大话设计模式17-20 文章目录【笔记】大话设计模式17-2017 适配器模式17.1 Example17.2 定义17.3 Show me the code17.4 总结18 备忘录模式18.1 Example18.2 定义18.3 Show me the code18.4 总结19 组合模式19.1 Example19.2 定义19.3 Show me the code19.4 总结20 迭代…

基于python的人工智能数据处理常用算法

文章目录二分法求解最小二乘法曲线拟合最小二乘法的来历最小二乘法与曲线拟合多项式曲线拟合SciPy内置最小二乘法应用泰勒级数背景引入泰勒公式泰勒级数展开与多项式近似二分法求解 机器学习过程中往往会用到很多变量&#xff0c;而这些变量之间的复杂关系一般用非线性方程来&…

VS系列知识-VS Code的安装+Vue环境的搭建+Vue指令

一、VS Code下载地址 Visual Studio Code - Code Editing. Redefined 二、VS Code初始化设置 1.安装插件 在安装好的VSCode软件的扩展菜单中查找安装如下4个插件 2、创建项目 vscode本身没有新建项目的选项&#xff0c;所以要先创建一个空的文件夹&#xff0c;如project_xx…

【用三大件写出的开门烟花特效】

又到了一年一度的春节时期啦&#xff01;昨天呢是北方的小年&#xff0c;今天是南方的小年&#xff0c;看到大家可以愉快的放烟花&#xff0c;过大年很是羡慕呀&#xff01;辞旧岁&#xff0c;贺新春&#xff0c;今年我呀要放烟花&#xff0c;过春节&#xff01;&#x1f9e8;。…

云原生|kubernetes|2022年底cks真题解析(1-10)

前言&#xff1a; cka和cks认证真的比较恶心&#xff0c;他们的那个PSI Bridge Secure Browser真的非常卡。 吐槽完毕&#xff0c;不废话&#xff0c;直接上真题解析。 CKS总共是16道题&#xff0c;题目顺序是打乱的&#xff0c;由于认证系统非常卡&#xff0c;因此&#xf…

通讯录最终版——动态存储+文件处理

最终版通讯录即从上一个版本修改过来先看总体代码&#xff0c;我们再看看差异ps&#xff1a;里面涉及到很多函数的使用&#xff0c;后续我会出专栏来书写这些函数的使用和实例&#xff0c;与常见错误大家可以通过https://cplusplus.com查找test.c#define _CRT_SECURE_NO_WARNIN…

Spring入门-IOC/DI入门与使用文件配置管理(1)

文章目录Spring入门1&#xff0c;Spring介绍1.1 为什么要学?1.2 学什么?1.3 怎么学?2&#xff0c;Spring相关概念2.1 初识Spring2.1.1 Spring家族2.1.2 了解Spring发展史2.2 Spring系统架构2.2.1 系统架构图2.2.2 课程学习路线2.3 Spring核心概念2.3.1 目前项目中的问题2.3.…

已解决:无法解析 jdk.tools:jdk.tools:1.6

文章目录问题描述解决方案问题描述 HBase API客户端操作时&#xff0c;报错&#xff1a;无法解析 jdk.tools:jdk.tools:1.6 这种问题司空见惯了&#xff0c;无非是依赖没下载&#xff0c;版本问题&#xff0c;依赖没加载成功&#xff0c;文件索引没更新成功&#xff0c;IDEA文…

大数据-Hadoop的介绍、配置和集群的使用

HDFS分布式文件系统 分布式&#xff1a;将多台服务器集中在一起&#xff0c;每台服务器都实现总体中的不同业务&#xff0c;做不同的事情 单机模式 厨房里只有一个人&#xff0c;这个人既要买菜&#xff0c;又要切菜&#xff0c;还要炒菜&#xff0c;效率低。 分布式模式 厨房…

leetcode2293:极大极小游戏(1.15每日一题)

题目表述&#xff1a; 给你一个下标从 0 开始的整数数组 nums &#xff0c;其长度是 2 的幂。 对 nums 执行下述算法&#xff1a; 设 n 等于 nums 的长度&#xff0c;如果 n 1 &#xff0c;终止 算法过程。否则&#xff0c;创建 一个新的整数数组 newNums &#xff0c;新数…

深浅copy

go 在go语言中值类型赋值都是深拷贝&#xff0c;引用类型一般都是浅拷贝其本质就是&#xff0c;深拷贝会拷贝数据&#xff0c;而浅拷贝只会拷贝内存的地址&#xff0c;所有就会出现&#xff0c;像slice那样修改底层数组的值&#xff0c;slice的值也跟着改动。 深拷贝 修改a的…

[iHooya]1月15日寒假班作业解析

过滤多余的空格 一个句子中也许有多个连续空格&#xff0c;过滤掉多余的空格&#xff0c;只留下一个空格。 输入&#xff1a;一行&#xff0c;一个字符串&#xff08;长度不超过200&#xff09;&#xff0c;句子的头和尾都没有空格。 输出&#xff1a;过滤之后的句子。 样例输…

全球各国机场名称、坐标经纬度、高程数据(更新至2022年)

数据来源&#xff1a;自主整理 时间跨度&#xff1a;更新至2022 区域范围&#xff1a;全球各国 指标说明&#xff1a; 全球机场坐标数据&#xff0c;包含CSV格式、shpfile格式、kml格式属性字段包括机场类型、经纬度&#xff0c;高程&#xff0c;所在国家省市区域&#xff…

5.12回溯法--连续邮资问题--子集树

回溯法的题目太多了&#xff0c;不想写这个代码了&#xff0c;于是我就开始水一篇文章&#xff0c;就单纯的分析一下这个问题保持整本书完整的队形 问题描述 如何用有限的邮票数&#xff0c;贴出更多面额的需求&#xff1f; 举例 n5&#xff0c;m4 设计1&#xff1a;X1{1, …

20多年老码农的IT学习之路

20年IT工作经历&#xff0c;目前在一家500强做企业架构&#xff0c;年薪税前150万多&#xff0e;最近公司业绩不好&#xff0c;有感觉工作不保&#xff0c;所以又捡起了编程&#xff0c;开始学习Golang&#xff0c;Angular等。我不是985&#xff0c;211也不是海归&#xff0c;我…

基于ssm+mysql+jsp实现在线花店

基于ssmmysqljsp实现在线花店一、系统介绍1、系统主要功能&#xff1a;2、环境配置二、功能展示1.主页(客户)2.登陆&#xff08;客户&#xff09;3.我的购物车(客户)4.我的订单&#xff08;客户&#xff09;5.主页&#xff08;管理员&#xff09;6.订单管理&#xff08;管理员&…

什么是链路追踪?分布式系统如何实现链路追踪?

在分布式系统&#xff0c;尤其是微服务系统中&#xff0c;一次外部请求往往需要内部多个模块&#xff0c;多个中间件&#xff0c;多台机器的相互调用才能完成。在这一系列的调用中&#xff0c;可能有些是串行的&#xff0c;而有些是并行的。在这种情况下&#xff0c;我们如何才…

PANNs:用于音频模式识别的大规模预训练音频神经网络

摘要 音频模式识别是机器学习领域的一个重要研究课题&#xff0c;它包括音频标注、声音场景分类、音乐分类、语音情感分类和声音事件检测等任务。近年来&#xff0c;神经网络已被应用于解决音频模式识别问题。然而&#xff0c;以前的系统是建立在特定数据集上的&#xff0c;数…