7.4.tensorRT高级(2)-使用RAII接口模式对代码进行有效封装

news2024/11/24 4:33:43

目录

    • 前言
    • 1. RAII接口模式
    • 2. 问答环节
    • 总结

前言

杜老师推出的 tensorRT从零起步高性能部署 课程,之前有看过一遍,但是没有做笔记,很多东西也忘了。这次重新撸一遍,顺便记记笔记。

本次课程学习 tensorRT 高级-使用 RAII 接口模式对代码进行有效封装

课程大纲可看下面的思维导图

在这里插入图片描述

1. RAII接口模式

这节我们学习多线程知识 —— RAII + 接口模式对生产者消费者模型进行封装,以及多 batch 的体现

那这边为什么提到多 batch 呢?这是因为 GPU 拥有计算密集型的特性,任务越密集越友好,假设 1 张图推理 需要 5ms,那你给它 2 张图,它的耗时很可能只有 6 ms,如果给 4 张图,它耗时可能 8 ms,也就是说它的耗时并不是成比例的,这就是我们说的多 batch 的体现

因此我们在对生产者消费者模式进行封装时就要考虑到 GPU 的特性,利用到它的特性,实现我们的高性能,高性能就是我们说的多 batch 的体现

接下来我们来看代码,我们先来写一个典型的程序,代码如下:

#include <thread>
#include <queue>
#include <mutex>
#include <string>
#include <stdio.h>
#include <chrono>
#include <condition_variable>
#include <memory>
#include <future>

using namespace std;

class Infer{
public:
    bool load_model(const string& file){
        context_ = file;
        return true;
    }

    void forward(){
        printf("使用 %s 进行推理\n", context_.c_str());
    }

    void destroy(){
        context_.clear();
    }

private:
    string context_;
};

int main(){

    return 0;
}

上述示例代码构建了一个 Infer 类,该类中有 load_model 和 forward 两个成员函数,模拟深度学习中的模型加载和推理过程,其中私有变量 context_ 可以认为是加载的模型权重文件,需要注意的是,实际工作中 context_ 是一个非常复杂的对象,包括有 vector、GPU 内存、CPU 内存等很多很多稀奇古怪的东西,你要进行释放的话还有硬件资源,包括你要加载的话初始化的东西也非常多,所以咱这边只是用 string 来表示一下,

我们需要通过对上述示例代码进行分析,然后慢慢的提出我们进行要学习的内容,即 RAII 接口模式

那首先我们先来使用这个类,具体代码如下:

int main(){
    
	Infer infer;
	infer.forward();	    
	return 0;
}

执行后你会发现,它直接会去拿一个空的 context_ 去进行推理,如果我的 context_ 是一个指针的情况下,你这个推理就直接崩溃了,所以这个时候你需要在 forward 函数中加上一个 context_ 是否为空的判断,如下所示:

void forward(){

    if(context_.empty()){
        // 说明模型没有加载上
        // 咱们对异常处理情况的定义很恼火
        printf("模型没有加载.\n");
        return;
    }

    printf("使用 %s 进行推理\n", context_.c_str());
}

那你可能会思考,如果 context_ 为空说明模型没有加载上,我们该如何处理呢?直接返回 false 还是干啥?🤔头大,我们在写代码时往往对于异常处理情况的定义很恼火,我们需要在 forward 函数中同时处理异常逻辑和正常逻辑,那为什么会这样呢?其实是因为我们的代码很容易就触发这种情况,你整个 infer 类的定义就是允许我这样使用的呀,没有任何地方告诉我不允许这么使用

对于使用者来讲他是很难发现其中的一些逻辑关系,比如说必须调用 load_model 才去调用 forward。所以站在使用者角度他很容易直接去执行 forward 函数,造成程序崩溃,然后就说你的 SDK 写得就是一个垃圾😂

所以根本原因是开发者没有告诉他你不可以直接去执行 forward 函数,你不可以这样去调用。如果开发者要让程序能够正常调用 forward,那么势必要写上异常逻辑处理,一旦要写异常逻辑,你会发现你的脑袋瓜像一个二叉树一样,每一个分支都要去考虑下怎么去处理,又遇到什么异常情况,所以异常逻辑会占用我们大量时间

所以根据上述分析,我们可以知道:

正常工作代码,异常逻辑需要耗费大量时间。异常逻辑如果没有写好,或者没写,就会造成封装的不安全性,导致程序崩溃。并且会导致程序的使用复杂度变高,编写复杂度变高

这是我们从 forward 函数中探讨出来的问题

同样的这个问题也会出现在 load_model 这个函数上,如果你已经加载过模型了,是不是应该 destory,因此也需要在 load_model 函数假设异常逻辑判断,如下所示:

if(!context_.empty()){
    destroy();
}

为什么一定要加异常逻辑呢?这是因为我们站在使用者的角度来看,他可能会执行多次 load_model,多次加载模型。那你可能会像似乎不加这个异常逻辑也行,因为 context_ 是 string 类型,会被覆盖掉

那之前有提到,现实中的 context_ 往往非常复杂,它可能包含线程的等待或退出、资源的释放等操作,所以它不是一个简单的覆盖就能结束的事情,工作量还是很多的,所以我们要加个 destroy 函数,是不是又多了一个异常逻辑代码

我们简单举了两个函数的例子,你会发现你代码多了一堆异常逻辑的例子,你的正常逻辑可能只有一行,你的异常逻辑有一堆,而且你还要考虑不同场景下这些异常该怎么处理,这就使得我们的压力变大,程序稳定性变差,安全性变差

既然它存在这个问题,我们就要想办法去解决,这就引入了我们的概念 RAII + 接口模式

RAII → \rightarrow 资源获取即初始化

接口模式 → \rightarrow 设计模式,是一种封装模式,实现类与接口类分离的模式

在我们的示例中,Infer infer 表示资源的获取,而 infer.load_model() 则表示资源的初始化,所谓的 RAII 就是将这两步合并成一步,代如下:

shared_ptr<Infer> create_infer(const string& file){
    
    shared_ptr<Infer> instance(new Infer());
    if(!instance->load_model(file))
        instance.reset();
    return instance;
}

之前我们是通过 Infer 类来获取资源,现在我们是通过 create_infer 来获取资源,它的好处在于获取 Infer 实例即表示加载了模型,加载模型失败,则表示资源获取失败,加载模型成功,则表示资源获取成功,它们强绑定,这是 RAII 所做的事情

这个时候交给外部使用者的逻辑就非常简单了,如下所示:

int main(){

    auto infer =  create_infer("a");
    if(infer == nullptr){
        printf("failed.\n");
        return -1;
    }

    infer->forward();

    return 0;
}

RAII 给我们带来的优点有:

1. 我们避免了外部执行 load_model,永远只有在一个地方执行 load_model,不可能出现在其它地方(RAII 没有完全限制,只是做到一部分,待会我们利用接口模式干掉它)

2. 一个实例的 load_model 不会执行超过 1 次

3. 获取的模型一定初始化成功,因此 forward 函数,不必判断模型是否加载成功

因此 load_model 中可以删掉对于重复 load 的判断,forward 函数中也可以删掉对是否加载成功的判断,修改后的代码如下所示:

#include <thread>
#include <queue>
#include <mutex>
#include <string>
#include <stdio.h>
#include <chrono>
#include <condition_variable>
#include <memory>
#include <future>

using namespace std;

class Infer{
public:
    bool load_model(const string& file){
        context_ = file;
        return true;
    }

    void forward(){

        printf("使用 %s 进行推理\n", context_.c_str());
    }

    void destroy(){
        context_.clear();
    }

private:
    string context_;
};

shared_ptr<Infer> create_infer(const string& file){
    
    shared_ptr<Infer> instance(new Infer());
    if(!instance->load_model(file))
        instance.reset();
    return instance;
}


int main(){

    auto infer =  create_infer("a");
    if(infer == nullptr){
        printf("failed.\n");
        return -1;
    }

    infer->forward();

    return 0;
}

可以发现确实简洁了很多,那你可能会问那我 Infer 类在外部不是一样可以调用 load_model 函数吗?

这就要引出我们的接口模式封装了,为了避免你外部调用这些多余的东西,我们需要使用接口模式把它们统统干掉

接口模式主要解决下面两个问题:

1. 解决 load_model 还能被外部看到的问题,拒绝外面调用 load_model

2. 解决成员变量对外可见的问题

  • 对于成员函数是特殊类型,比如 cudaStream_t,那么使用者就必须包含 cuda_runtime.h 头文件,否则会导致语法解析失败
  • 因此成员变量如果外部可见,会造成命名空间污染/头文件污染,有可能还会造成程序错误,异常,容易出现各种编译错误等等非预期的结果

我们来看具体该如何实现,首先我们需要定义一个接口类,它是一个纯虚类,原则是只暴露调用者需要的函数,其它一概不暴露,比如说 load_model,我们前面通过 RAII 做了定义,因此 load_model 属于不需要的范畴。内部如果有启动线程的函数如 start、stop 等等,也不需要暴露,而是初始化的时候就自动启动,都是 RAII 的定义。因此当前的接口类只对外暴露 forward 即可,代码实现如下:

class InferInterface{
public:
    virtual void forward() = 0;
};

我们再将之前的 Infer 类修改为 InferImpl 实现类,它继承自 InferInterface 公开类,另外之前的 RAII 部分也需要修改,代码如下所示:

#include <thread>
#include <queue>
#include <mutex>
#include <string>
#include <stdio.h>
#include <chrono>
#include <condition_variable>
#include <memory>
#include <future>

using namespace std;

class InferInterface{
public:
    virtual void forward() = 0;
};

class InferImpl : public InferInterface{
public:
    bool load_model(const string& file){
        context_ = file;
        return true;
    }

    virtual void forward() override{
        printf("使用 %s 进行推理\n", context_.c_str());
    }

    // 在析构函数中进行清理操作
    ~InferImpl(){
        context_.clear();
    }

private:
    string context_;
};

shared_ptr<InferInterface> create_infer(const string& file){
    shared_ptr<InferImpl> instance(new InferImpl());
    if(!instance->load_model(file))
        instance.reset();
    return instance;
}

int main(){
    auto infer =  create_infer("a");
    if(infer == nullptr){
        printf("failed.\n");
        return -1;
    }

    infer->forward();

    return 0;
}

这么做对于使用者来讲非常友好,因为你只能看到 forward ,其它都看不到

这个时候我们就可以实际的把它拆分了写成头文件形式,如下所示:

infer.hpp

#ifndef INFER_HPP
#define INFER_HPP

#include <memory>
#include <string>

class InferInterface{
public:
    virtual void forward() = 0;
};

std::shared_ptr<InferInterface> create_infer(const std::string& file);

#endif // INFER_HPP

infer.cpp

#include "infer.hpp"

using namespace std;

class InferImpl : public InferInterface{
public:
    bool load_model(const string& file){
        context_ = file;
        return true;
    }

    virtual void forward() override{
        printf("使用 %s 进行推理\n", context_.c_str());
    }

    // 在析构函数中进行清理操作
    ~InferImpl(){
        context_.clear();
    }

private:
    string context_;
};

shared_ptr<InferInterface> create_infer(const string& file){
    shared_ptr<InferImpl> instance(new InferImpl());
    if(!instance->load_model(file))
        instance.reset();
    return instance;
}

main.cpp

#include "infer.hpp"

int main(){
    
    auto infer = create_infer("a");
    if(infer == nullptr){
        printf("failed.\n");
        return -1;
    }

    infer->forward();
    return 0;
}

这就是我们建议的一个封装方式,这是我们通过 RAII + 接口模式封装的效果,通过这个效果我们来总结下几个原则:

1. 头文件,尽量只包含需要的部分

2. 外界不需要的,尽量不让它看到,保持定义的简洁

3. 不要在头文件中写 using namespace 这种,但是可以在 cpp 中写 using namespace ,对于命名空间,应当尽量少的展开

看到这个不由的想起 tensorRT_Pro,这种封装方式其实在 tensorRT_Pro 中有所体现,只是以前一直没有注意罢了

我们在 tensorRT_Pro 中如果想要用 YOLO 模型进行推理该怎么做呢?部分代码如下:

auto engine = Yolo::create_infer(...);
if(engine == nullptr){
    INFO("Engine is nullptr");
    return;
}

auto boxes = engine->commit(image);

...

上面的 create_infer 是不是一样,commit 可以看作 forward,你点开它的 yolo.hpp 和 yolo.cpp 可以看到 create_infer 以及 Infer 接口类的定义,如下所示:

class Infer{
    public:
    virtual shared_future<BoxArray> commit(const cv::Mat& image) = 0;
    virtual vector<shared_future<BoxArray>> commits(const vector<cv::Mat>& images) = 0;
};

shared_ptr<Infer> create_infer(
    const string& engine_file, Type type, int gpuid, 
    float confidence_threshold, float nms_threshold,
    NMSMethod nms_method, int max_objects,
    bool use_multi_preprocess_stream
){
    shared_ptr<InferImpl> instance(new InferImpl());
    if(!instance->startup(
        engine_file, type, gpuid, confidence_threshold, 
        nms_threshold, nms_method, max_objects, use_multi_preprocess_stream)
      ){
        instance.reset();
    }
    return instance;
}

是不是和我们的示例一模一样!!!,Infer 接口类只暴露 forward 接口,而 create_infer 体现 RAII 思想,果然再撸一遍还是没有错的,之前很多细节方面的东西似乎都遗漏了呀😂

2. 问答环节

博主对多线程相关的知识不怎么了解,因此疯狂询问 chatGPT,故此做个记录方便下次查看,以下内容来自于博主和 chatGPT 之间的对话

问题1:RAII是什么呢?能详细解释下吗?🤔

回答:当然可以!

RAII 是 “Resource Acquisition Is Initialization” 的缩写,直译为 资源获取即初始化。它是 C++ 编程中的一种编程技巧,用于管理资源(如内存、文件句柄、网络套接字等)的生命周期。

RAII 的基本思想是将资源的生命周期与对象的生命周期绑定起来:当对象被创建时,资源被获取;当对象被销毁时,资源被释放。这种自动管理资源的方式可以避免资源泄漏,同时提高代码的简洁性和可读性。

RAII 主要有以下几个特点:

  1. 资源的自动管理:在对象的构造函数中获取资源,在析构函数中释放资源。这样,只要对象的生命周期结束,相关资源就会被自动释放。
  2. 异常安全:由于资源的释放是自动的,所以即使在异常的情况下,资源也能够被正确地释放。
  3. 避免资源泄漏:通过自动管理资源,可以避免因遗漏资源释放而导致的资源泄漏。

下面是一个简单的 RAII 示例,展示了如何使用 RAII 来管理动态分配的内存:

class RAIIArray {
private:
   int* data;

public:
   // 构造函数:分配内存
   RAIIArray(size_t size) {
       data = new int[size];
   }

   // 析构函数:释放内存
   ~RAIIArray() {
       delete[] data;
   }

   // 其他成员函数...
};

在这个示例中,当 RAIIArray 对象被创建时,其构造函数会自动分配内存;当对象被销毁时,其析构函数会自动释放内存。因此,使用者无需担心内存的释放问题,可以确保不会发生内存泄漏。

RAII 不仅仅用于管理内存,它也可以用于管理其他类型的资源,如文件、数据库连接等。

问题2:什么是接口模式呢?能详细解释下吗?🤔

回答:接口模式,通常指的是设计模式中的接口编程,它强调的是编程到接口而不是实现。这使得代码更加模块化,易于测试和维护。

在面向对象编程中,接口是一种定义了一组方法的抽象类型,但没有实现这些方法。类可以实现一个或多个接口,并提供这些方法的具体实现。这种方法强调的是:

  • 分离定义和实现:您可以定义一个接口,而不需要知道谁或如何实现它。
  • 多态性:您可以使用接口作为一个类型,然后有多个不同的实现,这为代码提供了巨大的灵活性。
  • 可互换性:如果有多个类实现了同一个接口,您可以在不更改大部分代码的情况下,轻松地替换这些类的实例。

在C++中,虽然没有专门的“接口”关键字,但我们可以使用纯虚函数来模拟接口:

class IInfer {
public:
   virtual void load_model(const std::string& file) = 0;
   virtual void forward() = 0;
};

问题3:我不太懂,为什么 RAII 中 create_infer 函数要求返回的是 InferInterface 接口类的智能指针,而实际返回的是 InferImpl 实现类的智能指针?还能够这样操作吗?🤔

问题:很好的问题!这正是面向对象编程中多态性的一个典型应用。

在 C++ 中,子类的指针可以隐式地转换为其父类的指针,这是由于继承的性质。当子类继承了一个基类(或接口),它隐含地表示子类 “是一个” 基类。因此,子类对象可以被视为其基类的对象。

在示例中:

  • InferImplInferInterface 的具体实现。这意味着 InferImpl “是一个” InferInterface
  • create_infer 函数的返回类型是 shared_pt0r<InferInterface>,这意味着它返回一个指向 InferInterface 类型的智能指针。
  • 但在函数的实现中,您实际上返回了一个指向 InferImplshared_ptr。由于 InferImplInferInterface 的子类,因此这是允许的。

这样做的好处是:

1. 封装实现细节:调用者不需要知道具体的实现类是什么,他们只需要知道接口。这提供了一个清晰的分界线,使得实现的修改不会影响到使用接口的代码。

2. 灵活性:如果您决定更改实现,只需要更改 create_infer 函数内部的实现,而不会影响到使用此函数的代码。

3. 多态:调用者可以透明地使用不同的实现,只要它们都遵循相同的接口。

总之,这是 C++ 多态性的一个典型应用,允许您将子类的对象当作父类的对象来使用。这为代码提供了更大的灵活性和可维护性。

总结

这次课程我们学习了 RAII + 接口模式对代码进行封装,我们模拟了一个非常常见的推理类,首先我们分析了其存在的问题,那就是实例化 Infer 类,去调用 load_model、forward 等函数时,需要在函数去写异常逻辑处理,非常让人头疼。我们首先通过 RAII 将资源在构造函数中就进行初始化,避免了外部获取资源后还要调用相关函数去做初始化,其次我们采取了接口模式,将接口纯虚类与具体实现类分离开,让使用者只考虑具体的接口,而不必关心具体的实现。这种封装方式为代码提供了更大的灵活性和可维护性,十分推荐使用

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

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

相关文章

实战:ros机器人运行不稳定,也许是use_sim_time没有设置对

搞机环境&#xff0c;ubuntu 20.04 ros2 版本 foxy ros机器人搞了很久了&#xff0c;但是有一个初学者很容易忽略的参数&#xff1a;use_sim_time&#xff0c;设置不对&#xff0c;会让程序出跑起来有莫名其妙的问题。 use_sim_time &#xff1a;直白翻译&#xff1a; 用_仿…

一文读懂HTML

文章目录 HTML的历史HTML的作用HTML的基本语言 HTML的历史 HTML&#xff08;HyperText Markup Language&#xff09;的历史可以追溯到20世纪90年代早期&#xff0c;它是互联网发展的重要里程碑之一。以下是HTML的历史概述&#xff1a; 早期阶段&#xff08;1980年代末 - 1990年…

如何撰写一份清晰有效的说明文档

如何撰写一份清晰有效的说明文档 文章目录 导语1.明确读者群体&#xff1a;2.明确文档目的&#xff1a;3.提供清晰的结构&#xff1a;4.使用简洁明了的语言&#xff1a;5.提供具体的示例&#xff1a;6.注意文档格式和风格&#xff1a;7.接受反馈并更新文档&#xff1a;结语 导语…

JZ39 数组中出现次数超过一半的数字

目录 一、题目 二、代码 一、题目 数组中出现次数超过一半的数字_牛客题霸_牛客网 二、代码 class Solution { public:/*** 代码中的类名、方法名、参数名已经指定&#xff0c;请勿修改&#xff0c;直接返回方法规定的值即可** * param numbers int整型vector * return int…

Spring Boot单元测试与Mybatis单表增删改查

目录 1. Spring Boot单元测试 1.1 什么是单元测试? 1.2 单元测试有哪些好处? 1.3 Spring Boot 单元测试使用 单元测试的实现步骤 1. 生成单元测试类 2. 添加单元测试代码 简单的断言说明 2. Mybatis 单表增删改查 2.1 单表查询 2.2 参数占位符 ${} 和 #{} ${} 和 …

LeetCode 周赛上分之旅 #39 结合中心扩展的单调栈贪心问题

⭐️ 本文已收录到 AndroidFamily&#xff0c;技术和职场问题&#xff0c;请关注公众号 [彭旭锐] 和 BaguTree Pro 知识星球提问。 学习数据结构与算法的关键在于掌握问题背后的算法思维框架&#xff0c;你的思考越抽象&#xff0c;它能覆盖的问题域就越广&#xff0c;理解难度…

赴日IT培训 国内的程序员去日本做IT容易吗?

去日本当程序员的两大要素就是技术和日语。所以说容易也容易&#xff0c;如果学历过关&#xff08;统招大专以上&#xff09;&#xff0c;再加上在国内有经验&#xff0c;所以技术方面问题不大。要说难也难&#xff0c;你要克服语言关&#xff0c;去本本工作对日语的要求比较高…

NeuralNLP-NeuralClassifier的使用记录(一),训练预测自己的【英文文本多分类】

NeuralNLP-NeuralClassifier的使用记录&#xff0c;训练预测自己的英文文本多分类 NeuralNLP-NeuralClassifier是腾讯开发的一个多层多分类应用工具&#xff0c;支持的任务包括&#xff0c;文本分类中的二分类、多分类、多标签&#xff0c;以及层次多标签分类。支持的文本编码…

运维工程师常见面试题

1、http常见返回码 2、mysql的同步方式 1&#xff09;异步复制 MySQL默认的复制即是异步的&#xff0c;主库在执行完客户端提交的事务后会立即将结果返给给客户端&#xff0c;并不关心从库是否已经接收并处理&#xff0c;这样就会有一个问题&#xff0c;主如果crash掉了&a…

207、仿真-51单片机脉搏心率与血氧报警Proteus仿真设计(程序+Proteus仿真+配套资料等)

毕设帮助、开题指导、技术解答(有偿)见文未 目录 一、硬件设计 二、设计功能 三、Proteus仿真图 四、程序源码 资料包括&#xff1a; 需要完整的资料可以点击下面的名片加下我&#xff0c;找我要资源压缩包的百度网盘下载地址及提取码。 方案选择 单片机的选择 方案一&a…

Docker部署rabbitmq遇到的问题 Stats in management UI are disabled on this node

1. Stats in management UI are disabled on this node #进入rabbitmq容器 docker exec -it {rabbitmq容器名称或者id} /bin/bash#进入容器后&#xff0c;cd到以下路径 cd /etc/rabbitmq/conf.d/#修改 management_agent.disable_metrics_collector false echo management_age…

ArcGIS Maps SDK for JavaScript系列之二:认识Map和MapView

目录 Map创建一个 Map 对象的示例代码&#xff1a;Map的常用属性Map的常用方法 MapViewMapView的常用属性MapView的常用方法 在 ArcGIS Maps SDK for JavaScript 中&#xff0c;Map 和 MapView 是两个重要的概念&#xff0c;用于创建和展示地图应用程序。 Map Map 表示一个地图…

2023-08-13 LeetCode每日一题(合并两个有序数组)

2023-08-13每日一题 一、题目编号 88. 合并两个有序数组二、题目链接 点击跳转到题目位置 三、题目描述 给你两个按 非递减顺序 排列的整数数组 nums1 和 nums2&#xff0c;另有两个整数 m 和 n &#xff0c;分别表示 nums1 和 nums2 中的元素数目。 请你 合并 nums2 到 …

Error: EACCES: permission denied, rename ‘/usr/local/lib/node_modules/appium‘

在使用npm uninstall -g appium卸载appium的过程中报错 Error: EACCES: permission denied, rename /usr/local/lib/node_modules/appium -> /usr/local/lib/node_modules/.appium-cfBVovI6 npm ERR! code EACCES npm ERR! syscall rename npm ERR! path /usr/local/lib/n…

gcc/g++ 编译选项详解

gcc/g 编译选项详解 文章目录 gcc/g 编译选项详解编译步骤gcc 与 g 区别gcc 命令的常用选项编译优化选项-O 优化-O1优化-O2-O0-Os-Ofast-Og-Oz-O 选项控制特定的优化 WarningsReference>>>>> 欢迎关注公众号【三戒纪元】 <<<<< 编译步骤 gcc 、…

Codeforces Round 891 (Div. 3)ABC

Codeforces Round 891 (Div. 3) 目录 A. Array Coloring题目大意思路代码 B. Maximum Rounding题目大意思路代码 C. Assembly via Minimums题目大意思路代码 A. Array Coloring 题目大意 给你一个包含 n n n个数字的数组&#xff0c;你的任务是判断这个数组是否可以划分成两个…

TCP/IP 下的计算机网络江湖

〇、引言 在当今数字化时代,计算机网络宛如广袤江湖,涵盖着五大门派:物理层、数据链路层、网络层、传输层和应用层。每个门派独具技能,共同构筑着现代网络的框架。物理层宛如江湖基石,将比特流传输;数据链路层如武林传承,组织数据帧传递;网络层则像导航大师,寻找传送路…

Web framework-Gin

一、Gin Go Web--Go Module 软件框架&#xff08;software framework&#xff09;&#xff0c;通常指的是为了实现某个业界标准或完成特定基本任务的软件组件规范&#xff0c;也指为了实现某个软件组件规范时&#xff0c;提供规范所要求之基础功能的软件产品。 框架就是&#…

【报童模型】随机优化问题二次规划

面对需求的不确定性&#xff0c;报童模型是做库存优化的常见模型。而标准报童模型假设价格是固定的&#xff0c;此时求解一个线性规划问题&#xff0c;可以得到最优订货量&#xff0c;这种模型存在局限性。因为现实世界中价格与需求存在一定的关系&#xff0c;本文假设需求q是价…

FreeRTOS源码分析-12 低功耗管理

目录 1 STM32低功耗管理概念及应用 1.1睡眠模式 1.2 停止模式 1.3 待机模式 2 Tickless低功耗管理 2.1 Tickless低功耗模式介绍 2.2 FreeRTOS低功耗模式配置 2.3 FreeRTOS低功耗模式应用 3 低功耗管理实际项目开发 3.1 低功耗设计必须要掌握的硬件知识 …