C++20 协程原理与应用

协程

要想了解协程,最好先搞清楚进程,线程,这样才能将三者区分开来!

进程 vs 线程 vs 协程

进程线程协程
切换者操作系统操作系统用户(编程者)
切换时机根据操作系统自己的切换策略,用户不感知根据操作系统自己的切换策略,用户不感知用户(编程者)自己决定
切换内容页全局目录、内核栈、硬件上下文内核栈、硬件上下文硬件上下文
切换内容的保存保存于内核栈中保存于内核栈中保存于用户栈中
切换过程用户态-内核态-用户态用户态-内核态-用户态用户态(没有陷入内核态)

协程优点:

  1. I/O 阻塞时,利用协程来处理(切换效率比较高)
  2. 不需要锁机制

协程缺点:

  1. 不能利用多核 CPU

1. 进程和线程

1.1 进程

进程,直观点说,保存在硬盘上的程序运行以后,会在内存空间里形成一个独立的内存体,这个内存体 有自己独立的地址空间,有自己的堆 ,上级挂靠单位是操作系统。 操作系统会以进程为单位,分配系统资源(CPU时间片、内存等资源),进程是资源分配的最小单位

1706250365964

1706250434187

【进程间通信(IPC)】:

  • 管道(Pipe)、命名管道(FIFO)、消息队列(Message Queue) 、信号量(Semaphore) 、共享内存(Shared Memory);套接字(Socket)。

1.2 线程

线程,有时被称为轻量级进程(Lightweight Process,LWP),是操作系统调度(CPU调度)执行的最小单位

1706250470527

1.3 进程和线程的区别和联系

1.3.1 区别

  • 调度: 线程作为调度和分配的基本单位,进程作为拥有资源的基本单位;
  • 并发性: 不仅进程之间可以并发执行,同一个进程的多个线程之间也可并发执行;
  • 拥有资源: 进程是拥有资源的一个独立单位,线程不拥有系统资源,但可以访问隶属于进程的资源。进程所维护的是程序所包含的资源(静态资源), 如:地址空间,打开的文件句柄集,文件系统状态,信号处理 handler 等;线程所维护的运行相关的资源(动态资源),如:运行栈,调度相关的控制信息,待处理的信号集等;
  • 系统开销: 在创建或撤消进程时,由于系统都要为之分配和回收资源,导致系统的开销明显大于创建或撤消线程时的开销。但是进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响,而线程只是一个进程中的不同执行路径。线程有自己的堆栈和局部变量,但线程之间没有单独的地址空间,一个进程死掉就等于所有的线程死掉,所以多进程的程序要比多线程的程序健壮,但在进程切换时,耗费资源较大,效率要差一些。

1.3.2 联系:

  • 一个线程只能属于一个进程,而一个进程可以有多个线程,但至少有一个线程
  • 资源分配给进程,同一进程的所有线程共享该进程的所有资源;
  • 处理机分给线程,即真正在处理机上运行的是线程
  • 线程在执行过程中,需要协作同步。不同进程的线程间要利用消息通信的办法实现同步。

1.3.3 举例说明进程和线程区别

假如我们把整条道路看成是一个 “进程” 的话,那么由白色虚线分隔开来的各个车道就是进程中的各个 “线程” 了。

  • 这些 线程(车道) 共享了 进程(道路) 的公共资源(土地资源)。

  • 这些 线程(车道) 必须依赖于 进程(道路) ,也就是说,线程不能脱离于进程而存在(就像离开了道路,车道也就没有意义了)。

  • 这些 线程(车道) 之间可以 并发执行(各个车道你走你的,我走我的) ,也可以 互相同步(某些车道在交通灯亮时禁止继续前行或转弯,必须等待其它车道的车辆通行完毕)

  • 这些 线程(车道) 之间依靠 代码逻辑(交通灯) 来控制运行,一旦 代码逻辑控制有误(死锁,多个线程同时竞争唯一资源) ,那么线程将陷入混乱,无序之中。

1.4 进程/线程之间的亲缘性

亲缘性的意思是进程/线程只在某个 CPU 上运行(多核系统),比如:

BOOL WINAPI SetProcessAffinityMask(
  _In_ HANDLE    hProcess,
  _In_ DWORD_PTR dwProcessAffinityMask
);
/*
dwProcessAffinityMask 如果是 0 , 代表当前进程只在 cpu0 上工作;
如果是 0x03 , 转为2进制是 00000011 . 代表只在 cpu0 或 cpu1 上工作;
*/

使用CPU亲缘性的好处:

设置CPU亲缘性是为了防止进程/线程在CPU的核上频繁切换 ,从而 避免因切换带来的CPU的L1/L2 cache失效 ,cache 失效会降低程序的性能。

2. 协程

概要速览:

协程(coroutine)是一种在程序设计中用于实现多任务并发执行的技术。它允许在单个线程内进行多个任务之间的切换,这种切换是协作式的,即任务主动让出控制权,而不是抢占式的。协程的概念最早可以追溯到1958年,由马尔文·康威(Marvin Conway)提出,并在1963年首次公开发表。

在程序中,协程通常通过函数或方法的暂停和恢复来实现。一个协程函数可以在执行到某个点时暂停,将控制权让给其他的协程函数,并在适当的时候恢复执行。这种机制使得一个线程可以在等待某些操作(如I/O)完成时切换到其他任务,从而提高资源的利用率。

协程的实现通常依赖于事件循环(event loop)或类似的机制,用于调度和管理协程的执行。在许多编程语言中,协程已经成为并发编程的标准库部分,如 Python 中的 asyncio 模块、Go 语言中的协程以及 Kotlin 中的协程支持。

协程的使用场景非常广泛,包括但不限于:

  • 异步 I/O 操作:在网络请求或文件操作中,协程可以在等待 I/O 完成时执行其他任务。
  • 并发任务调度:在图形用户界面(GUI)应用程序中,协程可以实现平滑的用户界面响应,同时处理后台任务。
  • 协程异常处理:协程支持异常的传播和捕获,允许在协程结构中传播异常信息。

协程的一个重要特性是它们可以携带状态,这意味着当协程被挂起时,它当前的状态(包括局部变量和程序计数器等)会被保存,并在恢复执行时恢复到挂起前的状态。

在不同的编程语言和平台中,协程的实现和调度机制可能有所不同,但其核心概念和目的是相似的:通过协作式的多任务并发执行,提高程序的性能和响应能力。

1706250775284

协程,是一种比线程更加轻量级的存在,协程不是被操作系统内核所管理,而完全是由程序所控制(也就是在用户态执行)。这样带来的好处就是性能得到了很大的提升,不会像线程切换那样消耗资源。

子程序,或者称为函数,在所有语言中都是层级调用,比如 A 调用 B,B 在执行过程中又调用了 C,C 执行完毕返回,B 执行完毕返回,最后是 A 执行完毕。所以子程序调用是通过栈实现的,一个线程就是执行一个子程序。子程序调用总是一个入口,一次返回,调用顺序是明确的。而协程的调用和子程序不同。

协程在子程序内部是可中断的,然后转而执行别的子程序,在适当的时候再返回来接着执行

def A():
    print '1'
    print '2'
    print '3'

def B():
    print 'x'
    print 'y'
    print 'z'

假设由协程执行,在执行A的过程中,可以随时中断,去执行B,B也可能在执行过程中中断再去执行A,结果可能是:1 2 x y 3 z

协程的特点在于是一个线程执行,那和多线程比,协程有何优势?

  • 极高的执行效率:因为子程序切换不是线程切换,而是由程序自身控制,因此,没有线程切换的开销,和多线程比,线程数量越多,协程的性能优势就越明显;

  • 不需要多线程的锁机制:因为只有一个线程,也不存在同时写变量冲突,在协程中控制共享资源不加锁,只需要判断状态就好了,所以执行效率比多线程高很多。

3. C++20 协程原理和应用

3.1 有栈协程和无栈协程

所谓的有栈,无栈并不是说这个协程运行的时候有没有栈,而是说协程之间是否存在调用栈(callbackStack)。

协程可分为两种:

  • 无栈协程:即可挂起/恢复的函数,无栈协程切换的成本相当于函数调用的成本。
  • 有栈协程:即相当于用户态线程,有栈协程切换的成本是用户态线程切换的成本。

无栈协程和线程的区别:

  • 无栈协程只能被线程调用,本身并不抢占内核调度,而线程则可抢占内核调度。

C++20 协程中采纳的是微软提出并主导(源于C#)的无栈协程。

C++世界演化的主旋律:异步化和并行化,而 C++20 协程能够以同步原语写异步代码的特性,使其成为编写异步代码的好工具。

具体可以看这篇文章:https://blog.csdn.net/weixin_43705457/article/details/106924435

3.2 C++20 为什么选择无栈协程?

有栈(stackful)协程通常的实现手段是在堆上提前分配一块较大的内存空间(比如 64K),也就是协程所谓的“栈”,参数、return address 等都可以存放在这个“栈”空间上。如果需要协程切换,那么通过 swapcontext 一类的形式来让系统认为这个堆上空间就是普通的栈,这就实现了上下文的切换。

有栈协程最大的优势就是侵入性小,使用起来非常简便,已有的业务代码几乎不需要做什么修改,但是 C++20 最终还是选择了使用无栈协程,主要出于下面这几个方面的考虑。

  • 栈空间的限制:

    有栈协程的“栈”空间普遍是比较小的,在使用中有栈溢出的风险;而如果让“栈”空间变得很大,对内存空间又是很大的浪费。无栈协程则没有这些限制,既没有溢出的风险,也无需担心内存利用率的问题。

  • 性能:

    有栈协程在切换时确实比系统线程要轻量,但是和无栈协程相比仍然是偏重的,这一点虽然在我们目前的实际使用中影响没有那么大(异步系统的使用通常伴随了 IO,相比于切换开销多了几个数量级),但也决定了无栈协程可以用在一些更有意思的场景上。

3.3 无栈协程是普通函数的泛化

无栈协程是一个可以暂停和恢复的函数,是函数调用的泛化。

一个函数的函数体(function body)是顺序执行的,执行完之后将结果返回给调用者,我们没办法挂起它并稍后恢复它,只能等待它结束。而无栈协程则允许我们把函数挂起,然后在任意需要的时刻去恢复并执行函数体,相比普通函数,协程的函数体可以挂起并在任意时刻恢复执行。

1706250841310

所以,从这个角度来说,无栈协程是普通函数的泛化。

3.4 C++20 协程的三个关键字

C++20 提供了三个新关键字(co_await、co_yield 和 co_return),如果一个函数中存在这三个关键字之一,那么它就是一个协程。

协程相关的对象

协程帧(coroutine frame)

当 caller 调用一个协程的时候会先创建一个协程帧,协程帧会构建 promise 对象,再通过 promise 对象产生 return object。

协程帧中主要有这些内容:

  • 协程参数

  • 局部变量

  • promise 对象

promise_type

promise_type 是 promise 对象的类型。promise_type 用于定义一类协程的行为,包括协程创建方式、协程初始化完成和结束时的行为、发生异常时的行为、如何生成 awaiter 的行为以及 co_return 的行为等等。promise 对象可以用于记录/存储一个协程实例的状态。每个协程桢与每个 promise 对象以及每个协程实例是一一对应的。

coroutine return object

它是 promise.get_return_object() 方法创建的,一种常见的实现手法会将 coroutine_handle 存储到 coroutine object 内,使得该 return object 获得访问协程的能力。

std::coroutine_handle

协程帧的句柄,主要用于访问底层的协程帧、恢复协程和释放协程帧。
程序员可通过调用 std::coroutine_handle::resume() 唤醒协程。

co_await、awaiter、awaitable
  • co_await:一元操作符;

  • awaitable:支持 co_await 操作符的类型;

  • awaiter:定义了 await_ready、await_suspend 和 await_resume 方法的类型。

co_await expr 通常用于表示等待一个任务(可能是 lazy 的,也可能不是)完成。co_await expr 时,expr 的类型需要是一个 awaitable,而该 co_await 表达式的具体语义取决于根据该 awaitable 生成的 awaiter。

看起来和协程相关的对象还不少,这正是协程复杂又灵活的地方,可以借助这些对象来实现对协程的完全控制,实现任何想法。但是,需要先要了解这些对象是如何协作的,把这个搞清楚了,协程的原理就掌握了,写协程应用也会游刃有余了。

4. 如何实现协程

https://hkrb7870j3.feishu.cn/docx/RUMldRX8koIgxIxvAmccy4PYnCc

利用操作系统提供的接口来实现协程:

  1. Linux 的 ucontext(下文示例都是用这个)
  2. Windows 的 Fiber

4.1 ucontext 结构体

typedef struct ucontext {
    struct ucontext     *uc_link;
    sigset_t             uc_sigmask;
    stack_t              uc_stack;
    mcontext_t           uc_mcontext;
    unsigned long int    uc_flags;
} ucontext_t;
  • uc_link:当前上下文(如使用 makecontext 创建的上下文)运行终止时系统会恢复 uc_link 指向的上下文
  • uc_sigmask:上下文中的阻塞信号集合
  • uc_stack:上下文中使用的栈
  • uc_mcontext:保存的上下文的寄存器信息

4.2 ucontext 相关函数

  • getcontext:保存上下文,将当前运行到的寄存器的信息保存到 ucontext_t 结构体中。
  • setcontext:恢复上下文,将 ucontext_t 结构体变量中的上下文信息重新恢复到 CPU 中并执行。
  • makecontext:修改上下文,给 ucontext_t 上下文指定一个程序入口函数,让程序从该入口函数开始执行。
  • swapcontext:切换上下文,保存当前上下文,并将下一个要执行的上下文恢复到 CPU 中。

具体 ucontext 函数族可以看这篇文章:

https://blog.csdn.net/qq_44443986/article/details/117739157

示例一:

#include <stdio.h>
#include <unistd.h>
#include <ucontext.h>

int main() {
    int i = 0;
    
    ucontext_t ctx;             //定义上下文结构体变量
    getcontext(&ctx);           //获取当前上下文
    
    printf("i = %d\n", i++);
    sleep(1);

    setcontext(&ctx);           //恢复上下文
    return 0;
}

示例二:

#include <iostream>
#include <unistd.h>
#include <ucontext.h>

void foo() {
    printf("this is child context\n");
}

int main() {
    char stack[1024 * 64] = {0};

    ucontext_t child, main;

    // 获取当前上下文
    getcontext(&child);

    // 分配栈空间, uc_stack.ss_sp 指向栈顶
    child.uc_stack.ss_sp = stack;
    child.uc_stack.ss_size = sizeof(stack);
    child.uc_stack.ss_flags = 0;

    // 指定后续的上下文
    child.uc_link = &main;

    //切换到child上下文,保存当前上下文到main
    makecontext(&child, (void (*)(void))foo, 0);

    // 如果设置了后继上下文,foo函数执行结束会返回此处 如果设置为NULL,就不会执行这一步
    swapcontext(&main, &child);

    printf("this is main context\n");
    return 0;
}

4.3 协程框架设计与实现

4.3.1 协程的状态

  1. RT_READY:就绪态
  2. RT_RUNNING:运行态
  3. RT_SUSPEND:挂起态

4.3.2 协程类 Routine

协程的方法:

  1. run:纯虚函数,需要子类来实现协程的具体业务逻辑。
  2. resume:唤醒协程来执行。
  3. yield:当前协程 “放弃” 执行,让调度器调度另一个协程继续执行。
class Routine {
    friend class Schedule;
public:
    enum Status {
        RT_READY = 0,
        RT_RUNNING,
        RT_SUSPEND
    };
    Routine();
    virtual ~Routine();

    virtual void run() = 0;

    void resume();
    void yield();
    int status();

protected:
    static void func(void * ptr);

private:
    ucontext_t m_ctx;
    int m_status;
    char * m_stack;
    int m_stack_size;
    Schedule * m_s;
};

4.3.4 栈增长方向

1706252701674

4.3.5 调度器 Schedule

调度器的方法:

  1. create:初始化协程调度器
  2. append:添加一个新协程到队列
  3. resume:唤醒队列里某个协程来执行
class Schedule {
    friend class Routine;
public:
    Schedule();
    ~Schedule();

    void create(int stack_size = 1024 * 1024);
    void append(Routine * c);
    bool empty() const;
    int size() const;

    void resume();

private:
    ucontext_t m_main;
    char * m_stack;
    int m_stack_size;
    std::list<Routine *> m_queue;
};

4.4 协程框架应用

定义一个 A 线程:a_routine.h

#pragma once
#include <iostream>
#include <routine/routine.h>
using namespace yazi::routine;

class ARoutine : public Routine {
public:
    ARoutine(int * num) : Routine(), m_num(num) {}
    ~ARoutine() {
        if (m_num != nullptr) {
            delete m_num;
            m_num = nullptr;
        }
    }
    virtual void run() {
        for (int i = 0; i < *m_num; i++) {
            std::cout << "a run: num=" << i << std::endl;
            yield();
        }
    }
private:
    int * m_num;
};

main.cpp

#include <iostream>
#include <routine/schedule.h>
#include <test/a_routine.h>
#include <test/b_routine.h>
using namespace yazi::routine;

int main() {
    Schedule s;
    s.create(1024 * 16);

    Routine * a = new ARoutine(5);
    s.append(a);

    Routine * b = new BRoutine(10);
    s.append(b);

    s.run();
    
    return 0;
}

视频讲解:【c++20协程太难用了,我还是自己设计一个吧】 https://www.bilibili.com/video/BV1SR4y1Y7tb/?share_source=copy_web

文章参考:

https://blog.csdn.net/sanmi8276/article/details/111375619

https://blog.csdn.net/weixin_43705457/article/details/106924435

https://hkrb7870j3.feishu.cn/docx/RUMldRX8koIgxIxvAmccy4PYnCc

https://blog.csdn.net/qq_44443986/article/details/117739157

https://blog.csdn.net/tennysonsky/article/details/46046317

https://www.liaoxuefeng.com/wiki/001374738125095c955c1e6d8bb493182103fac9270762a000/0013868328689835ecd883d910145dfa8227b539725e5ed000

https://www.cnblogs.com/work115/p/5620272.html

https://blog.csdn.net/liu251890347/article/details/38509943

https://www.cnblogs.com/fah936861121/articles/8043187.html

http://blog.chinaunix.net/uid-25601623-id-5095687.html

http://t.csdnimg.cn/RolRj

http://t.csdnimg.cn/s6wn0

https://blog.csdn.net/tennysonsky/article/details/46046317

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

惠友小课堂】拇外翻常见的几个误区,来看看你中了几个?

拇外翻作为常见的足部畸形&#xff0c;在日常生活中困扰着许多人。歪脚趾不仅外观不好看&#xff0c;还会出现疼痛、影响行走运动。但大多数人对于拇外翻的认识都不足常常落入认知误区&#xff0c;快来看看你中了几个&#xff1f; 误区一 Q 我都没穿过高跟鞋&#xff0c;怎么也…

宝塔面板SRS音视频TRC服务器启动失败

首先&#xff0c;查找原因 1.先看srs服务在哪 find / -type f -name srs 2>/dev/null运行结果&#xff1a; /var/lib/docker/overlay2/5347867cc0ffed43f1ae24eba609637bfa3cc7cf5f8c660976d2286fa6a88d2b/diff/usr/local/srs/objs/srs /var/lib/docker/overlay2/5347867…

安装vue devtools及常见问题

vue devtools 下载百度网盘下载极简插件下载github下载 安装常见问题 下载 以下三种方式选择一种即可。 百度网盘下载 下载链接&#xff1a;https://pan.baidu.com/s/1ktNIarB-zXz2ij0pda6IQw?pwdv6d3 推荐方式。网盘中包含vue2和vue3的devtools安装工具&#xff0c;根据项目…

机器学习_常见算法比较模型效果(LR、KNN、SVM、NB、DT、RF、XGB、LGB、CAT)

文章目录 KNNSVM朴素贝叶斯决策树随机森林 KNN “近朱者赤&#xff0c;近墨者黑”可以说是 KNN 的工作原理。 整个计算过程分为三步&#xff1a; 计算待分类物体与其他物体之间的距离&#xff1b;统计距离最近的 K 个邻居&#xff1b;对于 K 个最近的邻居&#xff0c;它们属于…

Java工程师的你,真的不想了解一下《JVM垃圾回收详解》吗?(重点)

Java工程师的你,真的不想了解一下《JVM垃圾回收详解》吗?(重点) 文章目录 Java工程师的你,真的不想了解一下《JVM垃圾回收详解》吗?(重点)前言堆空间的基本结构内存分配和回收原则对象优先在 Eden 区分配大对象直接进入老年代长期存活的对象将进入老年代主要进行 gc 的…

【简单易懂】Java中字符的输入

目录 Java中字符的输入 1. 读取单个字符&#xff1a; 2. 读取整行字符&#xff1a; 3. 读取多个字符&#xff1a; Java中字符的输入 当涉及到在Java中获取字符输入时&#xff0c;可能会涉及不同的情况&#xff0c;包括读取单个字符、读取整行字符等。下面&#xff0c;我将…

Java Web(五)--DOM

介绍 DOM 全称是 Document Object Model 文档对象模型&#xff1b; DOM 是 W3C&#xff08;万维网联盟&#xff09;的标准。 DOM 定义了访问 HTML 和 XML 文档的标准&#xff1a; "W3C 文档对象模型 &#xff08;DOM&#xff09; 是中立于平台和语言的接口&#xff0…

Java项目:SSM框架基于spring+springmvc+mybatis实现的心理预约咨询管理系统(ssm+B/S架构+源码+数据库+毕业论文)

一、项目简介 本项目是一套ssm823基于SSM框架的心理预约咨询管理系统&#xff0c;主要针对计算机相关专业的正在做毕设的学生与需要项目实战练习的Java学习者。 包含&#xff1a;项目源码、数据库脚本等&#xff0c;该项目附带全部源码可作为毕设使用。 项目都经过严格调试&am…

Jenkins配置SSH Server连接远程服务器

前言 我们需要配置远程服务器SSH Server&#xff0c;才可以通过jenkins登录到你想进入的那台服务器里面&#xff0c;执行指令操作 前提&#xff1a; 首先我们要先安装Publish Over SSH插件&#xff0c;然后再配置我们需要登录的远程服务器信息 我们可以在插件管理查询是否已…

芯片设计行业(EDA)数据管理解决方案

IC 产业持续发展&#xff0c;成为我国数字经济的基石 终端需求强劲&#xff1a; 市场发展趋势显示&#xff0c;集成电路&#xff08;integrated circuit&#xff09;市场正在加速向中国迁移&#xff0c;市场格局加快调整&#xff0c;云计算、物联网、智能制造&#xff0c;大数…

牛刀小试 - C++ 推箱子小游戏

参考文档 C笔记&#xff1a;推箱子小游戏 copy函数 memcpy()函数用法&#xff08;可复制数组&#xff09; 使用memcpy踩出来的坑&#xff0c;值得注意 完整代码 /********************************************************************* 程序名:推箱子小游戏 说明&#x…

类和对象 第三部分第三小节:const修饰成员函数

一.常函数&#xff1a; &#xff08;一&#xff09;成员函数后面加const后我们成这个函数为常函数 &#xff08;二&#xff09;常函数内不可以修改成员函数属性 额外补充&#xff1a; this指针的本质&#xff0c;是指针常量&#xff0c;指针指向的是不可以修改的 但是指针指向的…

DB2数据库基本介绍

文章目录 一、DB2数据库1、简介2、特点&#xff08;来自alai&#xff09;3、DB2 Connect4、下载使用5、使用场景 参考文章 一、DB2数据库 百度 1、简介 DB2是IBM一种分布式数据库解决方案。说简单点&#xff1a;DB2就是IBM开发的一种大型关系型数据库平台。它支持多用户或应用…

MIT_线性代数笔记:线性代数常用概念及术语总结

目录 1.系数矩阵2.高斯消元法3.置换矩阵 Permutation4.逆矩阵 Inverse5.高斯-若尔当消元法6.矩阵的 LU 分解7.三角矩阵8.正定矩阵 1.系数矩阵 线性代数的基本问题就是解 n 元一次方程组。例如&#xff1a;二元一次方程组 2 x − y 0 − x 2 y 3 \begin{align*} & 2x -…

数字图像处理(入门篇)十五 OpenCV-Python计算和绘制图像的二维直方图

目录 1 方案 2 实践 彩色图像有三个通道,分别是红、绿和蓝,可以同时为两个颜色通道计算二维直方图,这个过程可以使用 cv2.calcHist() 函数来实现。 1 方案 ①导入依赖库 import cv2 import matplotlib.pyplot as plt ②读取输入图片 <

C语言第八弹---一维数组

✨个人主页&#xff1a; 熬夜学编程的小林 &#x1f497;系列专栏&#xff1a; 【C语言详解】 【数据结构详解】 一维数组 1、数组的概念 2、⼀维数组的创建和初始化 2.1、数组创建 2.2、数组的初始化 2.3、数组的类型 3、⼀维数组的使用 3.1、数组下标 3.2、数组元素…

23款奔驰GLE350升级小柏林音响 无损音质效果

小柏林之声音响是13个喇叭1个功放&#xff0c;功率是590W&#xff0c;对应普通音响来说&#xff0c;已经是上等了。像著名的哈曼卡顿音响&#xff0c;还是丹拿音响&#xff0c;或者是BOSE音响&#xff0c;论地位&#xff0c;论音质柏林之声也是名列前茅。星骏汇小许Xjh15863 升…

微信小程序(十六)slot插槽

注释很详细&#xff0c;直接上代码 上一篇 温馨提醒&#xff1a;此篇需要自定义组件的基础&#xff0c;如果不清楚请先看上一篇 新增内容&#xff1a; 1.单个插槽 2.多个插槽 单个插糟 源码&#xff1a; myNav.wxml <view class"navigationBar custom-class">…

【Unity3D日常开发】Unity3D中UGUI的Text、Dropdown输入特殊符号

推荐阅读 CSDN主页GitHub开源地址Unity3D插件分享简书地址我的个人博客 大家好&#xff0c;我是佛系工程师☆恬静的小魔龙☆&#xff0c;不定时更新Unity开发技巧&#xff0c;觉得有用记得一键三连哦。 一、前言 在开发中会遇到需要显示特殊符号的情况&#xff0c;比如上标、…

今天来看看工商业储能收益模式有哪些

安科瑞武陈燕acrelcy 2023 年有望成为工商业储能的发展元年&#xff0c;主要原因2023年工商业储能的经济性有望大幅提升。工商业储能下游主要为工商业企业&#xff0c;投资是否具有经济性是工商业需求的核心因素之一&#xff0c;而2023年工商业储能经济性或将显著提升&#xf…
最新文章