Sylar C++高性能服务器学习记录09 【协程调度模块-知识储备篇】

早在19年5月就在某站上看到sylar的视频了,一直认为这是一个非常不错的视频,由于本人一直是自学编程,基础不扎实,也没有任何人的督促,没能坚持下去,每每想起倍感惋惜。恰逢互联网寒冬,在家无事,遂提笔再续前缘。

为了能更好的看懂sylar,本套笔记会分两步走,每个系统都会分为两篇博客。
分别是【知识储备篇】和【代码分析篇】
(ps:纯粹做笔记的形式给自己记录下,欢迎大家评论,不足之处请多多赐教)
QQ交流群:957100923


协程调度模块-代码分析篇

一、Schedule调度器是什么【线程级别的调度器】

我的博客已经停更一周了,并不是 五一 假期出去游玩导致的。
原因很简单:就是因为 sylar 视频中协程调度相关的知识过于复杂,导致自己反复观看了数遍才勉强理解一二。
要搞懂协程调度模块一上来就看sylar的视频或代码未免门槛太高了。
我总结了一下这部分知识为什么难以下咽

  1. 知识点过多,将调度器、线程、协程糅合在一起讲解,原本基础就不扎实,这样一锅乱炖容易糊掉。
  2. 想法没有提前告知,不知道要做什么,在做什么,雾里看花。
  3. 弹幕的误导,sylar-yin 确实会有说错写错的地方,如果你不懂,那么请你百分百遵循他的写法。
  4. 不够坚持,不要放过每一个你不熟悉的知识点,请暂停视频,把不会的知识点了解了再往下(不要过分深入)。

本人属于不太聪明的类型,所以如果你和我一样,那么请稳扎稳打,别人学一天我们就学三天,别人学一个月,我们就学一年。


1.最普通的多线程使用

首先抛开协程,我们想要使用多线程来完成多个任务需要怎么做呢?
下面我举个例子(该案例中会开启三个子线程来分别执行 唱歌、跳舞、说唱):

#include <chrono>
#include <iostream>
#include <pthread.h>
#include <vector>
#include <list>
#include <unistd.h>
#include <functional>
#include "sylar/thread.h"
#include "sylar/mutex.h"//测试用的全局累加计数字段
int num = 0;
//循环累加的次数
uint64_t loop_times = 100000000;typedef sylar::Mutex MutexType;
MutexType g_mutx;//唱歌的方法,会专门分配一个线程来处理
void sing(){MutexType::Lock lock(g_mutx);for(size_t i = 0; i < loop_times; ++i){++num;}std::cout << sylar::Thread::GetName() << " sing~ " << num << std::endl;
}//跳舞的方法,会专门分配一个线程来处理
void dance(){MutexType::Lock lock(g_mutx);for(size_t i = 0; i < loop_times; ++i){++num;}std::cout <<sylar::Thread::GetName() << " dance~ " << num << std::endl;   
}//说唱的方法,会专门分配一个线程来处理
void rap(){MutexType::Lock lock(g_mutx);for(size_t i = 0; i < loop_times; ++i){++num;}std::cout <<sylar::Thread::GetName() << " rap~ " << num << std::endl;   
}int main(){std::cout << "====Main start====" << std::endl;{//记录当前时间auto start = std::chrono::high_resolution_clock::now();//创建线程用于执行唱歌的方法sylar::Thread::ptr thr1(new sylar::Thread(&sing,"THREAD_1"));//创建线程用于执行跳舞的方法sylar::Thread::ptr thr2(new sylar::Thread(&dance,"THREAD_2"));//创建线程用于执行说唱的方法sylar::Thread::ptr thr3(new sylar::Thread(&rap,"THREAD_3"));thr1->join();thr2->join();thr3->join();//记录结束时间auto end = std::chrono::high_resolution_clock::now();//计算花费的时间std::chrono::duration<double,std::milli> elapsed = end - start;std::cout << "Elapsed: " << (int)elapsed.count() << std::endl;}std::cout << "====Main end====" << std::endl;return 0;
}

以下是输出(可以看到三个线程按顺序执行了):

===Main start===
THREAD_1 sing~ 100000000
THREAD_2 dance~ 200000000
THREAD_3 rap~ 300000000
Elapsed: 294
===Main end====

2.复用多线程

上述的代码中,我们有三个任务(唱歌、跳舞、说唱),同时我们开辟了三个线程来执行。
但是很多情况下我们的线程数量是需要控制在适当范围内的。

最佳线程数目 = ((线程等待时间+线程CPU时间)/线程CPU时间 )* CPU数目CPU核数为4核,一个任务线程cpu耗时为20ms,线程等待(网络IO、磁盘IO)耗时80ms,
那最佳线程数目:( 80 + 20 )/20 * 4 = 20。也就是设置20个线程数最佳。线程的等待时间越大,线程数就要设置越大
1、CPU密集型:操作内存处理的业务,一般线程数设置为:CPU核数 + 1 或者 CPU核数*2。核数为4的话,一般设置 5 或 8
2、IO密集型:文件操作,网络操作,数据库操作,一般线程设置为:cpu核数 / (1-0.9),核数为4的话,一般设置 40

我的机器是1核的,我这里其实适合使用两个线程,但是我在上述代码中使用了三个线程,那么多线程反而变成了低效的。
所以我们可以复用其中一个线程,在该线程执行完成任务后再执行其他的任务。
代码如下(改动很少,改动部分用 !!!标注了):

int main(){std::cout << "====Main start====" << std::endl;{//记录当前时间auto start = std::chrono::high_resolution_clock::now();//创建线程用于执行唱歌的方法sylar::Thread::ptr thr1(new sylar::Thread(&sing,"THREAD_1"));//创建线程用于执行跳舞的方法sylar::Thread::ptr thr2(new sylar::Thread(&dance,"THREAD_2"));thr1->join();thr2->join();//!!!复用线程用于执行说唱的方法!!!thr1.reset(new sylar::Thread(&rap,"THREAD_1"));thr1->join();//记录结束时间auto end = std::chrono::high_resolution_clock::now();//计算花费的时间std::chrono::duration<double,std::milli> elapsed = end - start;std::cout << "Elapsed: " << (int)elapsed.count() << std::endl;}std::cout << "====Main end====" << std::endl;return 0;
}

以下是输出:

===Main start====
THREAD_1 sing~ 100000000
THREAD_2 dance~ 200000000
THREAD_1 rap~ 300000000
Elapsed: 265
==Main end====

问题:
问题来了,以上的两份代码都存在以下问题:
1.就是需要程序员自己来控制线程的创建。
2.需要程序员手动来进行线程的调度。
3.需要程序员手动的去复用一个线程对象。
4.需要程序员手动的join对应的每一个线程。
这样是很繁琐的事情,而且一不小心就会出错,那么我们有什么好的办法吗?


3.调度器的出现

想要解决以上代码带来的问题,我们就要提出需求:
1.将创建线程的事情交由【调度器】执行,我们只需创建调度器时指定线程数量即可。
2.将线程调度和复用线程的事情交由【调度器】执行,我们只需提供所需执行的 任务 即可。
3.等待各个线程完成的jion()方法也由【调度器】执行,我们只需等待调度器结束即可。

综上所述,我们给出以下代码(记得先看main方法中的调度器使用,再看实现):

#include <chrono>
#include <iostream>
#include <pthread.h>
#include <vector>
#include <list>
#include <unistd.h>
#include <functional>
#include "sylar/thread.h"
#include "sylar/mutex.h"//测试用的全局累加计数字段
int num = 0;
//循环累加的次数
uint64_t loop_times = 100000000;//唱歌的方法,会专门分配一个线程来处理
void sing(){for(size_t i = 0; i < loop_times; ++i){++num;}std::cout << sylar::Thread::GetName() << " sing~ " << num << std::endl;
}//跳舞的方法,会专门分配一个线程来处理
void dance(){for(size_t i = 0; i < loop_times; ++i){++num;}std::cout <<sylar::Thread::GetName() << " dance~ " << num << std::endl;   
}//说唱的方法,会专门分配一个线程来处理
void rap(){for(size_t i = 0; i < loop_times; ++i){++num;}std::cout <<sylar::Thread::GetName() << " rap~ " << num << std::endl;   
}//调度器
class Schedule {
public://定义锁,使用typedef方便锁类型替换typedef sylar::Mutex MutexType;//调度器构造函数,指定线程池的大小Schedule(size_t size = 1):m_poll_size(size){std::cout << "Specify a thread poll size of "<< size << std::endl;}//调度器析构函数~Schedule(){}//调度方法void run(){//多个线程操作任务队列,涉及到线程安全,所以要加锁MutexType::Lock lock(m_mutex);//定义一个任务对象来存放后续从任务队列中取出的任务std::function<void()> task;//开始对任务队列进行迭代auto it = m_tasks_queue.begin();while(it != m_tasks_queue.end()){//将任务从任务队列中取出task = *it;//将取出的任务从任务队列中擦除m_tasks_queue.erase(it);//每次按顺序取出一个,取出后跳出循环break;}//判断是否有取到任务if(task != nullptr){//有取到任务则执行任务task();}}//开启调度器方法void start(){std::cout << "Schedule start..." << std::endl;std::cout << "Init a thread poll" << std::endl;//根据调度器初始化时指定的线程池大小,来初始化线程池容器的大小 m_thread_poll.resize(m_poll_size);std::cout << "Schedule run..." << std::endl;//构建线程池中的每一个线程对象,每个线程都将绑定调度器的run方法,且给每个线程对象指定了名称用于区分for(size_t i = 0; i < m_poll_size; ++i){//这里的reset方法是为了复用线程对象节约内存资源m_thread_poll[i].reset(new sylar::Thread(std::bind(&Schedule::run,this),"THREAD_"+std::to_string(i)));}}//停止调度器方法void stop(){//判断任务队列中的任务是否全部被消化while(!m_tasks_queue.empty()){for(size_t i = 0; i < m_poll_size; ++i){m_thread_poll[i].reset(new sylar::Thread(std::bind(&Schedule::run,this),"THREAD_"+std::to_string(i)));}}//如果任务队列中的任务全部被消化了,那么就在这等待每个子线程的结束for(size_t i = 0; i < m_poll_size; ++i){m_thread_poll[i]->join();}std::cout << "Schedule stop..." << std::endl; }//接受调度任务方法void schedule(std::function<void()> task){std::cout << "Push task into queue " << &task << std::endl;//将任务对象依次存放到调度器的任务队列中m_tasks_queue.push_back(task);}private://由于是多线程环境下所以提供锁MutexType m_mutex;//线程池的大小size_t m_poll_size = 0;//线程池std::vector<sylar::Thread::ptr> m_thread_poll;//任务队列std::list<std::function<void()> > m_tasks_queue;      
};int main(){std::cout << "====Schedule====" << std::endl;std::cout << "====Main start====" << std::endl;{//记录当前时间auto start = std::chrono::high_resolution_clock::now();//构建调度器,指定线程池中线程数量Schedule sc(2);//依次将要执行的任务塞入调度器sc.schedule(std::bind(&sing));sc.schedule(std::bind(&dance));sc.schedule(std::bind(&rap));//执行调度器sc.start();//停止调度器sc.stop();//记录结束时间auto end = std::chrono::high_resolution_clock::now();//计算花费的时间std::chrono::duration<double,std::milli> elapsed = end - start;std::cout << "Elapsed: " << (int)elapsed.count() << std::endl;}std::cout << "====Main end====" << std::endl;return 0;
}

以下是输出:

===Schedule===
===Main start====
Push task into queue  0x7ffd602ffb00
Push task into queue  0x7ffd602ffb40
Push task into queue  0x7ffd602ffb80
Schedule start...
Init a thread poll
Schedule run...
THREAD_1 sing~ 100000000
THREAD_2 dance~ 200000000
THREAD_1 rap~ 300000000
Schedule stop...
Elapsed: 234
===Main end====

可以看到以上的代码使用了调度器,代码的编写会很舒服,以下是使用调度器代码的部分:

//构建调度器,指定线程池中线程数量
Schedule sc(2);
//依次将要执行的任务塞入调度器
sc.schedule(std::bind(&sing));
sc.schedule(std::bind(&dance));
sc.schedule(std::bind(&rap));
//执行调度器
sc.start();
//停止调度器
sc.stop();

这样一来,无论是有多少任务需要执行,程序员都不需要关系如何创建线程和如何进行调度了,
只需要把所要执行的任务全部一股脑的交给调度器就行了。
线程也不需要自己来维护了,只需要告诉调度器需要多少个线程就行。
只要调度器完善的够好,程序员就只需要专心处理业务问题就好了。

看到这里相信你对调度器的功能和必要性有了一定的了解,那么我们来继续优化这个调度器,让它更健硕。


二、考虑多线程下的多个调度器问题【线程级别的调度器】

  1. 由于上边代码调度器Schedule类可以在其他任意线程中构建与执行,所以需要考虑多线程下调度器的区分问题。
  2. 由于这里为了由浅入深,所以没有使用协程,那么调度器就必须占用一个线程来执行。

接下来的场景会变成:一个调度器线程和多个其他的线程和一系列的函数任务。
所以需要使用线程局部变量来控制一个线程只存在一个调度器
当然这个判断需要将调度器的this存储起来做比较

以下是控制一个线程只存在一个调度器的核心逻辑

static thread_local Schedule* t_schedule = nullptr;Schedule(size_t size = 1):m_poll_size(size){//断言当前线程是否不存在调度器,如果已存在那么报错SYLAR_ASSERT2(GetThis()==nullptr,"one thread one schedule");//设置当前线程上的调度器为当前调度器setThis();
}~Schedule(){//如果当前调度器就是对应线程上的调度器那么将线程上的调度器清空if(GetThis()==this){t_schedule = nullptr;}
}static Schedule* GetThis(){return t_schedule;
}void setThis(){t_schedule = this;
}

例子(完整代码):

#include <chrono>
#include <iostream>
#include <pthread.h>
#include <vector>
#include <list>
#include <unistd.h>
#include <functional>
#include "sylar/log.h"
#include "sylar/thread.h"
#include "sylar/mutex.h"
#include "sylar/macro.h"int num = 0;
uint64_t loop_times = 100000000;void sing(){for(size_t i = 0; i < loop_times; ++i){++num;}std::cout << sylar::Thread::GetName() << " sing~ " << num << std::endl;
}void dance(){for(size_t i = 0; i < loop_times; ++i){++num;}std::cout <<sylar::Thread::GetName() << " dance~ " << num << std::endl;   
}void rap(){for(size_t i = 0; i < loop_times; ++i){++num;}std::cout <<sylar::Thread::GetName() << " rap~ " << num << std::endl;   
}class Schedule;static thread_local Schedule* t_schedule = nullptr;class Schedule {
public:typedef sylar::Mutex MutexType;Schedule(size_t size = 1):m_poll_size(size){SYLAR_ASSERT2(GetThis()==nullptr,"one thread one schedule");setThis();m_callerThread = sylar::Thread::GetThis();}~Schedule(){if(GetThis()==this){t_schedule = nullptr;}}static Schedule* GetThis(){return t_schedule;}void setThis(){t_schedule = this;}void run(){MutexType::Lock lock(m_mutex);std::function<void()> task;auto it = m_tasks_queue.begin();while(it != m_tasks_queue.end()){task = *it;m_tasks_queue.erase(it);break;}if(task != nullptr){task();}}void start(){std::cout << "Schedule start..." << std::endl;std::cout << "Init a thread poll" << std::endl;m_thread_poll.resize(m_poll_size);std::cout << "Schedule run..." << std::endl;for(size_t i = 0; i < m_poll_size; ++i){m_thread_poll[i].reset(new sylar::Thread(std::bind(&Schedule::run,this),"THREAD_"+std::to_string(i)));}}void stop(){while(!m_tasks_queue.empty()){for(size_t i = 0; i < m_poll_size; ++i){m_thread_poll[i].reset(new sylar::Thread(std::bind(&Schedule::run,this),"THREAD_"+std::to_string(i)));}}for(size_t i = 0; i < m_poll_size; ++i){m_thread_poll[i]->join();}std::cout << "Schedule stop..." << std::endl; }void schedule(std::function<void()> task){std::cout << "Push task into queue " << &task << std::endl;m_tasks_queue.push_back(task);}private:MutexType m_mutex;size_t m_poll_size = 0;std::vector<sylar::Thread::ptr> m_thread_poll;std::list<std::function<void()> > m_tasks_queue;      sylar::Thread* m_callerThread = 0;
};//第二个调度线程
void subThread(){std::cout << "Sub Schedule Thread" << std::endl;Schedule sc(1);sc.schedule(std::bind(&rap));sc.start();sc.stop();
}int main(){std::cout << "====Schedule====" << std::endl;std::cout << "====Main start====" << std::endl;{auto start = std::chrono::high_resolution_clock::now();Schedule sc(2);sc.schedule(std::bind(&subThread));sc.schedule(std::bind(&dance));// sc.schedule(std::bind(&rap));sc.start();sc.stop();auto end = std::chrono::high_resolution_clock::now();std::chrono::duration<double,std::milli> elapsed = end - start;std::cout << "Elapsed: " << (int)elapsed.count() << std::endl;}std::cout << "====Main end====" << std::endl;return 0;
}

可以看到,调度器可以在任何一个线程中使用,互不影响。


三、加入协程的概念【协程级别的调度器】

一旦加入协程的概念,那么调度器的复杂度就会上升一个层次。
首先要理清楚有哪些东西参与到这个调度器中。
其次要统一称呼,视频中就是很多称呼不明确导致看不懂。
需要理清楚以下几个点:

  1. 加入协程的概念后,调度器就不需要占用一个线程来专门做调度,而是退居到某个线程下的 协程 中去。
  2. 一旦退居到某个协程中,那么该调度器所在的协程我们叫做 【调度协程】
  3. 【调度协程】 所在的线程我称它为 【调度线程】
  4. 在一个线程中会有一个 【主协程】 ,要记住 【调度协程】 可以是 【主协程】 也可能不是(调度线程是主线程时必然不是)。
  5. 在多线程中会有一个 【主线程】,要记住 【调度线程】 可以是 【主线程】 也可以不是。
  6. 调度任务会变成两种类型:【函数任务】、【协程任务】,这里可以把他们包装到一起,统称 【任务】
  7. 【函数任务】最终可以包装成【协程任务】,这样统一调用方式。
  8. 我们封装的协程对象会有很多状态,所以在调度器中需要有比较复杂的判断(这个需要看代码了)。

四、总结

1.要看懂协程调度必须拆分着看,辩证的看。
2.由于协程调度的IO协程篇还没看完,所以存在几个遗留方法,这样无法看到全貌,可以理解大概流程后继续。
3.作者代码中确实有不足的地方,需要先内心赞同他的一切做法,当自己完全看懂后再做修改(比如Fiber中有Schedule的耦合代码)。
4.本人还没完全看完协程调度相关的视频,在看完IO协程调度后可能会回过头来修改这部分的博客内容。

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

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

相关文章

【挑战30天首通《谷粒商城》】-【第一天】03、简介-分布式基础概念

文章目录 课程介绍 ( 本章了解即可&#xff0c;可以略过)1、微服务简而言之: 2、集群&分布式&节点2.1、定义2.2、示例 3、远程调用4、负载均衡常见的负裁均衡算法: 5、服务注册/发现&注册中心6、配置中心7、服务熔断&服务降级7.1、服务熔断7.2、服务降级 8、AP…

网络工程师-----第二十天

1、计算机发展&#xff1a; 早期计算工具&#xff1a;小石头 算筹、算盘、计算尺 机械计算机与机电计算机&#xff1a; 电子计算机&#xff1a; ①1943年 EDVAC计算机 ②1946年 ENIAC计算机 ※ENIAC&#xff1a;世界上第一台计算机是ENIAC&#xff08;1946年&#xff09;&a…

【MySQL】3.MySQL核心概念解析:数据完整性、事务处理、索引及聚簇索引与非聚簇索引

探索MySQL的内部机制&#xff0c;理解数据完整性、事务处理、索引策略以及聚簇索引与非聚簇索引的区别是至关重要的。这些概念构成了数据库设计和优化的基础&#xff0c;对于确保数据的准确性、提高查询效率、维护数据的一致性和实现复杂的数据库操作至关重要。本文将逐一剖析这…

短视频矩阵系统源码saas开发--可视化剪辑、矩阵托管、多功能合一开发

短视频矩阵系统源码saas开发&#xff08;可视化剪辑、矩阵托管、智能私信聚合、线索转化、数据看板、seo关键词、子账号等多个板块开发&#xff09; 短视频矩阵系统是一种集成了多种功能的系统&#xff0c;旨在帮助用户在短视频平台上进行高效的内容创作、管理和发布。根据您提…

C++ vector的使用

C中的vector是一个动态数组&#xff0c;它提供了一种灵活的方式来存储和操作元素集合。vector是C标准模板库&#xff08;STL&#xff09;的一部分&#xff0c;它允许你添加、删除、排序和搜索元素。以下是一些基本的vector操作&#xff1a; 声明和初始化: std::vector<int&g…

复习python面向对象

复习python面向对象 1.面向对象定义2.类特殊方法3.封装property装饰器 4.继承多重继承 5.多态6.属性和方法 1.面向对象定义 在讲面向对象之前&#xff0c;先来看看面向过程。 面向过程&#xff1a;将一个功能分解成一个一个小的步骤&#xff0c;通过完成一个个步骤来完成一个程…

Vue 中 $nextTick 的作用是什么?

目录 一、NextTick是什么 为什么要有nexttick 二、使用场景 三、实现原理 一、NextTick是什么 官方对其的定义 在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法&#xff0c;获取更新后的 DOM 什么意思呢&#xff1f; 我们可以理解成&#xff0c…

陪诊系统|陪诊小程序成品|陪诊系统功能

随着人们对健康的日益关注以及医疗技术的不断进步&#xff0c;陪诊小程序应运而生&#xff0c;通过提供陪同就医、医疗服务和健康管理等功能为患者和家庭成员提供了更多的便利和选择。本文将分析陪诊小程序的关键功能&#xff0c;以便更好地理解其在医疗领域的作用。 在陪诊小程…

闲鱼最新暴力玩法,靠低价渠道单日收益1000+,附详细实操及渠道

详情介绍 今天给大家分享的是最近非常爆火的&#xff0c;外面收费998的各大会员低价渠道项目&#xff0c;购买实操之后发现这个项目确实盈利&#xff0c;而且收益也不错&#xff0c;并把主要的低价渠道获取方式提供给大家

【C语言】动态分配内存

内存的五大分区 1、堆区&#xff08;heap&#xff09;——由程序员分配和释放&#xff0c; 若程序员不释放&#xff0c;程序结束时一般由操作系统回收。注意它与数据结构中的堆是两回事 2、栈区&#xff08;stack&#xff09;——由编译器自动分配释放 &#xff0c;存放函数的…

【2024最新华为OD-C卷试题汇总】字符串分割(100分) - 三语言AC题解(Python/Java/Cpp)

&#x1f36d; 大家好这里是清隆学长 &#xff0c;一枚热爱算法的程序员 ✨ 本系列打算持续跟新华为OD-C卷的三语言AC题解 &#x1f4bb; ACM银牌&#x1f948;| 多次AK大厂笔试 &#xff5c; 编程一对一辅导 &#x1f44f; 感谢大家的订阅➕ 和 喜欢&#x1f497; 文章目录 前…

智能家居2 -- 实现网络控制模块

这一模块的思路和前面的语言控制模块很相似&#xff0c;差别只是调用TCP 去控制 废话少说&#xff0c;放码过来 增添/修改代码 socket_interface.c #include <pthread.h>#include "socket_interface.h" #include "control.h" #include "socke…

由北京车展想到的,技术红利时代的“重启”

北京车展刚刚落幕&#xff0c;雷军和周鸿祎成为网红&#xff0c;国产品牌站上王座。与此同时&#xff0c;马斯克“光速”访华&#xff0c;FSD酝酿入华再掀新竞争。华为在车展前发布的智驾新品牌“乾崑”&#xff0c;同样在现场广受关注。它们的精彩&#xff0c;让燃油车羡慕。 …

【Vue】pinia

pinia 官网:https://pinia.vuejs.org/zh/ 搭建 pinia 环境 第一步&#xff1a;npm install pinia --save 第二步&#xff1a;操作src/main.ts import { createApp } from vue import App from ./App.vue/* 引入createPinia&#xff0c;用于创建pinia */ import { createP…

新一代智慧音视频平台,企业必备新基建

随着5G、云计算、实时音视频、多模态、大模型、数字人等前沿技术的发展&#xff0c;企业与客户的交互方式正加速趋于移动化、视频化。 国家有关部门也相继出台系列政策法规&#xff0c;确保线上业务安全合规&#xff0c;以保障消费者权益。如&#xff0c;针对保险、银行、证券…

C#语言进阶(四) 枚举器和迭代器

总目录 C# 语法总目录 枚举器和迭代器 枚举器和迭代器1. 枚举器2. 可枚举对象3. 迭代器4. 迭代器语义5. yield break 语句6. 组合序列 枚举器和迭代器 1. 枚举器 枚举器(Enumerator)是一个只读的且只能在值序列上前移的游标。 任何具有MoveNext方法和Current属性的对象都被称…

STM32快速入门(定时器之输入捕获)

STM32快速入门&#xff08;定时器之输入捕获&#xff09; 前言 本节主要讲解STM32利用通用定时器&#xff0c;在输入引脚出现指定电平跳变时&#xff0c;将CNT的值锁存到CCR寄存器当中&#xff0c;从而计算PWM波形的频率、占空比、脉冲间隔、电平持续时间等。其功能的应用有&…

QT_BEGIN_NAMESPACE

最近碰到了QT_BEGIN_NAMESPACE这个宏&#xff0c;这个宏就是一个命名空间&#xff0c;意思是如果不用这个宏&#xff0c;可能我qwidget定义的一个变量a会和标准C定义的变量a冲突对不&#xff0c;Qt通过这个命名空间&#xff0c;将所有类和函数封装在一个作用域里&#xff0c;防…

浅谈自己用过最好用的AI工具概括

个人最经常用的AI工具的其实是Copilot&#xff0c;但是也有别的一些最好用的AI工具&#xff0c;包括&#xff1a; OpenAI GPT-3&#xff1a;这是一个自然语言生成模型&#xff0c;具有强大的语言理解和生成能力。它可以用于各种任务&#xff0c;如文字生成、自动回复和文本摘要…

brpc profiler

cpu profiler cpu profiler | bRPC MacOS的额外配置 在MacOS下&#xff0c;gperftools中的perl pprof脚本无法将函数地址转变成函数名&#xff0c;解决办法是&#xff1a; 安装standalone pprof&#xff0c;并把下载的pprof二进制文件路径写入环境变量GOOGLE_PPROF_BINARY_PA…