首页
烟花
归档
随笔
聚合
部落格精华
部落格资讯
CSS教程
CSS3教程
HTML教程
HTML5教程
建站经验
网站优化
资讯
今日早报
资讯日历
科技新闻
今天
罗盘
网盘
友链
留言
1
「今日早报」 2025年5月12日, 农历四月十五, 星期一
2
「今日早报」 2025年5月11日, 农历四月十四, 星期日
3
「今日早报」 2025年5月10日, 农历四月十三, 星期六
4
「今日早报」 2025年5月9日, 农历四月十二, 星期五
5
「今日早报」 2025年5月8日, 农历四月十一, 星期四
沙漠渔
把過去的累積,善用到當下
累计撰写
2,466
篇文章
累计创建
385
个标签
累计收到
994
条评论
栏目
首页
烟花
归档
随笔
聚合
部落格精华
部落格资讯
CSS教程
CSS3教程
HTML教程
HTML5教程
建站经验
网站优化
资讯
今日早报
资讯日历
科技新闻
今天
罗盘
网盘
友链
留言
搜索
标签搜索
代理服务
winsw
override
VMware
api
popai
拉取镜像
人工智能
copilot
chatgpt
openai
coze
objdump
ldd
日志
版本
latest
批处理
bat
节能模式
iwconfig
排序
du
设计模式
hostname
面板
cockpit
版本不兼容
npm
统计
烟花
新春
leveldb
Java heap space
堆内存
harbor
utf8mb4
网络聚合
IPV6
nmtui
测速
带宽
千兆
路由器
nmcli
nmlci
orangepi
motd
中文乱码
webdav
香橙派
代码折叠
享元模式
单例模式
解锁
锁定
无法安装
Xshell
并发编程
ScheduledThreadPoolExecutor
ThreadPoolExecutor
线程池
Fock-Join
并发
ExecutorService
nextcloud
alist
panic
hung task
时间戳
ping
tail
dd
嵌入式
点灯
mount
共享
NFS
curl
全屏
ChannelOption
C++
comparator
桥头堡
开源
varchar
char
StringBuilder
StringBuffer
String
命令行工具
网络配置
netsh
建议
专家
离谱
2022
设备管理器
虚拟网卡
环回适配器
安全
攻击
CC
DDOS
跳槽
过年
年假
春节
2023
优站计划
鼻塞
咳嗽
大号流感
阳
新冠病毒
循环冗余校验
CRC-16/XMODEM
crc校验
stream
DMZ主机
域名解析
CDN
七牛云
DDNS
类加载器
双亲委派
加载机制
删除
搜索
变化时间
修改时间
访问时间
响应异常
超时
jsoup
用法详解
压缩命令
打包压缩
zip
优缺点
打包
数据压缩
密码
sudoers
索引量
权重
瞬间
魔鬼洞
沙漠鱼
沙漠渔
面试
引用传递
值传递
即时编译器
机器码
JIT
旗舰版
ISO
原版镜像
win7
参数
配置文件
8小时
时间错误
报告
coverity
jetbrains
idea
谷歌翻译
rest
403
ERR_UNKNOWN_URL_SCHEME
DevTools
优先级
location
FTP
挂载
curlftpfs
提示
自定义参数
springboot
配置
死锁
超级密码
桥接
联通光猫
Dockerfile
构建
命令行
eclipse
光猫
路由模式
桥接模式
bash-4.2
sudo
oracle帐号
JDK下载
百度收录
定时发布
哨兵模式
内存管理
JVM
登录
免密
不一致
服务器时间
BIO,NIO,AIO
ByteBuffer
FileWriter
BufferedOutputStream
流水线
pipeline
环境变量
重启
任务消失
jenkins
卸载
snap
20.04
Ubuntu
容器
大数据传输
InputStream
学习笔记
CSS
最佳实践
DOM操作
原型链
原型
No such file or Directory
SSH配置
SSH免密
tar
命令
网站优化
取消快照
百度快照
全局配置
修改密码
控制台
gitlab
解码器
Netty
文件传输
sz
rz
Jar
打铁花
羊毛沟
容器日志
docker
规范
博客
rejected
Gerrit
wireshark
代理
Go
http-server
nodejs
package-lock.json
996加班
删除标签
钟薛高
60s新闻接口
每日新闻接口
百度福利
自动回复
localstorage
QQ机器人
redis
近乎
客户端
JuiceSSH
乔恩
加菲猫
新浪邮箱
joe
公告
父亲节
图标
每日新闻
51la
服务器
腾讯云
插件
罗永浩
编码
avatar
小虎墩大英雄
端午劫
端午节
中石化
中国石化
儿童节
win10
激活
shell
金门大桥
旧金山
产品经理
免疫力
熬夜
云旅游
云游
招聘
海尔
halo
校友会
创新
哈工大
鸿蒙
王成录
标题
主题
北京
疫情
域名
沙漠渔溏
方法声明
jpa
表情包
emoji
JavaAgent
姓名测试
姓名打分
起名
百度
收录
SEO
group by
distinct
去重
JDBC
validationQuery
兄弟元素
点击事件
$event
9600
传输延时
波特率
串口
站点统计
站长工具
cnzz
生命周期
refs
vue
replaceAll
replace
JavaScript
软件
FRP
内网外入
内网穿透
学习
解题
leetcode
云计算
中国医药
神思电子
股票
Java
报错
数据块
关键词
风筝
清明节
注解
spring
clang-format
格式化
反向代理
nginx
索引
linux
vim
数据库
仓库管理
git
压缩
winrar
mysql
测试
markdown
目 录
CONTENT
以下是
网络聚合
相关的文章
2024-01-19
Python基础之面向对象
Python基础之面向对象 引言,学习面向对象之前先讨论一下编程思想 目前我们学了两种编程思想。一是面向过程编程即流程,面向过程就是按照固定的解决流程解决问题,比如编写注册功能、登录功能、转账功能等,需要列举出每一步的流程,并且随着步骤的深入问题的解决思路越来越简单,然后指定出该问题的解决方案,用函数关键字def来完成面向过程的编程。二是面向对象编程,有点类似于造物主的感觉 只需要造出一个个对象至于该对象将来会如何发展跟程序员没有关系也无法控制,比如游戏人物。用关键字class来定义出类来完成面向对象的编程。这两种编程思想没有优劣之分需要结合实际需求而定、实际上这两种编程思想是彼此交融的只是占比不同。如果需求是注册登录 人脸识别那肯定使用用面向过程更合适。如果需求是游戏人物肯定是面向对象更合适。面向对象并不是一门新的技术为了好区分定义出来了独有的语法格式。那么本篇文章接下来详细的研究一下编程思想之一面向对象。 一、面向对象之类与对象 对象:数据与功能的结合体 是面向对象的核心 必须先定义出类才能用对象 类:多个对象相同数据和功能的结合体 主要是为了节省代码 1.类与对象的创建以及调用 class 类名: '''代码注释''' 对象公共的数据 对象公共的功能 """ class 是定义类的官关键字 类名的命名跟变量名几乎一直 但是要首字母大写 数据是变量名与数据值得绑定 功能是方法其实就是函数 """ 类在定义阶段就会执行类体代码 但是属于类的局部名称空间 外界无法直接调用 类和对象访问数据或者功能 可以统一采用句点符 类名加括号就会产生对象 并且每执行一次都会产生一个全新的对象 2.对象独有的数据和功能 '''对象独有数据''' 1.直接利用对象点名字的方式 obj1 = Student() obj1.dict['name'] = 'almira' obj1.age = 18 obj2 = Student() obj2.dict['name'] = 'Alina' obj2.age = 29 2.将添加对象独有数据的代码封装成函数 def init(obj,name,age): obj.name = name # obj.dict['name'] = name obj.age = age # obj.dict['age'] = age 3.将上述方法绑定给特定类的对象 class Student: def init(obj,name,age): obj.name = name # obj.dict['name'] = name obj.age = age # obj.dict['age'] = age 4.自动触发的方法 class Student: def init(self,name,age): self.name = name # obj.dict['name'] = name self.age = age # obj.dict['age'] = age 5.类中如果有双下init方法 意味着类加括号需要传参数 obj = Student('almira', 18) '''对象独有的功能''' 1.也是直接在全局定义函数 然后给对象 def func():pass obj = Student() obj.func = func 2.面向对象的思想 放到类中 3.对象的绑定方法 类中定义的方法既可以说是对象公共的方法 也可以是对象独有的方法 哪个对象来调用 谁就是主人公 二、动静态方法 在类体中定义函数的三种方式 1.类中直接定义函数 默认绑定给对象 类调用有几个参数传几个阐述 对象调用第一个参数就是对象自身 class Student: shcool_name = '天津财经大学' def func1(self): print('天才学子') obj = Student() # 1.绑定给对象的方法 obj.func1() Student.func1(123) 2.被@classmethod修饰的函数 默认绑定给类 类调用第一个参数就是类自身 对象也可以调用并且会自动将产生该对象的类当做第一个参数传入 class Student: school_name = '天津财经大学' @classmethod def func2(cls): print('是不是蒙了呢', cls) obj = Student() # 2.绑定给类的方法 Student.func2() # fun2(Student) obj.func2() # func2(Student) 3.普普通通的函数 无论是类还是对象调用 都必须自己手动传参 class Student: school_name = '天津财经大学' @staticmethod def func3(a): print('哈哈哈,我很烦',a) obj = Student() # 1.静态方法 Student.func3(123) obj.func3(321) 三、面向对象三大特征之继承 1.继承的含义 在编程世界中继承表示类与类之间资源的从属关系 比如 类A继承类B 2.继承的目的 在编程世界中类A继承类B就拥有了类B中所有的数据和方法使用权 3.继承得本质 继承的本质就是对某一批类的抽象,从而实现对现实世界更好的建模 4.继承的实操 class Son(Father): pass '''括号里面可以写其他类名Son就继承了Father''' # 继承其他类的类 Son 称为子类派生类 class Son(F1, F2, F3): pass '''可以多继承 在括号内的类名用逗号隔开即可''' # 被继承的类 Father F1, F2, F3 称为父类、基类、超类 5.名字的查找顺序 不继承:对象自身>>>产生对象的类 单继承:对象自身>>>产生对象的类>>>类的父类们 多继承:对象自身>>>产生对象的类>>>类的父类们 四、面向对象三大特征之封装 封装:就是将数据和功能'封装'起来 隐藏:将数据和功能隐藏起来不让用户直接调用 而是开发一些接口间接调用从而可以在接口内添加额外的操作 伪装:将类里面的方法伪装成类里面的数据 class C1: __name = 'almira' def __func(self):pass '''也有可能会出现单个下划线开头的变量名也表示隐藏的意思或者没有''' class C2: @property def BMI(self):pass 五、面向对象三大特征之多态 多态其实就是一种事物的多种形态 面向对象中的多态的意思是 一种事物可以有多种形态但是针对相同的功能应该定义相同的用法 这样无论拿到的是那个具体的事物 都可以通过相同的方法调用功能 '''多个类如果有相似之处 那么里面针对相似的功能应该起相同的名字''' class Memory: def read(self):pass class Disk: def read(self):pass '''鸭子类型实际案例''' class QQ: def send(self):pass class Wechat:pass def send(self):pass class Email:pass def send(self):pass class Msg:pass def send(self):pass 六、面向对象之反射 反射是指利用字符串操作对象的数据或方法,从另一个角度分析的话反射能够将面向对象的代码与用交互起来 hasattr() 判断对象是否含有某个字符串对应的属性名或方法名 getattr() 根据字符串获取对象对应的属性名或方法名 setattr() 根据字符串对象设置或修改数据 delattr() 根据字符串删除对象里面的名字 七、面向对象的魔法方法 魔法方法:类中定义的双下方法 不需要人为调用 在特定的条件下会自动触发运行 比如双下init方法创建空对象之后自动触发给对象添加独有的数据如下列举出来了一些魔法方法名称个、和它的功能解释 __init__ 对象添加独有的数据的时候自动触发 __str__ 对象被执行打印操作的时候自动触发 __call__ 对象加括号调用的时候自动触发 __getattr__ 对象点不存在的名字的时候自动触发 __getatribute__ 对象点名字就会自动触发 __setattr__ 给对象添加或者修改数据的时候自动触发 __enter__ 当对象被当做with上下文管理操作的开始自动触发 __exit__ 当with上下文管理语法运行完毕之后自动触发 __new__ 可以产生空对象 __dict__ 以字典方式打印所有属性 八、创建类的两种方式 # 方式1:使用关键字class class Student: school_name = '天津财经大学' def func1(self):pass print(Student) print(Student.__dict__) # 方式2:使用元类type 格式:type(类名,类的父类,类的名称空间) cls = type('Student', (object,), {'name': 'almira'}) print(cls) print(cls.dict)
2024-01-19
164
0
0
网络聚合
2024-01-19
Springboot整合AOP和注解,实现丰富的切面功能
简介 我们在文章《Spring AOP与AspectJ的对比及应用》介绍了AOP的使用,这篇文章讲解一下AOP与注解的整合,通过注解来使用AOP,会非常方便。为了简便,我们还是来实现一个计时的功能。 整合过程 首先创建一个注解: @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface PkslowLogTime { } 然后在一个Service中使用注解: @Service @Slf4j public class TestService { @PkslowLogTime public void fetchData() { log.info("fetchData"); try { Thread.sleep(500); } catch (InterruptedException e) { throw new RuntimeException(e); } } } 这个Service的方法会在Controller中调用: @GetMapping("/hello") public String hello() { log.info("------hello() start---"); test(); staticTest(); testService.fetchData(); log.info("------hello() end---"); return "Hello, pkslow."; } 接着是关键一步,我们要实现切面,来找到注解并实现对应功能: @Aspect @Component @Slf4j public class PkslowLogTimeAspect { @Around("@annotation(com.pkslow.springboot.aop.PkslowLogTime) && execution(* *(..))") public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable { log.info("------PkslowLogTime doAround start------"); MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature(); // Get intercepted method details String className = methodSignature.getDeclaringType().getSimpleName(); String methodName = methodSignature.getName(); // Measure method execution time StopWatch stopWatch = new StopWatch(className + "->" + methodName); stopWatch.start(methodName); Object result = joinPoint.proceed(); stopWatch.stop(); // Log method execution time log.info(stopWatch.prettyPrint()); log.info("------PkslowLogTime doAround end------"); return result; } } @Around("@annotation(com.pkslow.springboot.aop.PkslowLogTime) && execution(* *(..))")这个表达式很关键,如果不对,将无法正确识别;还有可能出现多次调用的情况。多次调用的情况可以参考:Stackoverflow 这里使用了Spring的StopWatch来计时。 测试 通过maven build包: $ mvn clean package 日志可以看到有对应的织入信息: [INFO] Join point 'method-execution(java.lang.String com.pkslow.springboot.controller.TestController.hello())' in Type 'com.pkslow.springboot.controller.TestController' (TestController.java:22) advised by around advice from 'com.pkslow.springboot.aop.ControllerAspect' (ControllerAspect.class(from ControllerAspect.java)) [INFO] Join point 'method-execution(java.lang.String com.pkslow.springboot.controller.TestController.hello())' in Type 'com.pkslow.springboot.controller.TestController' (TestController.java:22) advised by before advice from 'com.pkslow.springboot.aop.ControllerAspect' (ControllerAspect.class(from ControllerAspect.java)) [INFO] Join point 'method-execution(void com.pkslow.springboot.controller.TestController.test())' in Type 'com.pkslow.springboot.controller.TestController' (TestController.java:31) advised by around advice from 'com.pkslow.springboot.aop.ControllerAspect' (ControllerAspect.class(from ControllerAspect.java)) [INFO] Join point 'method-execution(void com.pkslow.springboot.controller.TestController.test())' in Type 'com.pkslow.springboot.controller.TestController' (TestController.java:31) advised by before advice from 'com.pkslow.springboot.aop.ControllerAspect' (ControllerAspect.class(from ControllerAspect.java)) [INFO] Join point 'method-execution(void com.pkslow.springboot.controller.TestController.staticTest())' in Type 'com.pkslow.springboot.controller.TestController' (TestController.java:37) advised by around advice from 'com.pkslow.springboot.aop.ControllerAspect' (ControllerAspect.class(from ControllerAspect.java)) [INFO] Join point 'method-execution(void com.pkslow.springboot.controller.TestController.staticTest())' in Type 'com.pkslow.springboot.controller.TestController' (TestController.java:37) advised by before advice from 'com.pkslow.springboot.aop.ControllerAspect' (ControllerAspect.class(from ControllerAspect.java)) [INFO] Join point 'method-execution(void com.pkslow.springboot.service.TestService.fetchData())' in Type 'com.pkslow.springboot.service.TestService' (TestService.java:12) advised by around advice from 'com.pkslow.springboot.aop.PkslowLogTimeAspect' (PkslowLogTimeAspect.class(from PkslowLogTimeAspect.java)) 启动应用后访问接口,日志如下: 总结 通过注解可以实现很多功能,也非常方便。而且注解还可以添加参数,组合使用更完美了。 代码请看GitHub: https://github.com/LarryDpk/pkslow-samples
2024-01-19
115
0
0
网络聚合
2024-01-19
如何5分钟跑起来一个完整项目?
今天熊哥和大家聊聊,我怎么在5分钟之内生成一个完整的项目。 效果 看看这个面板,这居然是我花了5分钟成功跑起来的项目。 竟然具备超过三十项功能。还可以直接在页面上生成代码。 它是什么?它是 go-gin-api 它支持哪些功能? 可能下面有一些功能你没听过,或者听不懂。没关系,先看看。我以后都会讲。 支持 rate 接口限流 支持 panic 异常时邮件通知 支持 cors 接口跨域 支持 Prometheus 指标记录 支持 Swagger 接口文档生成 支持 GraphQL 查询语言 支持 trace 项目内部链路追踪 支持 pprof 性能剖析 支持 errno 统一定义错误码 支持 zap 日志收集 支持 viper 配置文件解析 支持 gorm 数据库组件 支持 go-redis 组件 支持 RESTful API 返回值规范 支持 生成数据表 CURD、控制器方法 等代码生成器 支持 cron 定时任务,在后台可界面配置 支持 websocket 实时通讯,在后台有界面演示 支持 web 界面,使用的 Light Year Admin 模板 不懂的关键字,如果感兴趣也可以自己下来查查我的宝。 三行代码跑起来 git clone https://github.com/xinliangnote/go-gin-api.git cd go-gin-api go run main.go -env dev 跑完以后立马就会弹出一个页面。 不得不说go-gin-api的作者实在想得周全,跑起来不报错,会提示你填写环境信息。 现在已经过了1分钟了,熊哥还有4分钟。 3分钟启一个环境 既然面板提示需要mysql和redis,立马打开hub.docker.com 搜索mysql和redis获得他们的启动命令。 docker容器,可以最快速在本地提供开发环境。不懂就问熊哥 直接在概述里拿到最简单的启动命令如下。 docker run --name some-mysql -e MYSQL_ROOT_PASSWORD=my-secret-pw -d mysql:tag docker run --name some-redis -d redis 稍做修改,加一下密码。 docker run --name mysql-bear -p 3307:3306 -e MYSQL_ROOT_PASSWORD=mysql-bear -d mysql:latest docker run --name redis-bear -p 6479:6379 -d redis docker ps可看到环境搞定了。 我这里把端口映射到主机上了,用的3307、6479 不然端口只能在容器之间能访问。 redis没有密码,mysql密码是mysql-bear。 最后1分钟 项目要求先建库。 docker exec -it mysql-bear mysql -pmysql-bear -e "create database bear_gin_db CHARACTER SET utf8 COLLATE utf8_general_ci;" 我直接把用户名密码写命令行了,为了安全建议密码不要写命令行,会要求输入。 创建了名为bear_gin_db的库,字符集是utf8。 点击初始化项目,在本地重启项目。 go run main.go -env dev 成功啦 后面给大家分别讲解各种概念,和go-gin-api怎么使用。欢迎和我讨论。希望大家真的能快速上手做项目。 一起进步 你好,我是小熊,是一个爱技术但是更爱钱的程序员。上进且佛系自律的人。喜欢发小秘密/臭屁又爱炫耀。 奋斗的大学,激情的现在。赚了钱买了房,写了书出了名。当过面试官,带过徒弟搬过转。 大厂外来务工人员。是我,我是小熊,是不一样的烟火欢迎围观。 我的博客 机智的程序员小熊 欢迎收藏
2024-01-19
98
0
0
网络聚合
2024-01-19
RabbitMQ、RocketMQ、Kafka延迟队列实现
延迟队列在实际项目中有非常多的应用场景,最常见的比如订单未支付,超时取消订单,在创建订单的时候发送一条延迟消息,达到延迟时间之后消费者收到消息,如果订单没有支付的话,那么就取消订单。 那么,今天我们需要来谈的问题就是RabbitMQ、RocketMQ、Kafka中分别是怎么实现延时队列的,以及他们对应的实现原理是什么? RabbitMQ RabbitMQ本身并不存在延迟队列的概念,在 RabbitMQ 中是通过 DLX 死信交换机和 TTL 消息过期来实现延迟队列的。 TTL(Time to Live)过期时间 有两种方式可以设置 TTL。 通过队列属性设置,这样的话队列中的所有消息都会拥有相同的过期时间 对消息单独设置过期时间,这样每条消息的过期时间都可以不同 那么如果同时设置呢?这样将会以两个时间中较小的值为准。 针对队列的方式通过参数x-message-ttl来设置。 Map<String, Object> args = new HashMap<String, Object>(); args.put("x-message-ttl", 6000); channel.queueDeclare(queueName, durable, exclusive, autoDelete, args); 针对消息的方式通过setExpiration来设置。 AMQP.BasicProperties properties = new AMQP.BasicProperties(); Properties.setDeliveryMode(2); properties.setExpiration("60000"); channel.basicPublish(exchangeName, routingKey, mandatory, properties, "message".getBytes()); DLX(Dead Letter Exchange)死信交换机 一个消息要成为死信消息有 3 种情况: 消息被拒绝,比如调用reject方法,并且需要设置requeue为false 消息过期 队列达到最大长度 可以通过参数dead-letter-exchange设置死信交换机,也可以通过参数dead-letter- exchange指定 RoutingKey(未指定则使用原队列的 RoutingKey)。 Map<String, Object> args = new HashMap<String, Object>(); args.put("x-dead-letter-exchange", "exchange.dlx"); args.put("x-dead-letter-routing-key", "routingkey"); channel.queueDeclare(queueName, durable, exclusive, autoDelete, args); 原理 当我们对消息设置了 TTL 和 DLX 之后,当消息正常发送,通过 Exchange 到达 Queue 之后,由于设置了 TTL 过期时间,并且消息没有被消费(订阅的是死信队列),达到过期时间之后,消息就转移到与之绑定的 DLX 死信队列之中。 这样的话,就相当于通过 DLX 和 TTL 间接实现了延迟消息的功能,实际使用中我们可以根据不同的延迟级别绑定设置不同延迟时间的队列来达到实现不同延迟时间的效果。 RocketMQ RocketMQ 和 RabbitMQ 不同,它本身就有延迟队列的功能,但是开源版本只能支持固定延迟时间的消息,不支持任意时间精度的消息(这个好像只有阿里云版本的可以)。 他的默认时间间隔分为 18 个级别,基本上也能满足大部分场景的需要了。 默认延迟级别:1s、 5s、 10s、 30s、 1m、 2m、 3m、 4m、 5m、 6m、 7m、 8m、 9m、 10m、 20m、 30m、 1h、 2h。 使用起来也非常的简单,直接通过setDelayTimeLevel设置延迟级别即可。 setDelayTimeLevel(level) 原理 实现原理说起来比较简单,Broker 会根据不同的延迟级别创建出多个不同级别的队列,当我们发送延迟消息的时候,根据不同的延迟级别发送到不同的队列中,同时在 Broker 内部通过一个定时器去轮询这些队列(RocketMQ 会为每个延迟级别分别创建一个定时任务),如果消息达到发送时间,那么就直接把消息发送到指 topic 队列中。 RocketMQ 这种实现方式是放在服务端去做的,同时有个好处就是相同延迟时间的消息是可以保证有序性的。 谈到这里就顺便提一下关于消息消费重试的原理,这个本质上来说其实是一样的,对于消费失败需要重试的消息实际上都会被丢到延迟队列的 topic 里,到期后再转发到真正的 topic 中。 Kafka 对于 Kafka 来说,原生并不支持延迟队列的功能,需要我们手动去实现,这里我根据 RocketMQ 的设计提供一个实现思路。 这个设计,我们也不支持任意时间精度的延迟消息,只支持固定级别的延迟,因为对于大部分延迟消息的场景来说足够使用了。 只创建一个 topic,但是针对该 topic 创建 18 个 partition,每个 partition 对应不同的延迟级别,这样做和 RocketMQ 一样有个好处就是能达到相同延迟时间的消息达到有序性。 原理 首先创建一个单独针对延迟队列的 topic,同时创建 18 个 partition 针对不同的延迟级别 发送消息的时候根据延迟参数发送到延迟 topic 对应的 partition,对应的key为延迟时间,同时把原 topic 保存到 header 中 ProducerRecord<Object, Object> producerRecord = new ProducerRecord<>("delay_topic", delayPartition, delayTime, data); producerRecord.headers().add("origin_topic", topic.getBytes(StandardCharsets.UTF_8)); 内嵌的consumer单独设置一个ConsumerGroup去消费延迟 topic 消息,消费到消息之后如果没有达到延迟时间那么就进行pause,然后seek到当前ConsumerRecord的offset位置,同时使用定时器去轮询延迟的TopicPartition,达到延迟时间之后进行resume 如果达到了延迟时间,那么就获取到header中的真实 topic ,直接转发 这里为什么要进行pause和resume呢?因为如果不这样的话,如果超时未消费达到max.poll.interval.ms 最大时间(默认300s),那么将会触发 Rebalance。
2024-01-19
155
0
0
网络聚合
2024-01-19
史上最简单 OpenCV for C++ 在 Windows 和 Ubuntu 上编译安装使用教程
准备工作 原材料 Ubuntu 系统(非必须,Windows 也可以,主要是 Ubuntu 适合编译) OpenCV 3.4.1 压缩包 OpenCV contrib 3.4.1 压缩包 MinGW(Windows 上运行 GCC) 版本信息 GCC 版本 7.5.0 G++ 版本 7.5.0 OpenCV 版本 3.4.1 Cmake 版本 3.10 编译组件和依赖包的安装(Windows 请跳过) sudo apt install -y build-essential ccache cmake doxygen g++ gcc git libavcodec-dev libavformat-dev libavresample-dev libdc1394-22-dev libgphoto2-dev libgtk2.0-dev libjasper-dev libjpeg-dev libjpeg.dev libopenblas-base libopenblas-dev libpng-dev libswscale-dev libtbb2 libtbb-dev libtiff-dev libtiff5.dev libv4l-dev libvtk5-dev libvtk6-dev openjdk-8-jdk pkg-config pylint python-dev python-numpy qt5-default 下载并解压 OpenCV 和 OpenCV Contrib # Ubuntu 命令行 和 Ubuntu 桌面 wget https://github.com/opencv/opencv/archive/3.4.1.zip wget https://github.com/opencv/opencv_contrib/archive/refs/tags/3.4.1.zip unzip opencv-3.4.1.zip unzip opencv_contrib-3.4.1.zip Windows 和 Ubuntu 桌面端直接下载解压就好了 编译 OpenCV 源码 Ubuntu 命令行 cmake 命令行编译安装 # 新建编译文件夹 mkdir opencv-3.4.1-build 通过 cd 命令进入编译文件夹 cd opencv-3.4.1-build CMAKE_INSTALL_PREFIX 是 OpenCV 的安装位置 OPENCV_EXTRA_MODULES_PATH 是 opencv_contrib-3.4.1 的 modules 文件夹 cmake -D CMAKE_BUILD_TYPE=Release -D CMAKE_INSTALL_PREFIX=/usr/local -D OPENCV_EXTRA_MODULES_PATH=/home/ahtelek/OpenCV/opencv_contrib-3.4.1/modules/ -D BUILD_TIFF=ON .. make 编译,也可以使用 make -j、make -j8、make -j16 等命令速度会稍快一些 sudo make 安装 sudo make install Ubuntu 桌面端和 Windows cmake-gui + cmake 编译安装 选择好 OpenCV 源码和 OpenCV 编译文件夹 就可以选择 Configure 选择 MinGW Makefiles 复选框选择第二个 这是 MinGW 中 bin 目录下的 gcc.exe 和 g++.exe 文件 选择 Finish 等他跑完 搜索栏搜索 extra 选择 opencv-contrib 下的 modules 再点击 Configure,配置完成点击 Generate。 一直到显示 Configuring done Generating done Win+R cmd cd 到编译文件夹里 输入 MinGW32-make 开始编译,也可以使用 make -j、make -j8、make -j16 等命令速度会稍快一些 编译完成后,再输入 MinGW32-make install 进行安装 编译太慢了就不展示了(下面展示的是 Ubuntu 的) 用别人已经编译好的 GitHub 仓库中有人提供已经编译好的 OpenCV,可以通过 Configuration 看到用了什么编译器、操作系统、cmake 版本 https://github.com/huihut/opencv-mingw-build https://github.com/huihut/OpenCV-MinGW-Build/releases
2024-01-19
521
0
0
网络聚合
2024-01-19
【架构设计】你的应用该如何分层呢?
前言 最近review公司的代码,发现现在整个代码层级十分混乱,一个service类的长度甚至达到了5000多行。而且各种分层模型DTO、VO乱用, 最终出现逻辑不清晰、各模块相互依赖、代码扩展性差、改动一处就牵一发而动全身等问题。 我们在吸取了阿里巴巴的分层规范以及网上的一些经验后,重新梳理总结了属于我们项目的分层规范。 欢迎关注个人公众号『JAVA旭阳』交流沟通 三层架构VS四层架构 我们公司原来的分层采用的是传统的三层架构,比如在构建项目的时候,我们通常会建立三个目录:Web、Service 和 Dao,它们分别对应了表现层、逻辑层还有数据访问层。 这样导致一个很大的问题,随着业务越来越复杂,逻辑层也就是service层越来越庞大,所以出现了前面说的5000多行的类,可想而知维护成本有多大。 参照阿里发布的《阿里巴巴 Java 开发手册 v1.4.0(详尽版)》,我们可以将原先的三层架构细化成下面的样子: 终端显示层:各端模板渲染并执行显示的层。当前主要是 Velocity 渲染,JS 渲染, JSP 渲染,移动端展示等。 开放接口层:将 Service 层方法封装成开放接口,同时进行网关安全控制和流量控制等。 Web 层:主要是对访问控制进行转发,各类基本参数校验,或者不复用的业务简单处理等。 Service 层:业务逻辑层。 Manager 层:通用业务处理层。主要实现下面的功能,1) 对第三方平台封装的层,预处理返回结果及转化异常信息,适配上层接口。2) 对 Service 层通用能力的下沉,如缓存方案、中间件通用处理。3) 与 DAO 层交互,对多个 DAO 的组合复用。 DAO 层:数据访问层,与底层 MySQL、Oracle、Hbase 等进行数据交互。 外部接口或第三方平台:包括其它部门 RPC 开放接口,基础平台,其它公司的 HTTP 接口。 在这个分层架构中主要增加了 Manager 层,它可以将Service层中的一些通用能力比如操作缓存、消息队列的操作下沉,也可以将通过feign调用其他服务的接口进行一层包装,再提供给Service调用,这也就是所谓的防腐层。 VO、DTO、BO、DO区别 前面讲解了整体的一个分层架构,那么在不同的层级之间必然需要一些模型对象进行流转传递,VO,BO,DO,DTO, 那么他们之间有什么区别呢? VO(View Object):视图对象,用于展示层,它的作用是把某个指定页面(或组件)的所有数据封装起来。 DTO(Data Transfer Object):数据传输对象,用于展示层与服务层之间的数据传输对象。 BO(Business Object):业务对象,把业务逻辑封装为一个对象,这个对象可以包括一个或多个其它的对象。 DO(Domain Object):领域对象,阿里巴巴规范中引入,此对象与数据库表结构一一对应,通过 DAO 层向上传输数据源对象。 VO和DTO什么区别? VO比较容易混淆的是DTO,DTO是展示层与服务层之间传递数据的对象,可以这样说,对于绝大部分的应用场景来说,DTO和VO的属性值基本是一致的,而且他们通常都是POJO,那么既然有了VO,为什么还需要DTO呢? 例如服务层有一个getUser的方法返回一个系统用户,其中有一个属性是gender(性别),对于服务层来说,它只从语义上定义:1-男性,2-女性,0-未指定,而对于展示层来说,它可能需要用“帅哥”代表男性,用“美女”代表女性,用“秘密”代表未指定。说到这里,可能你还会反驳,在服务层直接就返回“帅哥美女”不就行了吗?对于大部分应用来说,这不是问题,但设想一下,如果需求允许客户可以定制风格,而不同风格对于“性别”的表现方式不一样,又或者这个服务同时供多个客户端使用(不同门户),而不同的客户端对于表现层的要求有所不同,那么,问题就来了。再者,回到设计层面上分析,从职责单一原则来看,服务层只负责业务,与具体的表现形式无关,因此,它返回的DTO,不应该出现与表现形式的耦合。 BO和DTO的区别? 从用途上进行根本的区别,BO是业务对象,DTO是数据传输对象,虽然BO也可以排列组合数据,但它的功能是对内的,但在提供对外接口时,BO对象中的某些属性对象可能用不到或者不方便对外暴露,那么此时DTO只需要在BO的基础上,抽取自己需要的数据,然后对外提供。在这个关系上,通常不会有数据内容的变化,内容变化要么在BO内部业务计算的时候完成,要么在解释VO的时候完成。 我们项目根据实际情况总结了分层领域模型的规范: 前端传入的参数统一使用DTO接收 由于公司是TO B项目,只有一个电脑web端,所以返回给展示层或者Feign返回的对象建议直接用DTO返回,特殊情况采用VO返回 由于历史原因,和数据库模型对应的不采用DO结尾,直接用原始对象,比如学生表student直接使用Student对象 目前很多代码存在继承关系,比如DTO、VO继承数据库对象,这个坚决不允许 项目目录结构最佳实践 可以参考github上面的https://github.com/alvinlkk/mall4cloud这个项目,它是一个极度遵守阿里巴巴代码规约的项目。 这里面有个关于Feign设计的亮点, 主要是为了避免服务提供方修改了接口,而调用方没有修改导致异常的问题。 将feign接口抽取出一个独立的模块 服务中依赖feign模块,实现FeignClient 我们来看下整体的一个结构。 mall4cloud ├─mall4cloud-api -- 内网接口 │ ├─mall4cloud-api-auth -- 授权对内接口 │ ├─mall4cloud-api-biz -- biz对内接口 │ ├─mall4cloud-api-leaf -- 美团分布式id生成接口 │ ├─mall4cloud-api-multishop -- 店铺对内接口 │ ├─mall4cloud-api-order -- 订单对内接口 │ ├─mall4cloud-api-platform -- 平台对内接口 │ ├─mall4cloud-api-product -- 商品对内接口 │ ├─mall4cloud-api-rbac -- 用户角色权限对内接口 │ ├─mall4cloud-api-search -- 搜索对内接口 │ └─mall4cloud-api-user -- 用户对内接口 ├─mall4cloud-auth -- 授权校验模块 ├─mall4cloud-biz -- mall4cloud 业务代码。如图片上传/短信等 ├─mall4cloud-common -- 一些公共的方法 │ ├─mall4cloud-common-cache -- 缓存相关公共代码 │ ├─mall4cloud-common-core -- 公共模块核心(公共中的公共代码) │ ├─mall4cloud-common-database -- 数据库连接相关公共代码 │ ├─mall4cloud-common-order -- 订单相关公共代码 │ ├─mall4cloud-common-product -- 商品相关公共代码 │ ├─mall4cloud-common-rocketmq -- rocketmq相关公共代码 │ └─mall4cloud-common-security -- 安全相关公共代码 ├─mall4cloud-gateway -- 网关 ├─mall4cloud-leaf -- 基于美团leaf的生成id服务 ├─mall4cloud-multishop -- 商家端 ├─mall4cloud-order -- 订单服务 ├─mall4cloud-payment -- 支付服务 ├─mall4cloud-platform -- 平台端 ├─mall4cloud-product -- 商品服务 ├─mall4cloud-rbac -- 用户角色权限模块 ├─mall4cloud-search -- 搜索模块 └─mall4cloud-user -- 用户服务 结束语 关于应用分层这块我觉得没那么简单,特别是团队大了以后,人员水平参差不齐,很难约束,很容易自由发挥,各写各的。不知道大家有没有什么好的办法呢? 欢迎关注个人公众号『JAVA旭阳』交流沟通
2024-01-19
151
0
0
网络聚合
2024-01-19
分布式协议与算法-Raft算法
本文总结自:极客时间韩健老师的分布式协议与算法实战课程。 大家都知道,Raft算法属于Multi-Paxos算法,它是在Multi-Paxos思想的基础上,做了一些简化和限制。关于Paxos算法,博主在之前的文章有过总结,大家可以从这里跳转:分布式协议与算法-Paxos算法 关于Raft算法相关的开源社区有很多,如果你想深入理解RAFT算法,博主在这里推荐蚂蚁金服的SOFAJRaft,它是基于JAVA语言开发的一个生产级高性能共识算法实现。 博主总结过一些关于SOFAJRaft的源码,有兴趣的读者可以访问:SOFAJRaft源码阅读。 @Author:Akai-yuan @更新时间:2023/1/30 1.如何选举领导者 1.1成员身份 成员身份,又叫做服务器节点状态,Raft 算法支持领导者(Leader)、跟随者(Follower)和候选人(Candidate) 3 种状态。 跟随者:就相当于普通群众,默默地接收和处理来自领导者的消息,当等待领导者心跳信息超时的时候,就主动站出来,推荐自己当候选人。 候选人:候选人将向其他节点发送请求投票(RequestVote)RPC 消息,通知其他节点来投票,如果赢得了大多数选票,就晋升当领导者。 领导者:蛮不讲理的霸道总裁,一切以我为准,平常的主要工作内容就是 3 部分,处理写请求、管理日志复制和不断地发送心跳信息,通知其他节点“我是领导者,我还活着,你们现在不要发起新的选举,找个新领导者来替代我。” 1.2选举过程: 首先,在初始状态下,集群中所有的节点都是跟随者的状态。 Raft 算法实现了随机超时时间的特性。也就是说,每个节点等待领导者节点心跳信息的超时时间间隔是随机的。通过上面的图片你可以看到,集群中没有领导者,而节点 A 的等待超时时间最小(150ms),它会最先因为没有等到领导者的心跳信息,发生超时。这个时候,节点 A 就增加自己的任期编号,并推举自己为候选人,先给自己投上一张选票,然后向其他节点发送请求投票 RPC 消息,请它们选举自己为领导者。 如果其他节点接收到候选人 A 的请求投票 RPC 消息,在编号为 1 的这届任期内,也还没有进行过投票,那么它将把选票投给节点 A,并增加自己的任期编号。 如果候选人在选举超时时间内赢得了大多数的选票,那么它就会成为本届任期内新的领导者。 节点 A 当选领导者后,他将周期性地发送心跳消息,通知其他服务器我是领导者,阻止跟随者发起新的选举,篡权。 2.节点间如何通信 在 Raft 算法中,服务器节点间的沟通联络采用的是远程过程调用(RPC),在领导者选举 中,需要用到这样两类的 RPC: 请求投票(RequestVote)RPC,是由候选人在选举期间发起,通知各节点进行投票; 日志复制(AppendEntries)RPC,是由领导者发起,用来复制日志和提供心跳消息。日志复制 RPC 只能由领导者发起,这是实现强领导者模型的关键之一。 3.什么是任期 我们知道,议会选举中的领导者是有任期的,领导者任命到期后,要重新开会再次选举。Raft 算法中的领导者也是有任期的,每个任期由单调递增的数字(任期编号)标识,比如节点 A 的任期编号是 1。任期编号是随着选举的举行而变化的,这是在说下面几点。 跟随者在等待领导者心跳信息超时后,推举自己为候选人时,会增加自己的任期号。比如节点 A 的当前任期编号为 0,那么在推举自己为候选人时,会将自己的任期编号增加为 1。 如果一个服务器节点,发现自己的任期编号比其他节点小,那么它会更新自己的编号到较大的编号值。比如节点 B 的任期编号是 0,当收到来自节点 A 的请求投票 RPC 消息时,因为消息中包含了节点 A 的任期编号,且编号为 1,那么节点 B 将把自己的任期编号更新为 1。 但是,与现实议会选举中的领导者的任期不同,Raft 算法中的任期不只是时间段,而且任期编号的大小,会影响领导者选举和请求的处理。 在 Raft 算法中约定,如果一个候选人或者领导者,发现自己的任期编号比其他节点小,那么它会立即恢复成跟随者状态。比如分区错误恢复后,任期编号为 3 的领导者节点B,收到来自新领导者的,包含任期编号为 4 的心跳消息,那么节点 B 将立即恢复成跟随者状态。 还约定如果一个节点接收到一个包含较小的任期编号值的请求,那么它会直接拒绝这个请求。比如节点 C 的任期编号为 4,收到包含任期编号为 3 的请求投票 RPC 消息,那么它将拒绝这个消息。 在这里,你可以看到,Raft 算法中的任期比议会选举中的任期要复杂。同样,在 Raft 算法中,选举规则的内容也会比较多。 4.选举有哪些规则 领导者周期性地向所有跟随者发送心跳消息(即不包含日志项的日志复制 RPC 消息),通知大家我是领导者,阻止跟随者发起新的选举。 如果在指定时间内,跟随者没有接收到来自领导者的消息,那么它就认为当前没有领导者,推举自己为候选人,发起领导者选举。 在一次选举中,赢得大多数选票的候选人,将晋升为领导者。 在一个任期内,领导者一直都会是领导者,直到它自身出现问题(比如宕机),或者因为网络延迟,其他节点发起一轮新的选举。 在一次选举中,每一个服务器节点最多会对一个任期编号投出一张选票,并且按照“先来先服务”的原则进行投票。比如节点 C 的任期编号为 3,先收到了 1 个包含任期编号为 4 的投票请求(来自节点 A),然后又收到了 1 个包含任期编号为 4 的投票请求(来自节点 B)。那么节点 C 将会把唯一一张选票投给节点 A,当再收到节点 B 的投票请求RPC 消息时,对于编号为 4 的任期,已没有选票可投了。 当任期编号相同时,日志完整性高的跟随者(也就是最后一条日志项对应的任期编号值更大,索引号更大),拒绝投票给日志完整性低的候选人。比如节点 B、C 的任期编号都是 3,节点 B 的最后一条日志项对应的任期编号为 3,而节点 C 为 2,那么当节点 C请求节点 B 投票给自己时,节点 B 将拒绝投票。 5.如何理解随机超时时间 在议会选举中,常出现未达到指定票数,选举无效,需要重新选举的情况。在 Raft 算法的选举中,也存在类似的问题,那它是如何处理选举无效的问题呢? 其实,Raft 算法巧妙地使用随机选举超时时间的方法,把超时时间都分散开来,在大多数情况下只有一个服务器节点先发起选举,而不是同时发起选举,这样就能减少因选票瓜分导致选举失败的情况。 我想强调的是,在 Raft 算法中,随机超时时间是有 2 种含义的,这里是很多同学容易理解出错的地方,需要你注意一下: 跟随者等待领导者心跳信息超时的时间间隔,是随机的; 当没有候选人赢得过半票数,选举无效了,这时需要等待一个随机时间间隔,也就是说,等待选举超时的时间间隔,是随机的。 6.如何复制日志 6.1如何理解日志 副本数据是以日志的形式存在的,日志是由日志项组成。日志项是一种数据格式,它主要包含用户指定的数据,也就是指令(Command),还包含一些附加信息,比如索引值(Log index)、任期编号(Term)。 6.2日志复制过程 你可以把 Raft 的日志复制理解成一个优化后的二阶段提交(将二阶段优化成了一阶段),减少了一半的往返消息,也就是降低了一半的消息延迟。那日志复制的具体过程是什么呢? 首先,领导者进入第一阶段,通过日志复制(AppendEntries)RPC 消息,将日志项复制到集群其他节点上。 接着,如果领导者接收到大多数的“复制成功”响应后,它将日志项提交到它的状态机,并返回成功给客户端。如果领导者没有接收到大多数的“复制成功”响应,那么就返回错误给客户端。 学到这里,有同学可能有这样的疑问了,领导者将日志项提交到它的状态机,怎么没通知跟随者提交日志项呢? 这是 Raft 中的一个优化,领导者不直接发送消息通知其他节点提交指定日志项。因为领导者的日志复制 RPC 消息或心跳消息,包含了当前最大的,将会被提交的日志项索引值。所以通过日志复制 RPC 消息或心跳消息,跟随者就可以知道领导者的日志提交位置信息。 因此,当其他节点接受领导者的心跳消息,或者新的日志复制 RPC 消息后,就会将这条日志项提交到它的状态机。而这个优化,降低了处理客户端请求的延迟,将二阶段提交优化为了一段提交,降低了一半的消息延迟。 6.3日志一致的实现 在 Raft 算法中,领导者通过强制跟随者直接复制自己的日志项,处理不一致日志。也就是说,Raft 是通过以领导者的日志为准,来实现各节点日志的一致的。具体有 2 个步骤。 首先,领导者通过日志复制 RPC 的一致性检查,找到跟随者节点上,与自己相同日志项的最大索引值。也就是说,这个索引值之前的日志,领导者和跟随者是一致的,之后的日志是不一致的了。 然后,领导者强制跟随者更新覆盖的不一致日志项,实现日志的一致。 为了方便演示,我们引入 2 个新变量: PrevLogEntry:表示当前要复制的日志项,前面一条日志项的索引值。比如在图中,如果领导者将索引值为 8 的日志项发送给跟随者,那么此时 PrevLogEntry 值为 7。 PrevLogTerm:表示当前要复制的日志项,前面一条日志项的任期编号,比如在图中,如果领导者将索引值为 8 的日志项发送给跟随者,那么此时 PrevLogTerm 值为 4。 领导者通过日志复制 RPC 消息,发送当前最新日志项到跟随者(为了演示方便,假设当前需要复制的日志项是最新的),这个消息的 PrevLogEntry 值为 7,PrevLogTerm 值为 4。 如果跟随者在它的日志中,找不到与 PrevLogEntry 值为 7、PrevLogTerm 值为 4 的日志项,也就是说它的日志和领导者的不一致了,那么跟随者就会拒绝接收新的日志项,并返回失败信息给领导者。 这时,领导者会递减要复制的日志项的索引值,并发送新的日志项到跟随者,这个消息的 PrevLogEntry 值为 6,PrevLogTerm 值为 3。 如果跟随者在它的日志中,找到了 PrevLogEntry 值为 6、PrevLogTerm 值为 3 的日志项,那么日志复制 RPC 返回成功,这样一来,领导者就知道在 PrevLogEntry 值为 6、PrevLogTerm 值为 3 的位置,跟随者的日志项与自己相同。 领导者通过日志复制 RPC,复制并更新覆盖该索引值之后的日志项(也就是不一致的日志项),最终实现了集群各节点日志的一致。 7.成员变更 单节点变更,就是通过一次变更一个节点实现成员变更。如果需要变更多个节点,那你需要执行多次单节点变更。比如将 3 节点集群扩容为 5 节点集群,这时你需要执行 2 次单节点变更,先将 3 节点集群变更为 4 节点集群,然后再将 4 节点集群变更为 5 节点集群,就像下图的样子。 现在,让我们回到开篇的思考题,看看如何用单节点变更的方法,解决这个问题。为了演示 方便,我们假设节点 A 是领导者: 目前的集群配置为[A, B, C],我们先向集群中加入节点 D,这意味着新配置为[A, B, C, D]。 成员变更,是通过这么两步实现的: 第一步,领导者(节点 A)向新节点(节点 D)同步数据; 第二步,领导者(节点 A)将新配置[A, B, C, D]作为一个日志项,复制到新配置中所有节点(节点 A、B、C、D)上,然后将新配置的日志项提交到本地状态机,完成单节点变更。 在变更完成后,现在的集群配置就是[A, B, C, D],我们再向集群中加入节点 E,也就是说, 新配置为[A, B, C, D, E]。成员变更的步骤和上面类似: 第一步,领导者(节点 A)向新节点(节点 E)同步数据; 第二步,领导者(节点 A)将新配置[A, B, C, D, E]作为一个日志项,复制到新配置中的所有节点(A、B、C、D、E)上,然后再将新配置的日志项提交到本地状态机,完成单节点变更。 这样一来,我们就通过一次变更一个节点的方式,完成了成员变更,保证了集群中始终只有一个领导者,而且集群也在稳定运行,持续提供服务。 在正常情况下,不管旧的集群配置是怎么组成的,旧配置的“大多数”和新配置的“大多数”都会有一个节点是重叠的。 也就是说,不会同时存在旧配置和新配置 2个“大多数”。 从上图中你可以看到,不管集群是偶数节点,还是奇数节点,不管是增加节点,还是移除节点,新旧配置的“大多数”都会存在重叠(图中的橙色节点)。 需要你注意的是,在分区错误、节点故障等情况下,如果我们并发执行单节点变更,那么就可能出现一次单节点变更尚未完成,新的单节点变更又在执行,导致集群出现 2 个领导者的情况。 如果你遇到这种情况,可以在领导者启动时,创建一个 NO_OP 日志项(也就是空日志项),只有当领导者将 NO_OP 日志项提交后,再执行成员变更请求。这个解决办法,你记住就可以了,可以自己在课后试着研究下。具体的实现,可参考 **Hashicorp Raft **的源码,也就是 runLeader() 函数中: noop := &logFuture{ log: Log{ Type: LogNoop, }, } r.dispatchLogs([]*logFuture{noop})
2024-01-19
134
0
0
网络聚合
2024-01-19
Ubuntu玩机记录,让我破电脑又飞起来了
写在前面 很早之前的电脑ThinkPad E440,一直没怎么用。最近整理了一下电脑的资料,全部备份到云盘。整理的过程感觉电脑很慢很慢,难受极了。整理完后,终于我要对它下手了! 我制作了启动U盘,把Ubuntu 22.04的镜像烧录进去,通过U盘启动,把系统装在ThinkPad上。居然电脑出奇的好用,根本不卡。那就记录一下吧。 安装Ubuntu系统 先从官网下载镜像,然后通过BalenaEtcher来把系统镜像放在U盘上。接着就是启动与安装了,没什么特别的,只要改一下BIOS的启动顺序即可。 参考: https://ubuntu.com/tutorials/install-ubuntu-desktop#2-download-an-ubuntu-image 截图 可以用系统自带的,但我使用的是Shutter,直接在Ubuntu Software搜索安装即可。但安装完不能使用自选区域截图,会提示: cannot work without X11 server 解决方案: 找到/etc/gdm3/custom.conf文件,去掉注释:WaylandEnable=false 重启一下:sudo systemctl restart gdm3即可。 设置快捷键: Settings -> Keyboard -> Keyboard Shortcuts -> View and Customize Shortcuts 接着拉到最后的Custom Shortcuts。添加如下: 参考: https://askubuntu.com/questions/1353360/ubuntu-21-04-shutter-did-not-work-without-x11-server https://hakanu.net/linux/2021/04/25/keyboard-shortcuts-for-shutter-in-ubuntu-for-easy-screenshots/ 安装Typora 新版本的Typora收费了,并且官网也下载不了原有的免费版本,可以在这个链接下载: 下载地址:https://github.com/iuxt/src/releases/download/2.0/Typora_Linux_0.11.18_amd64.deb 然后使用apt安装即可: sudo apt install ./Typora_Linux_0.11.18_amd64.deb 参考:https://zahui.fan/posts/64b52e0d/ 显示电池百分比 Settings -> Power -> Show Battery Percentage 中文输入法 设置里找到Regin & Language,Manage installed Languages,安装中文。输入法系统选iBus即可。接着在Keyboard那添加中文输入法,有拼音和五笔,如果操作不了,可能需要先重启: 可以通过Win + 空格来切换。中英文通过Shift。 git安装 大致如下: sudo apt install git git config --global user.name "LarryDpk" git config --global user.email "
[email protected]
" 生成ssh key,把pub key放GitHub上 ssh-keygen -t rsa -b 4096 -C "
[email protected]
" Git拉取代码报错: $ git pull ssh: connect to host github.com port 22: Connection timed out fatal: Could not read from remote repository. Please make sure you have the correct access rights and the repository exists. 换成另一个域名:ssh.github.com: 参考: https://docs.github.com/en/authentication/troubleshooting-ssh/using-ssh-over-the-https-port git status显示数字,不显示中文: git config --global core.quotepath false Chrome 因为Chrome在Ubuntu不在Software Center,所以要通过先下载安装包的方式来下载: wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb sudo dpkg -i google-chrome-stable_current_amd64.deb python相关 系统已经自带了Python3,版本也比较高,也没办法再安装了。但要安装pip3: $ python3 --version Python 3.10.6 sudo apt install python3-pip 挂载硬盘 我的电脑是一个SSD盘,一个普通硬盘,系统装在SSD上,所以需要把硬盘挂载一下: sudo mount /dev/sda3 /home/larry/data 但每次都这样手动mount很麻烦,又要输入密码,所以我们让系统在启动的时候就mount,在/etc/fstab文件中添加一行: # disk UUID=277de78c-6639-4373-a5cd-38feff129de7 /home/larry/data ext4 defaults 0 0 重启即可。 参考: https://developerinsider.co/auto-mount-drive-in-ubuntu-server-22-04-at-startup/ OSS阿里云 下载Linux 64位版本:https://github.com/aliyun/oss-browser 直接打开会报错,少了libgconfi-2-4,安装后打开即可: $ ./oss-browser ./oss-browser: error while loading shared libraries: libgconf-2.so.4: cannot open shared object file: No such file or directory $ sudo apt install libgconf-2-4 参考:https://stackoverflow.com/questions/37624225/shared-libraries-libgconf-2-so-4-is-missing JDK 下载JDK: https://github.com/graalvm/graalvm-ce-builds/releases 我下载的版本是:graalvm-ce-java11-linux-amd64-22.3.0.tar.gz 解压后指定JAVA_HOME即可。 JAVA_HOME=/home/larry/software/graalvm-ce-java11-22.3.0 export PATH=$JAVA_HOME/bin:$PATH Maven 直接下载后解压即可:https://maven.apache.org/download.cgi export M2_HOME=/home/larry/software/apache-maven-3.8.6 export PATH=$M2_HOME/bin:$PATH bash_profile设置 在.bashrc添加: if [ -f ~/bash_profile.sh ]; then . ~/bash_profile.sh fi 然后便可在bash_profile.sh文件中添加自己的配置了: export JAVA_HOME=/home/larry/software/graalvm-ce-java11-22.3.0 export PATH=$JAVA_HOME/bin:$PATH export M2_HOME=/home/larry/software/apache-maven-3.8.6 export PATH=$M2_HOME/bin:$PATH alias l='ls -al' SSH SFTP客户端WindTerm 下载软件: https://github.com/kingToolbox/WindTerm/releases/tag/2.5.0 选择自动复制已经支持了。 右键直接粘贴: 参考: https://github.com/kingToolbox/WindTerm/issues/19#issuecomment-719334753 修改锁屏密码: 点击一下右下角的Lock Screen即可修改。 添加程序到Farorites Bar 以IDEA为例,直接通过IDEA自己提供的工具,如下图所示: 或者自己创建对应的Entry,放在特定的位置: $ cat /usr/share/applications/jetbrains-idea.desktop [Desktop Entry] Version=1.0 Type=Application Name=IntelliJ IDEA Ultimate Edition Icon=/home/larry/software/ideaIU-2022.3/bin/idea.svg Exec="/home/larry/software/ideaIU-2022.3/bin/idea.sh" %f Comment=Capable and Ergonomic IDE for JVM Categories=Development;IDE; Terminal=false StartupWMClass=jetbrains-idea StartupNotify=true 如果只是自己生效则放在~/.local/share/applications。 其它例子: [Desktop Entry] Version=1.0 Type=Application Name=WindTerm Icon=/home/larry/software/WindTerm_2.5.0/windterm.png Exec="/home/larry/software/WindTerm_2.5.0/WindTerm" %f Comment=WindTerm Categories=Development;SSH; Terminal=false StartupNotify=true VSCode IDEA占用的内存还是太多了,还是使用VSCode吧,直接Ubuntu Software搜索安装即可。 安装Anaconda 到官网下载: https://repo.anaconda.com/archive/Anaconda3-2022.10-Linux-x86_64.sh 然后执行命令安装: $ bash ./Anaconda3-2022.10-Linux-x86_64.sh 会有一些提示,要输入yes等。最后init,执行命令: $ source ~/.bashrc 其它就是帮我们加了这段内容到.bashrc文件中: # >>> conda initialize >>> # !! Contents within this block are managed by 'conda init' !! __conda_setup="$('/home/larry/anaconda3/bin/conda' 'shell.bash' 'hook' 2> /dev/null)" if [ $? -eq 0 ]; then eval "$__conda_setup" else if [ -f "/home/larry/anaconda3/etc/profile.d/conda.sh" ]; then . "/home/larry/anaconda3/etc/profile.d/conda.sh" else export PATH="/home/larry/anaconda3/bin:$PATH" fi fi unset __conda_setup # <<< conda initialize <<< 同时,它直接把系统的python也切换了: $ which python /home/larry/anaconda3/bin/python $ which python3 /home/larry/anaconda3/bin/python3 $ which pip /home/larry/anaconda3/bin/pip $ which pip3 /home/larry/anaconda3/bin/pip3 $ python --version Python 3.9.13 $ python3 --version Python 3.9.13 $ pip --version pip 22.2.2 from /home/larry/anaconda3/lib/python3.9/site-packages/pip (python 3.9) 百度网盘 直接在官网下载Linux deb版本: https://pan.baidu.com/download#pan 然后用下面命令安装即可: $ sudo apt install ./baidunetdisk_4.15.6_amd64.deb 视频播放器VLC 可以直接在Software Center安装,也可以到官网下载安装:https://www.videolan.org/
2024-01-19
149
0
0
网络聚合
2024-01-19
Java堆外缓存(一个很有意思的应用)
我们在开发过程中会遇到这样的场景:就是一个服务的各项 JVM 的配置都比较合理的情况下,它的 GC 情况还是不容乐观。分析之后发现有 2 个对象特别巨大,占了总存活堆内存的 90%以上。其中第 1 大对象是本地缓存, GC 之后对象一直存活。然后不久应用就会抛出OutOfMemoryError,那怎样避免这种情况呢? 这次给大家推荐一个比较好用的技术:堆外缓存。哈哈哈,顾名思义就是在Java堆之外的缓存,也就是把一些大的难以被GC回收的对象放到堆之外。PS堆外内存不受,堆内内存大小的限制,只受服务器物理内存的大小限制。这三者之间的关系是这样的:物理内存=堆外内存+堆内内存。 技术大佬的GITHUB地址:https://github.com/snazy/ohc 。 要使用他的技术就先要引用对应的jar包,Maven坐标如下: <dependency> <groupId>org.caffinitas.ohc</groupId> <artifactId>ohc-core</artifactId> <version>0.7.4</version> </dependency> //大神给的使用方式如下: //Quickstart: OHCache ohCache = OHCacheBuilder.newBuilder() .keySerializer(yourKeySerializer) .valueSerializer(yourValueSerializer) .build(); 上面是Quickstart 看起来使用如此的丝滑(简单),但是上面的代码是填空题,我们看到复制粘贴后代码不能使用后开始。。。。。。此处省略一万字。其实大神写的代码怎么不能用的呢,不要怀疑大神一定是自己的方法不对,我们的口号是? 如果提供的代码复制粘贴不能直接用的工程师不能称之为大神工程师。 但是github上的大神写的东西怎么不能直接用呢?一定是你的思路不对,老司机都知道,大神写代码一定有写单元测试的,要不然不会有那么多人用的(所以要想成为大神单元测试一定要写好),所以把代码拉下来,复制单元测试的东西应该能直接使用 。下面是CTRL+C来的代码。 public static void main(String[] args) { OHCache ohCache = OHCacheBuilder.<String, String>newBuilder() .keySerializer(new StringSerializer()) .valueSerializer(new StringSerializer()) .build(); ohCache.put("name","xiaozhang"); System.out.println(ohCache.get("name")); // 结果 xiaozhang } static class StringSerializer implements CacheSerializer<String>{ @Override public void serialize(String value, ByteBuffer buf) { // 得到字符串对象UTF-8编码的字节数组 byte[] bytes = value.getBytes(Charsets.UTF_8); // 用前16位记录数组长度 buf.put((byte) ((bytes.length >>> 8) & 0xFF)); buf.put((byte) ((bytes.length) & 0xFF)); buf.put(bytes); } @Override public String deserialize(ByteBuffer buf) { // 判断字节数组的长度 int length = (((buf.get() & 0xff) << 8) + ((buf.get() & 0xff))); byte[] bytes = new byte[length]; // 读取字节数组 buf.get(bytes); // 返回字符串对象 return new String(bytes, Charsets.UTF_8); } @Override public int serializedSize(String value) { byte[] bytes = value.getBytes(Charsets.UTF_8); // 设置字符串长度限制,2^16 = 65536 if (bytes.length > 65536) throw new RuntimeException("encoded string too long: " + bytes.length + " bytes"); // 设置字符串长度限制,2^16 = 65536 return bytes.length + 2; } } 上面的代码我们看到这家伙类似Java中的Map 。也就是一个key和value 结构的对象。感觉So easy ,没什么大的用途。简单?那是你想简单了,后面大招来了。 很简单的代码演示如下: public class MapCasheTest { static HashMap<String,String> map = new HashMap<>(); public static void main(String[] args) throws Exception { oomTest(); } private static void oomTest() throws Exception{ // 休眠几秒,便于观察堆内存使用情况 TimeUnit.SECONDS.sleep(30); int result = 0 ; while (true){ String string = new String(new byte[1024*1024]) ; map.put(result+"",string) ; result++; } } } 运行一小会就报这个错了,也是文章中刚开始说的那个错误。 然后我们去监控系统的堆栈使用情况如下图(只用一小会就把堆内存快用满了,然后自然系统就报错了) 下面我们使用同样的逻辑写如下代码,很神奇的事情出现了,大跌眼镜的事情出现了,先亮出代码如下: public static void main(String[] args) throws Exception{ TimeUnit.SECONDS.sleep(30); OHCache ohCache = OHCacheBuilder.<String, String>newBuilder() .keySerializer(new Test.StringSerializer()) .valueSerializer(new Test.StringSerializer()) .build(); int result = 0 ; while (true){ String string = new String(new byte[2048*2048]) ; ohCache.put(result+"",string) ; result++; } } 程序一直很稳定的运行,没有报错,堆栈运行一上一下,至少程序没报错: 为什么会出现这种情况呢?因为文章开始就讲了这个用的是堆外内存,也就是用的自己电脑的内存,自己电脑的内存目前普通的电脑也有2个G那么大,所以程序一直运行稳定。下面看看我们CPU运行的情况,刚开始有点高,当我把程序关闭CPU使用立马下来了。 哈哈哈,如果是自己测试的时候记得要把自己的工作相关的东西都先保存了,免得你的电脑内存太小,有可能会造成电脑关机。 那么我们在什么情况会使用这个大神的工具呢?就是自己程序有大的对象,并且GC一直无法把这个大对象回收,就可以使用上面的方法,能保证程序稳定运行。具体OH大神是怎么实现这种方式的呢?本文不做研究,他用的技术太深了。 最近ChatGPT比较火,我也问了些问题,回得很好,给个赞。你如果有啥想问的我也可以帮你问问它。 欢迎关注微信公众号:程序员xiaozhang 。会更新更多精彩内容。
2024-01-19
145
0
0
网络聚合
2024-01-19
VS2019发布至远程IIS部署流程
服务器部署 传统的开发将项目发布至本地桌面之后,复制至站点目录或通过FTP上传站点目录,有点小麻烦,通过开发工具VS2019本身集成的功能,可以一步到发布到远程IIS站点。 条件: VS系列发工具,例如VS2019,VS2022 Windows Server 操作系统 在目标服务器上面安装IIS,要注意默认安装IIS没有勾上“管理服务”,需要手动勾上。 安装Web Deploy v3.6,官网地址:Download Web Deploy v3.6 from Official Microsoft Download Center,安装成功之后可以在“服务”中找到相关服务。 IIS开启远程访问,默认是8172端口,需要关闭防火墙或打开“8172”端口。 配置站点“IIS管理器权限”,添加远程访问的账号。 基本服务器端部署就完成了。 客户端发布 默认情况下需要SSL支持,如果没有证书,可以在配置文件取消限制。 重新配置VS2019发布文件,取消限制,在<PropertyGroup>节点内加入如下代码: <AllowUntrustedCertificate>True</AllowUntrustedCertificate> 再次发布,站点己成功更新到服务器。 注意事项 appsettings.json配置在本地测试和服务器端并不完全一致,默认情况下每次都会将本地配置文件同步更新至服务器,可以通过配置pubxml文件发布时不同步更新文件。在<Project>节点中加入代码: <ItemGroup> <Content Remove="appsettings.Development.json" /> <Content Remove="appsettings.json" /> </ItemGroup> 整体部署流程基本结束,喜欢的朋友关注一下~
2024-01-19
140
0
0
网络聚合
2024-01-19
ArcGIS Pro发布地图服务(影像、矢量)
做GIS一般都是用ArcMap发布影像或者矢量服务,由于ArcGIS后续不在更新ArcMap,改用ArcGIS Pro,本文对ArcGIS Pro发布服务进行说明。 本文示例使用(因为portal的授权的版本只有10.5的,故使用10.5进行示例): 软件:ArcGIS Pro3.0.1(破解版), ArcGIS Portal10.5 当ArcGIS Pro和Portal不在一个机器或者版本不一样的时候,是可以连接使用的,需要在Portals配置好需要连接的Portal地址,并进行登录。 然后在发布服务前,先切换到已经登录好的Portal。 在使用ArcGIS Pro发布矢量、影像、地形等服务时,首先需要打开ArcGIS Pro,选择地图工程模板来进行发布。 注意:不能选择全局场景或局部场景,这两个场景下在相应菜单并没有发布地图选项!!! 1、发布影像切片服务 (1)选择需要切片的影像,拖动或者添加到地图中 (2)找到Share(共享)->Web Layer(Web图层)->Publish Web Layer(发布Web图层),打开Share As Web Layer(共享为Web图层)工具进行发布: 类型选择切片,并勾选共享对象,然后打开配置页,根据实际情况配置切片属性: 配置好切片属性,点击分析,没问题就可以点击发布了,发布好打开portal,在我的内容里就可以看到刚刚发布的服务了。 使用ArcGIS Pro发布的地图服务默认在Hosted目录下,建议在发布的时候自己创建一个新的目录或者选一个已存在的目录: 然后使用ArcGIS JS API就可以在Web上把影像加载上了(切的是一个全球影像): 2、发布矢量服务 (1)和影像数据的发布方式一样,先加载数据,然后选择发布web图层工具: 填好信息,分析后即可发布服务,发布好的服务如下所示:
2024-01-19
349
0
0
网络聚合
2024-01-19
【译】.NET 7 中的性能改进(三)
原文 | Stephen Toub 翻译 | 郑子铭 PGO 我在我的 .NET 6 性能改进一文中写了关于配置文件引导优化 (profile-guided optimization) (PGO) 的文章,但我将在此处再次介绍它,因为它已经看到了 .NET 7 的大量改进。 PGO 已经存在了很长时间,有多种语言和编译器。基本思想是你编译你的应用程序,要求编译器将检测注入应用程序以跟踪各种有趣的信息。然后你让你的应用程序通过它的步伐,运行各种常见的场景,使该仪器“描述”应用程序执行时发生的事情,然后保存结果。然后重新编译应用程序,将这些检测结果反馈给编译器,并允许它根据预期的使用方式优化应用程序。这种 PGO 方法被称为“静态 PGO”,因为所有信息都是在实际部署之前收集的,这是 .NET 多年来一直以各种形式进行的事情。不过,从我的角度来看,.NET 中真正有趣的开发是“动态 PGO”,它是在 .NET 6 中引入的,但默认情况下是关闭的。 动态 PGO 利用分层编译。我注意到 JIT 检测第 0 层代码以跟踪方法被调用的次数,或者在循环的情况下,循环执行了多少次。它也可以将它用于其他事情。例如,它可以准确跟踪哪些具体类型被用作接口分派的目标,然后在第 1 层专门化代码以期望最常见的类型(这称为“保护去虚拟化 (guarded devirtualization)”或 GDV)。你可以在这个小例子中看到这一点。将 DOTNET_TieredPGO 环境变量设置为 1,然后在 .NET 7 上运行: class Program { static void Main() { IPrinter printer = new Printer(); for (int i = 0; ; i++) { DoWork(printer, i); } } static void DoWork(IPrinter printer, int i) { printer.PrintIfTrue(i == int.MaxValue); } interface IPrinter { void PrintIfTrue(bool condition); } class Printer : IPrinter { public void PrintIfTrue(bool condition) { if (condition) Console.WriteLine("Print!"); } } } DoWork 的第 0 层代码最终看起来像这样: G_M000_IG01: ;; offset=0000H 55 push rbp 4883EC30 sub rsp, 48 488D6C2430 lea rbp, [rsp+30H] 33C0 xor eax, eax 488945F8 mov qword ptr [rbp-08H], rax 488945F0 mov qword ptr [rbp-10H], rax 48894D10 mov gword ptr [rbp+10H], rcx 895518 mov dword ptr [rbp+18H], edx G_M000_IG02: ;; offset=001BH FF059F220F00 inc dword ptr [(reloc 0x7ffc3f1b2ea0)] 488B4D10 mov rcx, gword ptr [rbp+10H] 48894DF8 mov gword ptr [rbp-08H], rcx 488B4DF8 mov rcx, gword ptr [rbp-08H] 48BAA82E1B3FFC7F0000 mov rdx, 0x7FFC3F1B2EA8 E8B47EC55F call CORINFO_HELP_CLASSPROFILE32 488B4DF8 mov rcx, gword ptr [rbp-08H] 48894DF0 mov gword ptr [rbp-10H], rcx 488B4DF0 mov rcx, gword ptr [rbp-10H] 33D2 xor edx, edx 817D18FFFFFF7F cmp dword ptr [rbp+18H], 0x7FFFFFFF 0F94C2 sete dl 49BB0800F13EFC7F0000 mov r11, 0x7FFC3EF10008 41FF13 call [r11]IPrinter:PrintIfTrue(bool):this 90 nop G_M000_IG03: ;; offset=0062H 4883C430 add rsp, 48 5D pop rbp C3 ret 而最值得注意的是,你可以看到调用[r11]IPrinter:PrintIfTrue(bool):这个做接口调度。但是,再看一下为第一层生成的代码。我们仍然看到调用[r11]IPrinter:PrintIfTrue(bool):this,但我们也看到了这个。 G_M000_IG02: ;; offset=0020H 48B9982D1B3FFC7F0000 mov rcx, 0x7FFC3F1B2D98 48390F cmp qword ptr [rdi], rcx 7521 jne SHORT G_M000_IG05 81FEFFFFFF7F cmp esi, 0x7FFFFFFF 7404 je SHORT G_M000_IG04 G_M000_IG03: ;; offset=0037H FFC6 inc esi EBE5 jmp SHORT G_M000_IG02 G_M000_IG04: ;; offset=003BH 48B9D820801A24020000 mov rcx, 0x2241A8020D8 488B09 mov rcx, gword ptr [rcx] FF1572CD0D00 call [Console:WriteLine(String)] EBE7 jmp SHORT G_M000_IG03 第一块是检查IPrinter的具体类型(存储在rdi中)并与Printer的已知类型(0x7FFC3F1B2D98)进行比较。如果它们不一样,它就跳到它在未优化版本中做的同样的接口调度。但如果它们相同,它就会直接跳到Printer.PrintIfTrue的内联版本(你可以看到这个方法中对Console:WriteLine的调用)。因此,普通情况(本例中唯一的情况)是超级有效的,代价是一个单一的比较和分支。 这一切都存在于.NET 6中,那么为什么我们现在要谈论它?有几件事得到了改善。首先,由于dotnet/runtime#61453这样的改进,PGO现在可以与OSR一起工作。这是一个大问题,因为这意味着做这种接口调度的热的长期运行的方法(这相当普遍)可以得到这些类型的去虚拟化/精简优化。第二,虽然PGO目前不是默认启用的,但我们已经让它更容易打开了。在dotnet/runtime#71438和dotnet/sdk#26350之间,现在可以简单地将 true 放入你的.csproj中。 csproj,它的效果和你在每次调用应用程序之前设置DOTNET_TieredPGO=1一样,启用动态PGO(注意,它不会禁止使用R2R图像,所以如果你希望整个核心库也采用动态PGO,你还需要设置DOTNET_ReadyToRun=0)。然而,第三,是动态PGO已经学会了如何检测和优化额外的东西。 PGO已经知道如何对虚拟调度进行检测。现在在.NET 7中,在很大程度上要感谢dotnet/runtime#68703,它也可以为委托做这件事(至少是对实例方法的委托)。考虑一下这个简单的控制台应用程序。 using System.Runtime.CompilerServices; class Program { static int[] s_values = Enumerable.Range(0, 1_000).ToArray(); static void Main() { for (int i = 0; i < 1_000_000; i++) Sum(s_values, i => i * 42); } [MethodImpl(MethodImplOptions.NoInlining)] static int Sum(int[] values, Func<int, int> func) { int sum = 0; foreach (int value in values) sum += func(value); return sum; } } 在没有启用PGO的情况下,我得到的优化汇编是这样的。 ; Assembly listing for method Program:Sum(ref,Func`2):int ; Emitting BLENDED_CODE for X64 CPU with AVX - Windows ; Tier-1 compilation ; optimized code ; rsp based frame ; partially interruptible ; No PGO data G_M000_IG01: ;; offset=0000H 4156 push r14 57 push rdi 56 push rsi 55 push rbp 53 push rbx 4883EC20 sub rsp, 32 488BF2 mov rsi, rdx G_M000_IG02: ;; offset=000DH 33FF xor edi, edi 488BD9 mov rbx, rcx 33ED xor ebp, ebp 448B7308 mov r14d, dword ptr [rbx+08H] 4585F6 test r14d, r14d 7E16 jle SHORT G_M000_IG04 G_M000_IG03: ;; offset=001DH 8BD5 mov edx, ebp 8B549310 mov edx, dword ptr [rbx+4*rdx+10H] 488B4E08 mov rcx, gword ptr [rsi+08H] FF5618 call [rsi+18H]Func`2:Invoke(int):int:this 03F8 add edi, eax FFC5 inc ebp 443BF5 cmp r14d, ebp 7FEA jg SHORT G_M000_IG03 G_M000_IG04: ;; offset=0033H 8BC7 mov eax, edi G_M000_IG05: ;; offset=0035H 4883C420 add rsp, 32 5B pop rbx 5D pop rbp 5E pop rsi 5F pop rdi 415E pop r14 C3 ret ; Total bytes of code 64 注意其中调用[rsi+18H]Func`2:Invoke(int):int:this来调用委托。现在启用了PGO。 ; Assembly listing for method Program:Sum(ref,Func`2):int ; Emitting BLENDED_CODE for X64 CPU with AVX - Windows ; Tier-1 compilation ; optimized code ; optimized using profile data ; rsp based frame ; fully interruptible ; with Dynamic PGO: edge weights are valid, and fgCalledCount is 5628 ; 0 inlinees with PGO data; 1 single block inlinees; 0 inlinees without PGO data G_M000_IG01: ;; offset=0000H 4157 push r15 4156 push r14 57 push rdi 56 push rsi 55 push rbp 53 push rbx 4883EC28 sub rsp, 40 488BF2 mov rsi, rdx G_M000_IG02: ;; offset=000FH 33FF xor edi, edi 488BD9 mov rbx, rcx 33ED xor ebp, ebp 448B7308 mov r14d, dword ptr [rbx+08H] 4585F6 test r14d, r14d 7E27 jle SHORT G_M000_IG05 G_M000_IG03: ;; offset=001FH 8BC5 mov eax, ebp 8B548310 mov edx, dword ptr [rbx+4*rax+10H] 4C8B4618 mov r8, qword ptr [rsi+18H] 48B8A0C2CF3CFC7F0000 mov rax, 0x7FFC3CCFC2A0 4C3BC0 cmp r8, rax 751D jne SHORT G_M000_IG07 446BFA2A imul r15d, edx, 42 G_M000_IG04: ;; offset=003CH 4103FF add edi, r15d FFC5 inc ebp 443BF5 cmp r14d, ebp 7FD9 jg SHORT G_M000_IG03 G_M000_IG05: ;; offset=0046H 8BC7 mov eax, edi G_M000_IG06: ;; offset=0048H 4883C428 add rsp, 40 5B pop rbx 5D pop rbp 5E pop rsi 5F pop rdi 415E pop r14 415F pop r15 C3 ret G_M000_IG07: ;; offset=0055H 488B4E08 mov rcx, gword ptr [rsi+08H] 41FFD0 call r8 448BF8 mov r15d, eax EBDB jmp SHORT G_M000_IG04 我选择了i => i * 42中的42常数,以使其在汇编中容易看到,果然,它就在那里。 G_M000_IG03: ;; offset=001FH 8BC5 mov eax, ebp 8B548310 mov edx, dword ptr [rbx+4*rax+10H] 4C8B4618 mov r8, qword ptr [rsi+18H] 48B8A0C2CF3CFC7F0000 mov rax, 0x7FFC3CCFC2A0 4C3BC0 cmp r8, rax 751D jne SHORT G_M000_IG07 446BFA2A imul r15d, edx, 42 这是从委托中加载目标地址到r8,并加载预期目标的地址到rax。如果它们相同,它就简单地执行内联操作(imul r15d, edx, 42),否则就跳转到G_M000_IG07,调用r8的函数。如果我们把它作为一个基准运行,其效果是显而易见的。 static int[] s_values = Enumerable.Range(0, 1_000).ToArray(); [Benchmark] public int DelegatePGO() => Sum(s_values, i => i * 42); static int Sum(int[] values, Func<int, int>? func) { int sum = 0; foreach (int value in values) { sum += func(value); } return sum; } 在禁用PGO的情况下,我们在.NET 6和.NET 7中得到了相同的性能吞吐量。 方法 运行时 平均值 比率 DelegatePGO .NET 6.0 1.665 us 1.00 DelegatePGO .NET 7.0 1.659 us 1.00 但当我们启用动态PGO(DOTNET_TieredPGO=1)时,情况发生了变化。.NET 6的速度提高了~14%,但.NET 7的速度提高了~3倍! 方法 运行时 平均值 比率 DelegatePGO .NET 6.0 1,427.7 ns 1.00 DelegatePGO .NET 7.0 539.0 ns 0.38 dotnet/runtime#70377是动态PGO的另一个有价值的改进,它使PGO能够很好地发挥循环克隆和不变量提升的作用。为了更好地理解这一点,简要地说说这些是什么。循环克隆 (Loop cloning) 是JIT采用的一种机制,以避免循环的快速路径中的各种开销。考虑一下本例中的Test方法。 using System.Runtime.CompilerServices; class Program { static void Main() { int[] array = new int[10_000_000]; for (int i = 0; i < 1_000_000; i++) { Test(array); } } [MethodImpl(MethodImplOptions.NoInlining)] private static bool Test(int[] array) { for (int i = 0; i < 0x12345; i++) { if (array[i] == 42) { return true; } } return false; } } JIT不知道传入的数组是否有足够的长度,以至于在循环中对数组[i]的所有访问都在边界内,因此它需要为每次访问注入边界检查。虽然简单地在前面进行长度检查,并在长度不够的情况下提前抛出一个异常是很好的,但这样做也会改变行为(设想该方法在进行时向数组中写入数据,或者以其他方式改变一些共享状态)。相反,JIT采用了 "循环克隆"。它从本质上重写了这个测试方法,使之更像这样。 if (array is not null && array.Length >= 0x12345) { for (int i = 0; i < 0x12345; i++) { if (array[i] == 42) // no bounds checks emitted for this access :-) { return true; } } } else { for (int i = 0; i < 0x12345; i++) { if (array[i] == 42) // bounds checks emitted for this access :-( { return true; } } } return false; 这样一来,以一些代码重复为代价,我们得到了没有边界检查的快速循环,而只需支付慢速路径中的边界检查。你可以在生成的程序集中看到这一点(如果你还不明白,DOTNET_JitDisasm是.NET 7中我最喜欢的功能之一)。 ; Assembly listing for method Program:Test(ref):bool ; Emitting BLENDED_CODE for X64 CPU with AVX - Windows ; Tier-1 compilation ; optimized code ; rsp based frame ; fully interruptible ; No PGO data G_M000_IG01: ;; offset=0000H 4883EC28 sub rsp, 40 G_M000_IG02: ;; offset=0004H 33C0 xor eax, eax 4885C9 test rcx, rcx 7429 je SHORT G_M000_IG05 81790845230100 cmp dword ptr [rcx+08H], 0x12345 7C20 jl SHORT G_M000_IG05 0F1F40000F1F840000000000 align [12 bytes for IG03] G_M000_IG03: ;; offset=0020H 8BD0 mov edx, eax 837C91102A cmp dword ptr [rcx+4*rdx+10H], 42 7429 je SHORT G_M000_IG08 FFC0 inc eax 3D45230100 cmp eax, 0x12345 7CEE jl SHORT G_M000_IG03 G_M000_IG04: ;; offset=0032H EB17 jmp SHORT G_M000_IG06 G_M000_IG05: ;; offset=0034H 3B4108 cmp eax, dword ptr [rcx+08H] 7323 jae SHORT G_M000_IG10 8BD0 mov edx, eax 837C91102A cmp dword ptr [rcx+4*rdx+10H], 42 7410 je SHORT G_M000_IG08 FFC0 inc eax 3D45230100 cmp eax, 0x12345 7CE9 jl SHORT G_M000_IG05 G_M000_IG06: ;; offset=004BH 33C0 xor eax, eax G_M000_IG07: ;; offset=004DH 4883C428 add rsp, 40 C3 ret G_M000_IG08: ;; offset=0052H B801000000 mov eax, 1 G_M000_IG09: ;; offset=0057H 4883C428 add rsp, 40 C3 ret G_M000_IG10: ;; offset=005CH E81FA0C15F call CORINFO_HELP_RNGCHKFAIL CC int3 ; Total bytes of code 98 G_M000_IG02部分正在进行空值检查和长度检查,如果任何一项失败,则跳转到G_M000_IG05块。如果两者都成功了,它就会执行循环(G_M000_IG03块)而不进行边界检查。 G_M000_IG03: ;; offset=0020H 8BD0 mov edx, eax 837C91102A cmp dword ptr [rcx+4*rdx+10H], 42 7429 je SHORT G_M000_IG08 FFC0 inc eax 3D45230100 cmp eax, 0x12345 7CEE jl SHORT G_M000_IG03 边界检查只显示在慢速路径块中。 G_M000_IG05: ;; offset=0034H 3B4108 cmp eax, dword ptr [rcx+08H] 7323 jae SHORT G_M000_IG10 8BD0 mov edx, eax 837C91102A cmp dword ptr [rcx+4*rdx+10H], 42 7410 je SHORT G_M000_IG08 FFC0 inc eax 3D45230100 cmp eax, 0x12345 7CE9 jl SHORT G_M000_IG05 这就是 "循环克隆"。那么,"不变量提升 (invariant hoisting) "呢?提升是指把某个东西从循环中拉到循环之前,而不变量是不会改变的东西。因此,不变量提升是指把某个东西从循环中拉到循环之前,以避免在循环的每个迭代中重新计算一个不会改变的答案。实际上,前面的例子已经展示了不变量提升,即边界检查被移到了循环之前,而不是在循环中,但一个更具体的例子是这样的。 [MethodImpl(MethodImplOptions.NoInlining)] private static bool Test(int[] array) { for (int i = 0; i < 0x12345; i++) { if (array[i] == array.Length - 42) { return true; } } return false; } 注意,array.Length - 42的值在循环的每次迭代中都不会改变,所以它对循环迭代是 "不变的",可以被抬出来,生成的代码就是这样做的。 G_M000_IG02: ;; offset=0004H 33D2 xor edx, edx 4885C9 test rcx, rcx 742A je SHORT G_M000_IG05 448B4108 mov r8d, dword ptr [rcx+08H] 4181F845230100 cmp r8d, 0x12345 7C1D jl SHORT G_M000_IG05 4183C0D6 add r8d, -42 0F1F4000 align [4 bytes for IG03] G_M000_IG03: ;; offset=0020H 8BC2 mov eax, edx 4439448110 cmp dword ptr [rcx+4*rax+10H], r8d 7433 je SHORT G_M000_IG08 FFC2 inc edx 81FA45230100 cmp edx, 0x12345 7CED jl SHORT G_M000_IG03 这里我们再次看到数组被测试为空(test rcx, rcx),数组的长度被检查(mov r8d, dword ptr [rcx+08H] then cmp r8d, 0x12345),但是在r8d中有数组的长度,然后我们看到这个前期块从长度中减去42(add r8d, -42),这是在我们继续进入G_M000_IG03块的快速路径循环前。这使得额外的操作集不在循环中,从而避免了每次迭代重新计算数值的开销。 好的,那么这如何适用于动态PGO呢?请记住,对于PGO能够做到的界面/虚拟调度的规避,它是通过进行类型检查,看使用的类型是否是最常见的类型;如果是,它就使用直接调用该类型方法的快速路径(这样做的话,该调用有可能被内联),如果不是,它就回到正常的界面/虚拟调度。这种检查可以不受循环的影响。因此,当一个方法被分层,PGO启动时,类型检查现在可以从循环中提升出来,使得处理普通情况更加便宜。考虑一下我们原来的例子的这个变化。 using System.Runtime.CompilerServices; class Program { static void Main() { IPrinter printer = new BlankPrinter(); while (true) { DoWork(printer); } } [MethodImpl(MethodImplOptions.NoInlining)] static void DoWork(IPrinter printer) { for (int j = 0; j < 123; j++) { printer.Print(j); } } interface IPrinter { void Print(int i); } class BlankPrinter : IPrinter { public void Print(int i) { Console.Write(""); } } } 当我们看一下在启用动态PGO的情况下为其生成的优化程序集时,我们看到了这个。 ; Assembly listing for method Program:DoWork(IPrinter) ; Emitting BLENDED_CODE for X64 CPU with AVX - Windows ; Tier-1 compilation ; optimized code ; optimized using profile data ; rsp based frame ; partially interruptible ; with Dynamic PGO: edge weights are invalid, and fgCalledCount is 12187 ; 0 inlinees with PGO data; 1 single block inlinees; 0 inlinees without PGO data G_M000_IG01: ;; offset=0000H 57 push rdi 56 push rsi 4883EC28 sub rsp, 40 488BF1 mov rsi, rcx G_M000_IG02: ;; offset=0009H 33FF xor edi, edi 4885F6 test rsi, rsi 742B je SHORT G_M000_IG05 48B9982DD43CFC7F0000 mov rcx, 0x7FFC3CD42D98 48390E cmp qword ptr [rsi], rcx 751C jne SHORT G_M000_IG05 G_M000_IG03: ;; offset=001FH 48B9282040F948020000 mov rcx, 0x248F9402028 488B09 mov rcx, gword ptr [rcx] FF1526A80D00 call [Console:Write(String)] FFC7 inc edi 83FF7B cmp edi, 123 7CE6 jl SHORT G_M000_IG03 G_M000_IG04: ;; offset=0039H EB29 jmp SHORT G_M000_IG07 G_M000_IG05: ;; offset=003BH 48B9982DD43CFC7F0000 mov rcx, 0x7FFC3CD42D98 48390E cmp qword ptr [rsi], rcx 7521 jne SHORT G_M000_IG08 48B9282040F948020000 mov rcx, 0x248F9402028 488B09 mov rcx, gword ptr [rcx] FF15FBA70D00 call [Console:Write(String)] G_M000_IG06: ;; offset=005DH FFC7 inc edi 83FF7B cmp edi, 123 7CD7 jl SHORT G_M000_IG05 G_M000_IG07: ;; offset=0064H 4883C428 add rsp, 40 5E pop rsi 5F pop rdi C3 ret G_M000_IG08: ;; offset=006BH 488BCE mov rcx, rsi 8BD7 mov edx, edi 49BB1000AA3CFC7F0000 mov r11, 0x7FFC3CAA0010 41FF13 call [r11]IPrinter:Print(int):this EBDE jmp SHORT G_M000_IG06 ; Total bytes of code 127 我们可以在G_M000_IG02块中看到,它正在对IPrinter实例进行类型检查,如果检查失败就跳到G_M000_IG05(mov rcx, 0x7FFC3CD42D98 then cmp qword ptr [rsi], rcx then jne SHORT G_M000_IG05),否则就跳到G_M000_IG03,这是一个紧密的快速路径循环,内联BlankPrinter.Print,看不到任何类型检查。 有趣的是,这样的改进也会带来自己的挑战。PGO导致了类型检查数量的大幅增加,因为专门针对某一特定类型的调用站点需要与该类型进行比较。然而,普通的子表达式消除 (common subexpression elimination)(CSE)在历史上并不适用这种类型的句柄(CSE是一种编译器优化,通过计算一次结果,然后存储起来供以后使用,而不是每次都重新计算,来消除重复的表达式)。dotnet/runtime#70580通过对这种常量句柄启用CSE来解决这个问题。例如,考虑这个方法。 [Benchmark] [Arguments("", "", "", "")] public bool AllAreStrings(object o1, object o2, object o3, object o4) => o1 is string && o2 is string && o3 is string && o4 is string; 在.NET 6上,JIT产生了这个汇编代码: ; Program.AllAreStrings(System.Object, System.Object, System.Object, System.Object) test rdx,rdx je short M00_L01 mov rax,offset MT_System.String cmp [rdx],rax jne short M00_L01 test r8,r8 je short M00_L01 mov rax,offset MT_System.String cmp [r8],rax jne short M00_L01 test r9,r9 je short M00_L01 mov rax,offset MT_System.String cmp [r9],rax jne short M00_L01 mov rax,[rsp+28] test rax,rax je short M00_L00 mov rdx,offset MT_System.String cmp [rax],rdx je short M00_L00 xor eax,eax M00_L00: test rax,rax setne al movzx eax,al ret M00_L01: xor eax,eax ret ; Total bytes of code 100 请注意,C#对字符串有四个测试,而汇编代码中的mov rax,offset MT_System.String有四个加载。现在在.NET 7上,加载只执行一次。 ; Program.AllAreStrings(System.Object, System.Object, System.Object, System.Object) test rdx,rdx je short M00_L01 mov rax,offset MT_System.String cmp [rdx],rax jne short M00_L01 test r8,r8 je short M00_L01 cmp [r8],rax jne short M00_L01 test r9,r9 je short M00_L01 cmp [r9],rax jne short M00_L01 mov rdx,[rsp+28] test rdx,rdx je short M00_L00 cmp [rdx],rax je short M00_L00 xor edx,edx M00_L00: xor eax,eax test rdx,rdx setne al ret M00_L01: xor eax,eax ret ; Total bytes of code 69 原文链接 Performance Improvements in .NET 7 本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。 欢迎转载、使用、重新发布,但务必保留文章署名 郑子铭 (包含链接: http://www.cnblogs.com/MingsonZheng/ ),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。 如有任何疑问,请与我联系 (
[email protected]
)
2024-01-19
157
0
0
网络聚合
2024-01-19
飞桨paddlespech 语音唤醒初探
PaddleSpeech提供了MDTC模型(paper: The NPU System for the 2020 Personalized Voice Trigger Challenge)在Hey Snips数据集上的语音唤醒(KWS)的实现。这篇论文是用空洞时间卷积网络(dilated temporal convolution network, DTCN)的方法来做的,曾获the 2020 personalized voice trigger challenge (PVTC2020)的第二名,可见这个方案是比较优秀的。想看看到底是怎么做的,于是我对其做了一番初探。 1,模型理解 论文是用空洞时间卷积网络(DTCN)的方法来实现的。为了减少参数量,用了depthwise & pointwise 一维卷积。一维卷积以及BatchNormal、relu等组成1个DTCNBlock, 4个DTCNBlock组成一个DTCNStack。实现的模型跟论文里的有一些差异。论文里的模型具体见论文,实现的模型框图见下图: 模型有PreProcess、DTCNStack(3个, DTCN:空洞时间卷积网络)、FCN(全连接网络)、sigmoid这些模块。PreProcess是做前处理,主要是由3个一维卷积(1个depthwise和两个pointwise)组成。每个DTCNStack由4个DTCNBlock组成,DTCNBlock跟preprocess模块相似,唯一的区别是多了残差模块(图中画红线的)。 这个模型的参数个数不到37K,见下图: 参数个数是比较少的,相对论文里的也少了不少。刚开始我不太相信,后来我对网络中的模型每层都算了参数个数,的确是这么多。想了一下,对比paper里的模型,参数变少主要有两点:一是少了一些模块,二是FCN由linear替代(linear替代FCN会少不少参数)。 模型用的特征是80维的mel-filter bank,即每帧的特征是一个80维的数据。把一个utterance的这些帧的特征作为模型的输入,输出是每一帧的后验概率,如果有一帧的后验概率大于threshold,就认为这一utterance是关键词,从而唤醒设备。举例来说,一个utterance有158帧,模型的输入就是158*80的矩阵(158是帧数,80是特征的维度),输出是158*1的矩阵,即158个后验概率。假设threshold设为0.8,这158个后验概率中只要有一个达到0.8,这个utterance就认为是关键词。 2,环境搭建 PaddleSpeech相关的文档里讲了如何搭建环境(Ubuntu下的),这里简述一下: 1)创建conda环境以及激活这个conda环境等: conda create --name paddletry python=3.7 conda activate paddletry 2)安装 paddelpaddle (paddlespeech 是基于paddelpaddle的) pip install paddlepaddle 3)clone 以及编译paddlespeech 代码 git clone https://github.com/PaddlePaddle/PaddleSpeech.git pip install . 3,数据集准备 数据集用的是sonos公司的”hey snips”。我几天内用三个不同的邮箱去注册申请,均没给下载链接,难道是跟目前在科技领域紧张的中美关系有关?后来联系到了这篇paper的作者, 他愿意分享数据集。在此谢谢他,真是个热心人!他用百度网盘分享了两次数据集,下载后均是tar包解压出错,估计是传输过程中出了问题。在走投无路的情况下尝试去修复坏的tar包。找到了tar包修复工具gzrt,运气不错,能修复大部分,关键是定义train/dev/test集的json文件能修复出来。如果自己写json文件太耗时耗力了。Json中一个wav文件数据格式大致如下: { "duration": 4.86, "worker_id": "0007cc59899fa13a8e0af4ed4b8046c6", "audio_file_path": "audio_files/41dac4fb-3e69-4fd0-a8fc-9590d30e84b4.wav", "id": "41dac4fb-3e69-4fd0-a8fc-9590d30e84b4", "is_hotword": 0 }, 数据集中原有wav文件96396个,修复了81401个。写python把在json中出现的但是audio_files目录中没有的去掉,形成新的json文件。原始的以及新的数据集中train/dev/test wav数如下: 从上表可以看出新的数据集在train/dev/test上基本都是原先的84%左右。 4,训练和评估 在PaddleSpeech/examples/hey_snips/kws0下做训练。训练前要把这个目录下conf/mdtc.yaml里的数据集的路径改成自己放数据集的地方。由于我用CPU训练,相应的命令就是./run.sh conf/mdtc.yaml 。 训练50个epoch(默认配置)后,在验证集下的准确率为99.79%(见下图),还是不错的,就没再训练下去。 评估出的DET图如下: Paddlespeech也提供了KWS推理命令: paddlespeech kws。需要研究一下这个命令是怎么用的,看相关代码。--input 后面既可以是一个具体的wav文件(这时只能评估一个文件),也可以是一个txt文件,把要评估的文件名都写在里面,具体格式如下图: --ckpt_path是模型的路径,--config是设置配置文件,也就是mdtc.yaml。因为要对整个测试集做评估,所以--input要写成txt的形式。Hey Snips数据集wav文件都在audio_files目录下,需要写脚本把测试集的wav文件取出来放在一个目录下(我的是heytest), 还要写脚本把这次测试文件的文件名以及路径写到上图所示的txt文件里。同时还要在paddlespeech 里加些代码看推理出的值是否跟期望值一致,做些统计。把这些都弄好后就开始做运行了,具体命令如下图: 最终测试集下的结果,见下图: 共19442个文件,跟期望一致的(图中correct的)是19410个,准确率为99.84%。与验证集下的大体相当。
2024-01-19
122
0
0
网络聚合
2024-01-19
ASP.NET Core如何知道一个请求执行了哪些中间件?
第一步,添加Nuget包引用 需要添加两个Nuget包分别是:Microsoft.AspNetCore.MiddlewareAnalysis和Microsoft.Extensions.DiagnosticAdapter,前者是分析记录中间件核心代码实现后者是用来接收日志输出的,由于是用的DiagnosticSource方式记录日志,所以需要使用DiagnosticListener对象的SubscribeWithAdapter方法来订阅。 第二步,实现一个分析诊断适配器 这个适配器是为了方便我们把从DiagnosticSource接收到的日志对象输出到控制台,具体代码实现如下 public class AnalysisDiagnosticAdapter { private readonly ILogger<AnalysisDiagnosticAdapter> _logger; public AnalysisDiagnosticAdapter(ILogger<AnalysisDiagnosticAdapter> logger) { _logger = logger; } [DiagnosticName("Microsoft.AspNetCore.MiddlewareAnalysis.MiddlewareStarting")] public void OnMiddlewareStarting(HttpContext httpContext, string name, Guid instance, long timestamp) { _logger.LogInformation($"中间件-启动: '{name}'; Request Path: '{httpContext.Request.Path}'"); } [DiagnosticName("Microsoft.AspNetCore.MiddlewareAnalysis.MiddlewareException")] public void OnMiddlewareException(Exception exception, HttpContext httpContext, string name, Guid instance, long timestamp, long duration) { _logger.LogInformation($"中间件-异常: '{name}'; '{exception.Message}'"); } [DiagnosticName("Microsoft.AspNetCore.MiddlewareAnalysis.MiddlewareFinished")] public void OnMiddlewareFinished(HttpContext httpContext, string name, Guid instance, long timestamp, long duration) { _logger.LogInformation($"中间件-结束: 耗时[{duration/10000}] '{name}'; Status: '{httpContext.Response.StatusCode}'"); } } 第三步,注册相关服务来启用分析中间件的功能 注册中间件分析服务 var builder = WebApplication.CreateBuilder(args); builder.Services.AddMiddlewareAnalysis(); 订阅我们的分析诊断适配器 var listener = app.Services.GetRequiredService<DiagnosticListener>(); var observer = ActivatorUtilities.CreateInstance<AnalysisDiagnosticAdapter>(app.Services); using var disposable = listener.SubscribeWithAdapter(observer); 这样基本就完成了分析记录中间件的功能,启动程序看看效果 日志已经成功的输出到我们的控制台了,不过才四个中间件,应该不止这么少的,再在注册中间件分析服务哪里添加一句代码 var builder = WebApplication.CreateBuilder(args); // 新增的下面这句代码 builder.Services.Insert(0, ServiceDescriptor.Transient<IStartupFilter, AnalysisStartupFilter>()); builder.Services.AddMiddlewareAnalysis(); 现在再来看看效果,发现变成8个中间件了多了四个 在Release模式编译后,运行发现中间件的执行效率非常高,几乎不占用时间 异常记录这里就不放图了,有兴趣的朋友自己去试试。 简单三步就可以知道一个请求到底执行了哪些中间件还是挺方便的。想知道实现原理可以去看看Microsoft.AspNetCore.MiddlewareAnalysis这个库,一共才四个文件看起来不费事。
2024-01-19
145
0
0
网络聚合
2024-01-19
P/Invoke之C#调用动态链接库DLL
本编所涉及到的工具以及框架: 1、Visual Studio 2022 2、.net 6.0 P/Invok是什么? P/Invoke全称为Platform Invoke(平台调用),其实际上就是一种函数调用机制,通过P/Invoke就可以实现调用非托管Dll中的函数。 在开始之前,我们首先需要了解C#中有关托管与非托管的区别 托管(Collocation),即在程序运行时会自动释放内存; 非托管,即在程序运行时不会自动释放内存。 废话不多说,直接实操 第一步: 打开VS2022,新建一个C#控制台应用 右击解决方案,添加一个新建项,新建一个"动态链接库(DLL)",新建完之后需要右击当前项目--> 属性 --> C/C++ --> 预编译头 --> 选择"不使用编译头" 在新建的DLL中我们新建一个头文件,用于编写我们的方法定义,然后再次新建一个C++文件,后缀以.c 结尾 第二步: 在我们DLL中的头文件(Native.h)中定义相关的Test方法,具体代码如下: #pragma once // 定义一些宏 #ifdef __cplusplus #define EXTERN extern "C" #else #define EXTERN #endif #define CallingConvention _cdecl // 判断用户是否有输入,从而定义区分使用dllimport还是dllexport #ifdef DLL_IMPORT #define HEAD EXTERN __declspec(dllimport) #else #define HEAD EXTERN __declspec(dllexport) #endif HEAD int CallingConvention Sum(int a, int b); 之后需要去实现头文件中的方法,在Native.c中实现,具体实现如下: #include "Native.h" // 导入头部文件 #include "stdio.h" HEAD int Add(int a, int b) { return a+b; } 在这些步骤做完后,可以尝试生成解决方案,检查是否报错,没有报错之后,将进入项目文件中,检查是否生成DLL (../x64/Debug) 第三步: 在这里之后,就可以在C#中去尝试调用刚刚所声明的方法,以便验证是否调用DLL成功,其具体实现如下: using System.Runtime.InteropServices; class Program { [DllImport(@"C:\My_project\C#_Call_C\CSharp_P_Invoke_Dll\x64\Debug\NativeDll.dll")] public static extern int Add(int a, int b); public static void Main(string[] args) { int sum = Add(23, 45); Console.WriteLine(sum); Console.ReadKey(); } } 运行结果为:68,证明我们成功调用了DLL动态链库 C#中通过P/Invoke调用DLL动态链库的流程 通过上述一个简单的例子,我们大致了解到了在C#中通过P/Invoke调用DLL动态链库的流程,接下我们将对C#中的代码块做一些改动,便于维护 在改动中我们将用到NativeLibrary类中的一个方法,用于设置回调,解析从程序集进行的本机库导入,并实现通过设置DLL的相对路径进行加载,其方法如下: public static void SetDllImportResolver (System.Reflection.Assembly assembly, System.Runtime.InteropServices.DllImportResolver resolver); 在使用这个方法前,先查看一下其参数 a、assembly: 主要是获取包含当前正在执行的代码的程序集(不过多讲解) b、resolber: 此参数是我们要注重实现的,我们可以通过查看他的元代码,发现其实现的是一个委托,因此我们对其进行实现。 原始方法如下: public delegate IntPtr DllImportResolver(string libraryName, Assembly assembly, DllImportSearchPath? searchPath); 实现resolver方法: const string NativeLib = "NativeDll.dll"; static IntPtr DllImportResolver(string libraryName, Assembly assembly, DllImportSearchPath? searchPath) { string dll = Path.Combine(new DirectoryInfo(Environment.CurrentDirectory).Parent.Parent.Parent.Parent.ToString(), "x64","Release", "NativeDll.dll"); // 此处为Dll的路径 //Console.WriteLine(dll); return libraryName switch { NativeLib => NativeLibrary.Load(dll, assembly, searchPath), _ => IntPtr.Zero }; } 该方法主要是用于区分在加载DLL时不一定只能是设置绝对路径,也可以使用相对路径对其加载,本区域代码是通过使用委托去实现加载相对路径对其DLL加载,这样做的好处是,便于以后需要更改DLL的路径时,只需要在这个方法中对其相对路径进行修改即可。 更新C#中的代码,其代码如下: using System.Reflection; using System.Runtime.InteropServices; class Program { const string NativeLib = "NativeDll.dll"; [DllImport(NativeLib)] public static extern int Add(int a, int b); static IntPtr DllImportResolver(string libraryName, Assembly assembly, DllImportSearchPath? searchPath) { string dll = Path.Combine(new DirectoryInfo(Environment.CurrentDirectory).Parent.Parent.Parent.Parent.ToString(), "x64","Release", "NativeDll.dll"); Console.WriteLine(dll); return libraryName switch { NativeLib => NativeLibrary.Load(dll, assembly, searchPath), _ => IntPtr.Zero }; } public static void Main(string[] args) { NativeLibrary.SetDllImportResolver(Assembly.GetExecutingAssembly(), DllImportResolver); int sum = Add(23, 45); Console.WriteLine(sum); Console.ReadKey(); } } 最后重新编译,检查其是否能顺利编译通过,最终我们的到的结果为:68 至此,我们就完成了一个简单的C#调用动态链接库的案例 下面将通过一个具体实例,讲述为什么要这样做?(本实例通过从性能方面进行对比) 在DLL中的头文件中,加入如下代码: HEAD void CBubbleSort(int* array, int length); 在.c文件中加入如下代码: HEAD void CBubbleSort(int* array, int length) { int temp = 0; for (int i = 0; i < length; i++) { for (int j = i + 1; j < length; j++) { if (array[i] > array[j]) { temp = array[i]; array[i] = array[j]; array[j] = temp; } } } } C#中的代码修改: using System.Diagnostics; using System.Reflection; using System.Runtime.InteropServices; class Program { const string NativeLib = "NativeDll.dll"; [DllImport(NativeLib)] public unsafe static extern void CBubbleSort(int* arr, int length); static IntPtr DllImportResolver(string libraryName, Assembly assembly, DllImportSearchPath? searchPath) { string dll = Path.Combine(new DirectoryInfo(Environment.CurrentDirectory).Parent.Parent.Parent.Parent.ToString(), "x64", "Release", "NativeDll.dll"); //Console.WriteLine(dll); return libraryName switch { NativeLib => NativeLibrary.Load(dll, assembly, searchPath), _ => IntPtr.Zero }; } public unsafe static void Main(string[] args) { int num = 10000; int[] arr = new int[num]; int[] cSharpResult = new int[num]; //随机生成num数量个(0-10000)的数字 Random random = new Random(); for (int i = 0; i < arr.Length; i++) { arr[i] = random.Next(10000); } //利用冒泡排序对其数组进行排序 Stopwatch sw = Stopwatch.StartNew(); Array.Copy(arr, cSharpResult, arr.Length); cSharpResult = BubbleSort(cSharpResult); Console.WriteLine($"\n C#实现排序所耗时:{sw.ElapsedMilliseconds}ms\n"); // 调用Dll中的冒泡排序算法 NativeLibrary.SetDllImportResolver(Assembly.GetExecutingAssembly(), DllImportResolver); fixed (int* ptr = &arr[0]) { sw.Restart(); CBubbleSort(ptr, arr.Length); } Console.WriteLine($"\n C实现排序所耗时:{sw.ElapsedMilliseconds}ms"); Console.ReadKey(); } //冒泡排序算法 public static int[] BubbleSort(int[] array) { int temp = 0; for (int i = 0; i < array.Length; i++) { for (int j = i + 1; j < array.Length; j++) { if (array[i] > array[j]) { temp = array[i]; array[i] = array[j]; array[j] = temp; } } } return array; } } 执行结果: C#实现排序所耗时: 130ms C实现排序所耗时:3ms 在实现本案例中,可能在编译后,大家所看到的结果不是很出乎意料,但这只是一种案例,希望通过此案例的分析,能给大家带来一些意想不到的收获叭。 最后 简单做一下总结叭,通过上述所描述的从第一步如何创建一个DLL到如何通过C#去调用的一个简单实例,也应该能给正在查阅相关资料的你有所收获,也希望能给在这方面有所研究的你有一些相关的启发,同时也希望能给目前对这方面毫无了解的你有一个更进一步的学习。 作者:百宝门-刘忠帅 原文地址:https://blog.baibaomen.com/p-invoke之c调用动态链接库dll/
2024-01-19
120
0
0
网络聚合
67
68
69
70
71