首页
烟花
归档
随笔
聚合
部落格精华
部落格资讯
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
自己动手从零写桌面操作系统GrapeOS系列教程——18.外设和IO
学习操作系统原理最好的方法是自己写一个简单的操作系统。 一、外设和I/O接口 前面我们介绍过冯·诺依曼结构包含5部分,其中输入设备和输出设备统称为外部设备,简称外设。常见的外设有鼠标、键盘、显示器、硬盘等。由于外设种类多、差异大、速度慢等原因,导致CPU无法直接与外设沟通。于是在CPU和外设之间产生了“中间人”,这个“中间人”就是I/O接口。如下图: CPU与外设的信息交流都是通过I/O接口来间接实现的。比如我们前面向屏幕输出字符,并不是CPU直接将数据传递给屏幕,而是先传递给显卡,显卡再去操控屏幕。显卡就是一种I/O接口。后面我们将要学习如何读写硬盘,同样CPU无法直接读写硬盘,而是通过“中间人”硬盘控制器来间接实现读写硬盘。硬盘控制器也是一种I/O接口。上图中只举了显示器和硬盘两个例子,实际还有很多,图中用省略号代表了,后面我们学到哪个再讲哪个。 二、I/O端口和端口访问 我们知道在CPU内部有一些寄存器,而在每个I/O接口上面也都有一些寄存器,通常叫做I/O端口。CPU与I/O接口的交流,主要就是读写这些I/O端口,也叫端口访问。 CPU访问I/O端口有两种方式,一种是通过内存地址访问,另一种是通过端口号来访问。 1.通过内存地址访问端口 这种方式就是将一部分内存地址映射到相应的端口上,CPU读写这些端口就和读写内存的指令是一样的。 2.通过端口号访问端口 这种方式是为每一个端口分配一个唯一的编号,叫端口号,然后通过端口号就可以读写这个端口。我们后面主要用这种。 我们需要注意2点: 在x86架构中端口号的最大取值范围是0~65535,也就是2个字节表示的范围。 端口作为一种寄存器,它的数据宽度有的是8位,也有的是16位。具体是多少位,我们用到的端口都会介绍。 三、端口访问代码 1.读端口代码 读端口总共有4种方式,代码如下: in al,dx in ax,dx in al,立即数 in ax,立即数 以上4行代码,每一行都表示从指定端口读取数据到al或ax寄存器中。 这里需要注意2点: 源操作数就是端口号,只能用dx或立即数表示,而且立即数只能在0~255范围内。 目的操作数只能是al或ax,如果端口是8位的就用al,如果端口是16位的就用ax。 2.写端口代码 写端口也有4种方式,代码如下: out dx,al out dx,ax out 立即数,al out 立即数,ax 以上4行代码,每一行都表示将al或ax中的数据写入到指定的端口中。 同样需要注意2点: 目的操作数就是端口号,只能用dx或立即数表示,而且立即数只能在0~255范围内。 源操作数只能是al或ax,如果端口是8位的就用al,如果端口是16位的就用ax。 本讲视频版地址:https://www.bilibili.com/video/BV1ig4y1b7cs/ 配套的代码与资料在:https://gitee.com/jackchengyujia/grapeos-course GrapeOS操作系统交流QQ群:643474045
2024-01-19
143
0
0
网络聚合
2024-01-19
我希望来年,更多是靠关系和模式挣钱——2022年我的总结与思考
我记得是2017年开始在博客园写博客,那年我儿子出生,并与当年年底写了17年的年度总结。 n年前,我没钱但年轻,我怕n年后我老时,还是一无所成——2017我的收获和反思,在随后的几年的年末,写总结文也已经成为了我的惯例,后面几年的总结文链接如下。 2018我跳出了舒适区,发现自己缺的不仅是技术,另外还得探索其它挣钱渠道,这一年里,我从外企跳到了一家互联网公司,开始从技术角度真正去探索高并发分布式等值钱技术。 2019我获得的些许成绩,能不能弥补这一年光阴的流逝?盘点2019我的得失,当年我用从互联网公司学到的技术又跳回了外企。 我不想安于当前的限度,以达到所谓的幸福,回顾下2020年的我。当年我依然在外企,开始习惯于外企的工作和生活平衡。 今年我拿到了期望中的收入,同时更希望能在睡后收入上有进一步的发展——2021年我的总结与思考,21年,也就是去年,我继续在外企苟着。 由于我一直在博客园首发年度总结,今年凭借这份香火之情,继续在这里发表年终总结,当然在这之前,也敷衍性地把若干篇在其他地方发表的高赞文章搬到了此地,不管怎样,总是希望博客园越办越好吧。 1 平台比努力重要,今年虽然收益减少,但我不能再挑剔 22年的年初一,我还去了上海城隍庙,那时人头攒动,大摆长龙,但不料从3月中旬到6月初,就一直是在家办公,而临近23年,依然是在家办公,彼此考虑更多的是身体。 论今年的收益,虽然本职收益没变,但兼职收益比起去年有所降低,而且这个降低的幅度不能说是“略有降低”,但本人好歹每月能按时拿到薪资,这样的情况,本人应当满足,来年的期望,除了身体康健以外,首要的还是先期待工作稳定吧。 今年对大家来说,不能说是容易,本人周围的店铺关张的有不少,而听到的公司倒闭情况,比往年也略有增多,应届生的就业难度,比往年也有所提升。来年开春不知情况如何,不过本人还是想说,平台的选择要优于能力,对待我们做IT这行的人来说,还是尽可能地找些能多抗风险的大公司吧。 2 出齐了Spring Boot到Cloud的书,但讲课收益不得不被降低 今年本人出了本Spring Cloud Alibaba的书,Spring cloud alibaba是spring cloud的第二代组件。 算上之前本人出的Spring Cloud第一代组件的书,以及Spring Boot的书,本人在Spring Boot+Cloud的书算是能成一个系列。 之后本人打算出本Spring Boot+全栈带案例的书,这样也算是能在这方面继续探索吧。 不过出书本身不挣钱,而出书外加大公司背景加持外加讲课,这才能挣到钱。由于众所周知的原因,今年能做线上课的平台普遍都不好,而线下课也很难开起来,所以讲课方面的收益降了不少。 只是希望来年万事可期,尽早恢复,这样本人尚能以驽钝之才多多挣些外快,以求在大城市里继续苟且偷生。同时更祝各位不甘人下的朋友,也尽快早日实现财务自由。 3 在某平台的发文,能做到日点击过3w,但似乎利用率不高 在某乎,如果搜本人的用户,“老胡聊Java”,应当能看到本人的文章,主要讲java面试。在22年,本人在某平台发文,高光之时能做到日点击过3万,即到了中午,当天所有文章累积点击过1万,到了下班时间,当天总点击过2万,到了晚上10点,总体点击过3万。同时本人在该平台发的一些文章,能在1,2个月内累计10w点击,这也算是本人发文的回报吧。 不过即使有这样的点击,能给本人带来的直接收益并不多。如何利用这些点击,如何于当下众多平台通过发文发视频给自己带来更多的收益,这也是本人后面需要重点考虑的问题。 不过本人想说的是,成系列的产品,比如系列视频系列文,或者说是自建一个网站运营,这才能带来更多的收益,如果是零散发文,可能看似热闹,但真无法带来直接的收益,这也算是本人持续发文后的一些感受吧。 4 年入百万极难,但不得不作为努力方向 本人在去年制定的22年乃至之后的目标是,年入百万。虽然在某乎平台上似乎是人均百万,但要做到这个太难。 比如在大平台,哪怕是架构,每天忙个不停,连睡觉时手机也得放身边,这样累计升级到资深架构,年入能有80w真不错了。 再如自己开公司,看似风光,手下好几十号人,做的项目报价也好几十万甚至百万,但一方面每天可能得如履薄冰,战战兢兢地维护客户维护产品,另一方面项目还有风险,到年底一算,除去房租人力成本,剩下的也真未必多,如果能比打工多些,那也该谢天谢地了。 本人私下揣度,本人靠薪资,尚能以年入六七十万为目标,如果再要在此方面更进一步,似乎得多找几个朋友,一方面找些性价比比较高的活,另一方面可能得自己接项目接活干了。这要比打工难太多,但万事不进则退,有时候只能是明知不可为而为之。 5 想想来年,还是以“苟住”作为第一目标吧 做最好希望,做最坏的打算。虽然一方面得多找机会,但在一些形势下,创业或开实体店,确实可能属于逆势而为。 所以这里本人还是无奈地讲,来年首要的,还是先苟住吧,先确保工资收益,先确保家人和自身的身体,后面的情况,谁也预料不到,但总是先期待一切会变好吧。 或者再说句悲观的话,不管怎样,生活总是要继续,希望来年至少本人能少些“逆来顺受”吧。 6 期望春暖花开,期待万事好转 时代的尘埃,落在个人的身上,都是一座大山,但与其抱怨,还不如多做。可能当下做的努力收效甚微,但只能是这样说,多做总比放弃要好很多,毕竟做了就有希望,做了才能知道该努力的方向。 这张图是上海龙华寺的,从2017年的年末总结文到现在,我是一直用这张图为我自己,为我的家人和为大家祈福。 在22年的年底,再次用此图,祝福各位全家身体健康,诸事圆成,更希望所有人,包括我自己,能在新的一年里能心想事成。
2024-01-19
269
0
0
网络聚合
2024-01-19
如何管理项目干系人
一、什么是项目干系人? 项目干系人是指积极参与项目或其利益可能受项目积极或消极影响的个人、组织。我们以软件项目为例,大家也可以将项目干系人视为在软件项目中拥有既得利益的任何人,包括员工、客户、供应商、投资者、合作伙伴,甚至竞争对手。他们的目标和优先级可能会与团队不同。 二、什么是项目干系人管理? 我们可以这样定义项目干系人管理: 识别、参与并与项目干系人保持联系以确保项目总体成功的持续过程。通过管理或影响项目干系人,我们可以推动项目的成功。同时,对于企业战略来说,我们可以帮助企业识别出项目干系人,了解他们的需求和期望,在满足项目干系人的需求的基础上,实现企业的发展战略。 三、如何管理项目干系人? 1、想要更好地管理项目干系人,我们需要注意以下技巧 保持沟通畅通、有效:我们应该更及时、快速地了解到他们最新的需求; 明确要求:我们需要从项目一开始就明确项目干系人需要做什么、交付什么以及交付的截止日期; 建立关系:与项目干系人建立良好的关系是成功管理项目干系人的必要条件。我们应该更了解他们以及他们要关心的内容,并与他们建立一个融洽的关系; 及时反 馈:项目干系人所反馈的问题以及他们的担忧,我们应及时有效地进行处理。这反映出我们认真对待了他们的意见,并能及时对他们的需求做出回应。 2、管理项目干系人:决定我们想从项目干系人那里得到什么 首先我们需要考虑这些方面: 项目干系人在项目中的角色是什么? 项目干系人的影响力有多大? 项目干系人对项目的兴趣是什么? 项目干系人对项目成功起了多大的推动作用? 然后,让项目干系人在项目早期就参与进来,并定期与他们保持有效的沟通和联系,确保他们对项目的接受和支持。我们可以通过以下几种方式与项目干系人建立联系: 定期的会议同步; 发送产品版本更新及产品报告; 进行干系人调研。 3、管理项目干系人:频繁沟通 与项目干系人的沟通是项目管理的关键点。对于项目干系人来说,他们需要知道在整个项目过程中发生了什么,然后针对这些变化表达自己的意见或建议。 因此,频繁沟通能够帮助项目干系人与项目团队之间建立起信任和理解关系,同样也能够帮助他们参与到项目中,了解项目的重要性。我们可以通过几种不同的方式与项目干系人进行沟通:电子邮件、电话、与团队的定期会议以及项目管理协作工具等。 4、管理项目干系人:应公开透明 将目标和目的透明化:对于项目干系人来说,比较重要的一点是他们需要知道我们想要实现什么,以及为什么要这样做。当涉及到与他们的沟通时,要尽可能地解释清楚,这样能够加强双方之间的信任关系; 明确项目时间表:明确项目时间表及时间节点,能够让项目干系人明确知道他们什么时候可以得到交付成果; 明确项目风险:在管理风险时,我们应该提前告知可能会遇到的风险,并做好相应的预防措施; 明确 潜在挑战:当我们在项目过程中遇到了一些潜在的障碍或挑战时,应及时告知项目干系人,听听他们的想法。 5、管理项目干系人:应处事灵活 在管理项目干系人时,我们应灵活变通。项目干系人的需求会随着时间的推移而产生变化,这时我们应积极地拥抱变化,并根据需要调整自己的管理方式与处理方法。请记住以下小技巧: 乐于改变:根据项目干系人的反馈,不断地尝试新事物、调整计划; 对项目干系人的要求做出回应:如果项目干系人要求什么,尝试满足他们; 适当妥协:对于项目干系人的一些要求,我们可以尝试找到一个让双方都满意的中间地带。 项目干系人的管理是一门学问,总结来说就是以下三步: 对项目干系人进行调研、分析; 了解他们的需求和意愿、想法; 针对干系人的不同需求提出相应的解决措施。 干系人管理是一个持续不断的过程,也是项目管理中不可忽视的一部分,希望通过项目干系人管理,让大家能够“ 兵来将挡,水来土掩”!
2024-01-19
209
0
0
网络聚合
2024-01-19
游戏设计之-排行榜处理
前言 我相信大部分人,乃至公司和团队在设计排行榜都考虑的是redis,zadd操作,不需要排序,维护获取,操作都极其简单; 无一例外我也是; 在项目中运营了大量的模板,来处理各个木块的排行榜信息; 统一的会在晚上又一些结算处理;就牵涉一次性拉取,过滤,发放奖励,甚至还有删除操作; 同时为了节约运营陈本,定期还需要对redis里面无效数据进行删除,收缩操作; 目前redis已经达到6.55g,这只是其中一个平台运营部署; 运营过项目的都知道云redis的费用是非常高昂的;所以就需要定期做一些无用数据的删除操作; 可是问题来了,随着运营时间的推移,每天早上总能看到如此的异常推送; 就问你揪心不?仔细一查才发现还是数据库相关的操作导致的卡主了; 于是下定决心对排行榜等相关功能进行调整; 削峰 1. 首先就是对数据库的一些删除操作,集中早上5-6点去处理他;这个时候在线玩家最少;处理一些垃圾数据最为合理;基本卡了也不会对玩家的感觉特别强烈反馈; 于是把原来处理的数据移除的代码调整为定时器创建异步任务在凌晨5-6点去处理; 2. 第二步就是为了避免玩家集体拉取数据,并对数据进行操作;考虑重构排行榜代码需求, 排行榜自定义实现 为了模拟排行榜实现的, 首先应该考虑,排行榜数据,既方便查找修改,又方便排序; 那么肯定需要考虑的是 一个map,用于存取和玩家相关积分数据; 一个list,用于排序积分; 首先我们来实现一个积分类 1 package org.wxd.lang.rank; 2 3 import lombok.Getter; 4 import lombok.Setter; 5 import lombok.experimental.Accessors; 6 import org.wxd.io.ObjectFactory; 7 import org.wxd.timer.TimeUtil; 8 9 import java.util.Comparator; 10 import java.util.Objects; 11 12 /** 13 * 排行 14 * 15 * @author: Troy.Chen(無心道, 15388152619) 16 * @version: 2022-12-08 21:27 17 **/ 18 @Getter 19 @Setter 20 @Accessors(chain = true) 21 public class RankScore implements Comparable<RankScore> { 22 23 /** 正序 */ 24 public static final Comparator<RankScore> Sort = (o1, o2) -> { 25 if (o1.score != o2.score) { 26 return Double.compare(o1.score, o2.score); 27 } 28 29 if (o1.scoreTime != o2.scoreTime) { 30 /**时间取值要倒叙*/ 31 return Long.compare(o2.scoreTime, o1.scoreTime); 32 } 33 34 return Long.compare(o1.uid, o2.uid); 35 }; 36 37 /** 倒叙 */ 38 public static final Comparator<RankScore> BreSort = (o1, o2) -> { 39 if (o1.score != o2.score) { 40 return Double.compare(o1.score, o2.score); 41 } 42 43 if (o1.scoreTime != o2.scoreTime) { 44 /**时间取值要倒叙*/ 45 return Long.compare(o2.scoreTime, o1.scoreTime); 46 } 47 48 return Long.compare(o1.uid, o2.uid); 49 }; 50 51 private long uid; 52 private double score; 53 private long scoreTime; 54 55 public RankScore setScore(double score) { 56 this.score = score; 57 this.scoreTime = TimeUtil.currentTimeMillis(); 58 return this; 59 } 60 61 @Override public int compareTo(RankScore o2) { 62 return Sort.compare(this, o2); 63 } 64 65 public int scoreIntValue() { 66 return (int) score; 67 } 68 69 public long scoreLongValue() { 70 return (long) score; 71 } 72 73 @Override public boolean equals(Object o) { 74 if (this == o) return true; 75 if (o == null || getClass() != o.getClass()) return false; 76 RankScore rankScore = (RankScore) o; 77 return uid == rankScore.uid; 78 } 79 80 @Override public int hashCode() { 81 return Objects.hash(uid); 82 } 83 84 @Override public String toString() { 85 return ObjectFactory.stringBuilder(sb -> { 86 sb.append(this.getClass().getSimpleName()).append("{"); 87 sb.append("uid=").append(uid); 88 sb.append(", score=").append(score); 89 sb.append('}'); 90 }); 91 } 92 } View Code 接下来我实现代码 1 package test; 2 3 import org.junit.Test; 4 import org.wxd.lang.RandomUtils; 5 import org.wxd.lang.rank.RankScore; 6 import org.wxd.system.MarkTimer; 7 8 import java.util.ArrayList; 9 import java.util.HashMap; 10 import java.util.List; 11 12 /** 13 * @author: Troy.Chen(無心道, 15388152619) 14 * @version: 2022-12-08 20:48 15 **/ 16 public class RankPackTest { 17 18 public class RankPack { 19 HashMap<Long, RankScore> rankMap = new HashMap<>(); 20 List<Long> rankList = new ArrayList<>(); 21 22 public void addScore(long uid, double score) { 23 /*忽律并发问题 可以自行改为 ConcurrentHashMap*/ 24 rankMap.computeIfAbsent(uid, l -> { 25 RankScore rankScore = new RankScore().setUid(uid).setScore(score); 26 /*能到这里初始化,那么list里面必然也没有数据*/ 27 rankList.add(uid); 28 return rankScore; 29 }); 30 } 31 32 public void sort() { 33 /*忽律并发问题*/ 34 rankList.sort((o1, o2) -> { 35 RankScore r1 = rankMap.get(o1); 36 RankScore r2 = rankMap.get(o2); 37 return r1.compareTo(r2); 38 }); 39 } 40 41 public void breSort() { 42 /*忽律并发问题*/ 43 rankList.sort((o1, o2) -> { 44 RankScore r1 = rankMap.get(o1); 45 RankScore r2 = rankMap.get(o2); 46 return r2.compareTo(r1); 47 }); 48 } 49 50 } 51 52 RankPack rankPack = new RankPack(); 53 54 @Test 55 public void test() { 56 init(); 57 sort(); 58 sort(); 59 sort(); 60 sort2(); 61 sort2(); 62 sort2(); 63 } 64 65 public int randomScore() { 66 return RandomUtils.random(100, 10000); 67 } 68 69 public void init() { 70 MarkTimer build = MarkTimer.build(); 71 int speed = 1; 72 for (int i = 0; i < 300; i++) { 73 rankPack.addScore(speed, randomScore()); 74 speed++; 75 } 76 rankPack.breSort(); 77 float v = build.execTime(); 78 System.out.println(rankPack.getClass().getSimpleName() + " - 数量:" + rankPack.rankMap.size() + ", 全部排序耗时:" + v); 79 show(); 80 } 81 82 public void sort() { 83 MarkTimer build = MarkTimer.build(); 84 rankPack.rankMap.values().forEach(rankScore -> rankScore.setScore(randomScore())); 85 rankPack.breSort(); 86 float v = build.execTime(); 87 System.out.println(rankPack.getClass().getSimpleName() + " - 数量:" + rankPack.rankMap.size() + ", 全部排序耗时:" + v); 88 show(); 89 } 90 91 public void sort2() { 92 MarkTimer build = MarkTimer.build(); 93 RankScore randomItem = (RankScore) RandomUtils.randomItem(rankPack.rankMap.values()); 94 randomItem.setScore(randomScore()); 95 rankPack.breSort(); 96 float v = build.execTime(); 97 show(); 98 int index = rankPack.rankList.indexOf(randomItem.getUid()); 99 System.out.println(v + " - " + randomItem + " - 当前排名:" + (index + 1)); 100 } 101 102 public void show() { 103 // AtomicInteger atomicInteger = new AtomicInteger(); 104 // for (int i = 0; i < 10; i++) { 105 // Long uid = rankPack.rankList.get(i); 106 // RankScore rankScore = rankPack.rankMap.get(uid); 107 // System.out.println(rankScore.toString() + " - 排名:" + (i + 1)); 108 // } 109 } 110 111 } 运行一下看看效果 视乎性能还可以,等等,我们是不是忽律一个问题,现在排行榜只有300个对象哦,加到3万试试 很明显看得出来,全排序情况下直接翻了30倍左右;单个项修改后排序直接翻了差不多40倍; 到这里或许有很多已经满足需求了,; 可是我的需求远不止如此,这很明显时间消耗太久了,不知道有么有更为适合的方案来处理性能; 突然想到一个问题,或许了解树结构的同学知道,树装结构数据,在插入数据的时候就已经排序了,并且根据hash索引,性能非常高; 由此我们想到, 那么我们排序很耗时;我能不能不做这个排序操作,通过索引数据结构来达到目的呢? 改造树装结构 我们把刚才hashmap改为treemap 吧list改为treeset试试效果 测试一下,感觉确实是快了很多呢 我们来打印一下数据看看情况 哦豁,为什么数据不对; 不是我们预期的效果, 然后研究了treeset数据结构就知道,它是排序的,但是他是add的时候排序的,一旦add就不再变更了; 那我们能不能尝试修改的时候先移除,再修改,然后在add呢? 我们来改造一下addScore方法块; 排序正常了 可以看出运行结构,整体差距不算特别大; 如果考虑并发性能问题;可以把 TreeMap 换成 ConcurrentSkipListMap TreeSet 换成 ConcurrentSkipListSet java自带的跳表结构,添加删除查询,都会非常高效; 最后粘贴一下最新的全部测试代码 1 package test; 2 3 import org.junit.Test; 4 import org.wxd.lang.RandomUtils; 5 import org.wxd.lang.rank.RankScore; 6 import org.wxd.system.MarkTimer; 7 8 import java.util.concurrent.ConcurrentSkipListMap; 9 import java.util.concurrent.ConcurrentSkipListSet; 10 11 /** 12 * @author: Troy.Chen(無心道, 15388152619) 13 * @version: 2022-12-08 20:48 14 **/ 15 public class RankPackTest { 16 17 public class RankPack { 18 ConcurrentSkipListMap<Long, RankScore> rankMap = new ConcurrentSkipListMap<>(); 19 ConcurrentSkipListSet<RankScore> rankList = new ConcurrentSkipListSet<>(); 20 21 public void addScore(long uid, double score) { 22 /*忽律并发问题 可以自行改为 ConcurrentHashMap*/ 23 RankScore score1 = rankMap.computeIfAbsent(uid, l -> { 24 RankScore rankScore = new RankScore().setUid(uid); 25 return rankScore; 26 }); 27 rankList.remove(score1); 28 score1.setScore(score); 29 rankList.add(score1); 30 } 31 32 // public void sort() { 33 // /*忽律并发问题*/ 34 // rankList.sort((o1, o2) -> { 35 // RankScore r1 = rankMap.get(o1); 36 // RankScore r2 = rankMap.get(o2); 37 // return r1.compareTo(r2); 38 // }); 39 // } 40 // 41 // public void breSort() { 42 // /*忽律并发问题*/ 43 // rankList.sort((o1, o2) -> { 44 // RankScore r1 = rankMap.get(o1); 45 // RankScore r2 = rankMap.get(o2); 46 // return r2.compareTo(r1); 47 // }); 48 // } 49 50 } 51 52 RankPack rankPack = new RankPack(); 53 54 @Test 55 public void test() { 56 sort(); 57 sort(); 58 sort(); 59 sort(); 60 sort2(); 61 sort2(); 62 sort2(); 63 sort2(); 64 } 65 66 public int randomScore() { 67 return RandomUtils.random(100, 10000); 68 } 69 70 public void sort() { 71 MarkTimer build = MarkTimer.build(); 72 int speed = 1; 73 for (int i = 0; i < 30000; i++) { 74 rankPack.addScore(speed, randomScore()); 75 speed++; 76 } 77 // rankPack.breSort(); 78 float v = build.execTime(); 79 System.out.println(rankPack.getClass().getSimpleName() + " - 数量:" + rankPack.rankList.size() + ", 全部排序耗时:" + v); 80 show(); 81 } 82 83 public void sort2() { 84 MarkTimer build = MarkTimer.build(); 85 RankScore randomItem = (RankScore) RandomUtils.randomItem(rankPack.rankMap.values()); 86 rankPack.addScore(randomItem.getUid(), randomScore()); 87 // rankPack.breSort(); 88 float v = build.execTime(); 89 show(); 90 int index = 0; 91 for (RankScore rankScore : rankPack.rankList) { 92 if (rankScore.getUid() == randomItem.getUid()) break; 93 index++; 94 } 95 System.out.println(v + " - " + randomItem + " - 当前排名:" + (index + 1)); 96 } 97 98 public void show() { 99 int i = 0; 100 for (RankScore rankScore : rankPack.rankList) { 101 System.out.println(rankScore.toString() + " - 排名:" + (i + 1)); 102 i++; 103 if (i >= 10) break; 104 } 105 } 106 107 } 大家如果有兴趣可以自己测试哦; 好了这里提供集中思路来处理排行榜相关的数据;以及排名功能; 不知道园子里的朋友们,还有没有更好的思路; 听说大家喜欢看,机会是留给有耐心的人
2024-01-19
139
0
0
网络聚合
2024-01-19
C#开发PACS医学影像三维重建(十四):基于能量模型算法将曲面牙床展开至二维平面
在医学影像领域中,将三维重建中的人体组织展开平铺至二维,用来研判病灶和制定治疗方案的重要手段之一, 它能够将立体曲面所包含的信息更为直观的展示到二维平面上,常用的情景包括: 牙床全景图、平铺血管、骨骼二维化展开(肋骨平铺)。 众所周知,人体牙床正常情况下是有弧度的,无论是从俯视位还是冠状位观察都是不能直观的了解牙齿状况, 或多或少的都会被其他组织或牙齿遮挡,如下图所示: 所以我们要将三维或二维的影像拉伸后平铺到桌面上,目前主流曲面展开算法有如下几种: ①元素法 ②旋转正交矩阵法 ③迭代应变能量释放法 本文将根据网络查询现有的算法粗略介绍用能量法展开牙床: 基于弹簧质点系统建立能量模型: 弹性变形能E和弹性力f的计算式为: 判断展开标准: 曲面展开算法示例: 以VTK中圆柱体为例,将一根圆柱展开为一个矩形平面的部分代码: void Cylinder_Expansion(vtkPolyData* srcData, vtkPolyData* destData, GEO_CYLINDER src_Cylinder) { vtkSmartPointer<vtkPoints>srcPoints = srcData->GetPoints(); vtkSmartPointer<vtkPoints>destPoints = vtkSmartPointer<vtkPoints>::New(); int num = srcPoints->GetNumberOfPoints(); double p[3],r[3],cross[3]; double v0[3] = { 0 }, v1[3] = {0}; v0[src_Cylinder.RdTran] = src_Cylinder.CenterTran; v0[src_Cylinder.RdLong] = src_Cylinder.CenterLong; v1[src_Cylinder.RdTran] = src_Cylinder.R; v1[src_Cylinder.RdLong] = 0; double arc_len; for (int i = 0; i < num; ++i) { srcPoints</span>-><span style="color: rgba(0, 0, 0, 1)">GetPoint(i, p); v0[src_Cylinder.Axial]</span>=<span style="color: rgba(0, 0, 0, 1)"> p[src_Cylinder.Axial]; vtkMath::Subtract(p, v0, p); arc_len </span>=<span style="color: rgba(0, 0, 0, 1)"> vtkMath::AngleBetweenVectors(v1,p); vtkMath::Cross(v1, p, cross); </span><span style="color: rgba(0, 0, 255, 1)">if</span> (cross[src_Cylinder.Axial]<<span style="color: rgba(128, 0, 128, 1)">0</span><span style="color: rgba(0, 0, 0, 1)">)<br> arc_len </span>= vtkMath::Pi()*<span style="color: rgba(128, 0, 128, 1)">2</span>-<span style="color: rgba(0, 0, 0, 1)">arc_len; r[src_Cylinder.RdTran]</span>= src_Cylinder.CenterTran+ arc_len*<span style="color: rgba(0, 0, 0, 1)">src_Cylinder.R; r[src_Cylinder.Axial] </span>=<span style="color: rgba(0, 0, 0, 1)"> v0[src_Cylinder.Axial]; r[src_Cylinder.RdLong] </span>=sqrt(p[<span style="color: rgba(128, 0, 128, 1)">0</span>] * p[<span style="color: rgba(128, 0, 128, 1)">0</span>] + p[<span style="color: rgba(128, 0, 128, 1)">1</span>] * p[<span style="color: rgba(128, 0, 128, 1)">1</span>] + p[<span style="color: rgba(128, 0, 128, 1)">2</span>] * p[<span style="color: rgba(128, 0, 128, 1)">2</span>]) -<span style="color: rgba(0, 0, 0, 1)"> src_Cylinder.R;</span> destPoints-><span style="color: rgba(0, 0, 0, 1)">InsertPoint(i,r); }</span> destData-><span style="color: rgba(0, 0, 0, 1)">SetPoints(destPoints); } 运行结果: 将此算法应用到医学影像中,开始定位展开锚点: 现在就可以在同一屏画面中得到了各种视角的牙床图: 同理,还可以应用到血管和骨骼的平铺展开,更好的观察血管阻塞和破裂、骨折骨裂等情况。
2024-01-19
276
0
0
网络聚合
2024-01-19
算法 | 数字图像处理之「中值滤波」
中值滤波原理 中值滤波就是用一个奇数点的移动窗口(要求奇数主要是为了保证整个模板有唯一中心元素),将窗口中心点的值用窗口内各点的中值代替。假设窗口内有5点,其值为80、90、200、110和120,那么此窗口内各点的中值即为110。 设有一个一维序列\(f_1,f_2,...,f_n\),取窗口长度(点数)为m(m为奇数),对其进行中值滤波,就是从输入序列中相机抽出m个数\(f_{i-v},...,f_{i-1},f_i,f_{i+1},...,f_{i+v}\)(其中\(f_i\)为窗口中心点值,\(v=(m-1)/2\)),再将这m个点按其数值大小排序,取其序号为中心点的那个数作为滤波输出。用数学公式表示为: \[y_i=Median\{f_{i-v},...,f_{i-1},f_i,f_{i+1},...,f_{i+v}\},i\in N,v=\frac{m-1}{2} \] 如:以3*3的领域为例求中值滤波中像素5的值。 int pixel[9]中存储像素1,像素2...像素9的值; 对数组pixel进行排序操作; 像素5的值即为数组pixel的中值pixel[4]。 代码实现 void medianFilter(cv::Mat& src, cv::Mat& dst, cv::Size size) { /*step1:判断窗口size是否为奇数*/ if (size.width % 2 == 0 || size.height % 2 == 0) { cout << "卷积核窗口大小应为奇数!\n"; exit(-1); } /*step2:对原图进行边界扩充*/ int h = (size.height - 1) / 2; int w = (size.width - 1) / 2; Mat src_border; copyMakeBorder(src, src_border, h, h, w, w, BORDER_REFLECT_101); /*step3:卷积操作*/ map<uchar, Point> mp; // 定义容器存储每个卷积窗口内各像素点的<像素值, 像素位置> for (int i = h; i < src.rows + h; i++) { for (int j = w; j < src.cols + w; j++) { mp.clear(); for (int ii = i - h; ii <= i + h; ii++) { for (int jj = j - w; jj <= j + w; jj++) { Point point(jj, ii); uchar value; if (src.channels() == 1) { // 灰度图像,存储灰度值 value = src_border.at<uchar>(ii, jj); }else { // 彩色图像,存储亮度值 uchar value_b = src_border.at<cv::Vec3b>(ii, jj)[0]; uchar value_g = src_border.at<cv::Vec3b>(ii, jj)[1]; uchar value_r = src_border.at<cv::Vec3b>(ii, jj)[2]; value = 0.114 * value_b + 0.587 * value_g + 0.299 * value_r; } mp[value] = point; } } // 将窗口中心点的值用窗口内个点的中值代替 auto iter = mp.begin(); int count = 0; Point pixel; int median_size = mp.size() / 2; while (iter != mp.end()) { if (count == median_size) { pixel = Point(iter->second.x, iter->second.y); break; } count++; iter++; } if (src.channels() == 1) { dst.at<uchar>(i - h, j - w) = src_border.at<uchar>(pixel.y, pixel.x); } else { dst.at<cv::Vec3b>(i - h, j - w)[0] = src_border.at<cv::Vec3b>(pixel.y, pixel.x)[0]; dst.at<cv::Vec3b>(i - h, j - w)[1] = src_border.at<cv::Vec3b>(pixel.y, pixel.x)[1]; dst.at<cv::Vec3b>(i - h, j - w)[2] = src_border.at<cv::Vec3b>(pixel.y, pixel.x)[2]; } } } } 代码讲解 copyMakeBorder(src,src_border,h,h,w,w,BORDER_REFLECT_101); 在模板或卷积的加权运算中的图像边界问题:当在图像上移动模板(卷积核)至图像边界时,在原图像中找不到与卷积核中的加权系数相对应的N个像素(N为卷积核元素个数),即卷积核悬挂在图像的边界上,这种现象在图像的上下左右四个边界上均会出现。例如,当模板为: \[\frac{1}{9} \begin{bmatrix} %该矩阵一共3列,每一列都居中放置 1 & 1 & 1\\ %第一行元素 1 & 1 & 1\\ %第二行元素 1 & 1 & 1\\ %第二行元素 \end{bmatrix} \] 设原图像为: \[\begin{bmatrix} %该矩阵一共3列,每一列都居中放置 1 & 1 & 1 & 1 & 1\\ %第1行元素 2 & 2 & 2 & 2 & 2\\ %第2行元素 3 & 3 & 3 & 3 & 3\\ %第3行元素 4 & 4 & 4 & 4 & 4\\ %第3行元素 \end{bmatrix} \] 经过卷积操作之后图像为: \[\begin{bmatrix} %该矩阵一共3列,每一列都居中放置 - & - & - & - & -\\ %第1行元素 - & 2 & 2 & 2 & -\\ %第2行元素 - & 3 & 3 & 3 & -\\ %第3行元素 - & - & - & - & -\\ %第3行元素 \end{bmatrix} \] "-"表示无法进行卷积操作的像素点。 解决方法有2种:①忽略图像边界数据(即不管边界,卷积操作的范围从整张图缩小为边界内缩K圈,K的值随卷积核尺寸变化)。②将原图像往外扩充像素,如在图像四周复制源图像边界的值,从而使得卷积核悬挂在原图像四周时也能进行正常的计算。 opencv边框处理copyMakeBorder: https://zhuanlan.zhihu.com/p/108408180 value=0.114*value_b+0.587*value_g+0.299*value_r; 对于彩色图像,我们取图像亮度的中间值,亮度值的计算方法为: \[luminance = 0.299R + 0.587G + 0.114B \] map<uchar, Point> mp; map为C++的stl中的关联性容器,为了实现快速查找,map内部本身就是按序存储的(map底层实现是红黑二叉树)。在我们插入<key, value>键值对时,就会按照key的大小顺序进行存储,其中key的类型必须能够进行 < 运算,且唯一,默认排序是按照从小到大遍历。 因此,将亮度值或灰度值作为键,map将自动进行按键排序,无需手动书写排序代码。 实现效果 卷积核size为(5, 5)。
2024-01-19
171
0
0
网络聚合
2024-01-19
Django笔记二十之手动编写migration文件
本文首发于公众号:Hunter后端 原文链接:Django笔记二十之手动编写migration文件 前面介绍过,migration 文件主要记录的是 Django 系统 model 的变化,然后通过 migrate 命令将变化适配到数据库中。 比如在某个 application 下新增了某张表,或者对某张表更改了字段,可以生成 migration 文件,然后通过 migrate 更改到数据库。 除了系统能够自动生成的,我们还可以手动创建 migration 文件来操作数据库,这个用途主要是用于比如,创建表后,需要写入一些初始化的数据的情况。 基础命令 migration文件介绍 自定义migration文件 RunSQL() RunPython() 1、基础命令 关于 migration 的命令有如下几条: makemigrations migrate sqlmigrate showmigrations 其中 前面三条命令在第二篇笔记中已经介绍过使用方法,这里介绍一下 showmigrations。 这个作用主要是查看某个 application 下的migration 文件是否已经被更改到数据库中,可以在 Django 系统的根目录用下面的命令测试: python3 manage.py showmigrations blog 可以看到下面的输出: blog [X] 0001_initial [X] 0002_auto_20220118_0926 [X] 0003_auto_20220121_1016 其中,前面的 [X] 表示已经被更改到数据库中,如果我们再对 blog 的 model 进行任意修改,然后执行 makemigrations 的操作,再次执行 showmigrations 的操作,可以看到下面的输出: blog [X] 0001_initial [X] 0002_auto_20220118_0926 [X] 0003_auto_20220121_1016 [ ] 0004_alter_book_price 可以看到最下面的一条记录 [] 中是没有 X 的,表示这条 migration 文件没有被执行 migrate。 2、migration文件介绍 每一次通过 makemigrations 生成的 migration 文件都存在系统中,一个最基础的 migration 文件像下面这样: from django.db import migrations, models class Migration(migrations.Migration): dependencies = [('blog', '0001_initial')] operations = [ migrations.DeleteModel('Tribble'), migrations.AddField('Author', 'rating', models.IntegerField(default=0)), ] 一个 Migration 的类下,有两个参数,一个是 dependencies,一个是 operations dependencies 作用是定位上一个执行的 migration 文件的地方,因为每一次 migrate 的执行都是按照顺序的 且他的参数是一个列表,列表的元素是一个元组,里面有两个参数,一个是 application 的名称,一个是上一次运行的 migration 文件,他是可以指定到多个 application 的,意义为在某两个 application 的 migration 文件之后再执行 operations 的作用是 migration 里需要执行的操作,可以是字段的增加、删除、修改、也可以是表的创建和删除 一个 migration 在执行 migrate 前,我们可以手动对其修改,甚至可以完全自己来定义 3、自定义migration文件 前面介绍了 migration 文件的基本结构,其中有一些关于字段和 model 的操作方法,这些操作都可以通过 makemigration 的方式自动生成。 我们自定义的 migration 文件,与上面的保持一致即可,自定义的 migration 文件需要修改的地方是 operations 里的元素。 假设我们有这样一个需求,创建一张基础映射表后,里面是系统运行所必需的数据,需要在创建表后立即写入,那么就用到了我们这个自定义的 migration 文件。 除了对表字段或者表的修改,还有两种方法实现数据的写入, 一种是使用 SQL 语句插入,用到的migration的函数是 RunSQL() 一种是使用 Django 的 ORM 语句,写 python 的函数来插入,函数是 RunPython 假设创建 Blog 表的migration file 是 0001_create_blog.py 现在需要对其插入两条数据,name 和 tagline 分别是 ('name_1', 'tagline_1') 和 ('name_2', 'tagline_2') 下面用 RunSQL() 和 RunPython() 两种方式来分别介绍。 4、RunSQL() RunSQL() 函数接受一个字符串,或者一个数组作为参数,参数的内容都是 SQL 语句,这也是为什么函数名为 RunSQL()。 字符串的形式为完整的 SQL 语句,比如我们需要插入这两条数据,则是: migrations.RunSQL( "INSERT INTO blog_blog (name, tagline) values('name_x_4', 'tagline_1'), ('name_x_5', 'tagline_2');" ) 如果是作为数组传入,形式则是: migrations.RunSQL( sql=[ ( "INSERT INTO blog_blog (name, tagline) values(%s, %s), (%s, %s);", ['name_x_6', 'tagline_1', 'name_x_7', 'tagline_2'] ) ] ) 在数组的传入形式中,我们将需要插入的数据都放到一个数组中传入 reverse_sql RunSQL() 函数除了 sql 参数,还有一个 reverse_sql 参数,用途是 sql 参数执行的 SQL 语句没有执行成功的情况下的一种操作,一般是用于防止数据污染。 假设说我们的 sql 为插入数据,但是因为某种原因,这条语句没有正确插入,报错了,那么系统就会执行 reverse_sql 中的语句,作为一个可逆的操作。 以下是官方的一个使用示例: migrations.RunSQL( sql=[("INSERT INTO musician (name) VALUES (%s);", ['Reinhardt'])], reverse_sql=[("DELETE FROM musician where name=%s;", ['Reinhardt'])], ) 5、RunPython() RunSQL() 函数操作的是 SQL 语句,RunPython() 参数则是 Python 函数,可以将我们需要写入的数据都写到函数的步骤里,然后在 RunPython() 中调用 以下是使用示例: def insert_blog_data(apps, schema_editor): Blog = apps.get_model("blog", "Blog") db_alias = schema_editor.connection.alias Blog.objects.using(db_alias).create(name="name_3", tagline="tagline_3") Blog.objects.using(db_alias).create(name="name_4", tagline="tagline_4") class Migration(migrations.Migration): dependencies = [ ("blog", "0001_initial"), ] operations = [ migrations.RunPython(insert_blog_data) ] 其中,insert_blog_data 是需要执行的函数,在这个函数里,有两个默认参数,apps 和 schema_editor apps 可以用来获取我们需要的 model,根据函数 apps.get_model(), 这个函数传入两个参数,一个是 application,我们这里是 blog, 一个 model 的名称,我们这里是 Blog 而 schema_editor 则是可以用于获取数据库的 alias 然后,数据的插入的方式就和普通的 model 的操作方法一致了。 RunPython() 函数和 RunSQL 一样,也可以输入两个参数,第二个参数作用也是用于操作失败的回退操作: migrations.RunPython(insert_blog_data, reverse_insert) 以上就是介绍 migration 的全部内容了,下一篇笔记将介绍如何在 Django 中使用原生的 SQL 来查询数据。 如果想获取更多后端相关文章,可扫码关注阅读:
2024-01-19
206
0
0
网络聚合
2024-01-19
vivo 游戏中心低代码平台的提效秘诀
作者:vivo 互联网服务器团队- Chen Wenyang 本文根据陈文洋老师在“2022 vivo开发者大会"现场演讲内容整理而成。公众号回复【2022 VDC】获取互联网技术分会场议题相关资料。 在互联网流量见顶和用户需求分层的背景下,如何快速迭代产品功能,满足用户需求成为了开发首要面对的问题。游戏中心低代码平台从产品定位入手,以组件化方式搭建用户端页面,快速支撑产品需求,提升了研发效率,缩短了项目周期。本文首先介绍背景与痛点,然后阐述了游戏中心是如何搭建低代码平台,最后展示了低代码平台带来的收益和未来建设方向。 一、背景介绍与痛点分析 vivo游戏是vivo用户玩游戏的平台,其主要产品形态是vivo游戏中心以及vivo游戏内置悬浮球,它为用户提供了找游戏,玩好游戏,找人一起玩游戏的价值。vivo游戏中心是vivo游戏的核心流量入口,因此游戏中心首页就承担了非常重要的角色。首页的风格延续了好几年,基础样式几乎没有什么变化,强调分发。随着时间的发展,各种问题就慢慢突显出来了。 1.从2020年开始,互联网流量见顶,分发提升困难,需要探索新方向,而应对新需求的时候研发周期过长。从需求评审到功能上线,灰度到全量,需要耗时一个月以上,运营效果往往低于预期。 2.核心用户的关注点不同。MOBA玩家看画面和平衡,传奇玩家看游戏人数,消消乐玩家看玩法,且用户对游戏福利活动的需求也是非常强烈。首页列表中,重点信息无法突出,也无法给用户带来强烈的下载冲动。 3.任何一个游戏都是有生命周期的。在不同的阶段,突出的重点是不一样的。预约的时候可能突出这个游戏的画风和画质,重点更新的时候可能突出的是新玩法。游戏中心首页没有相关的位置或者手段来突出这些信息。 4.无法快速响应运营或者开发者诉求。如果运营需要更换首页跳转的二级落地页,或者响应开发者诉求搭建一个特殊专区的时候,都是需要开发的,现有功能无法快速支撑。 这几个问题是表层的问题,透过现象看本质,我们可以归纳出,游戏中心缺少了两项基础能力。一方面,游戏中心缺少灵活多样,且能动态调整的组件化能力;另一方面,游戏中心缺少可视化,快速搭建页面的能力。基于以上痛点,结合行业前沿知识,笔者所在团队商量决定,利用低代码思想,打破原有秩序,重新搭建新平台。 二、如何建设游戏中心低代码平台 大家可能会好奇,低代码平台一般都是通用性比较强的平台,怎么能和业务属性如此鲜明的游戏中心结合呢?那接下来笔者为大家一一道来。 低代码平台离不开组件化设计,那什么是组件化设计呢?组件化设计是指针对相同或者不同功能,性能,规格的产品,进行功能分析,设计出一系列的功能组件。通过组件的多样选择将产品客制化,以满足不同的市场需求。由此可以推出,游戏中心组件化设计就是针对游戏中心进行功能分析,设计出一系列功能组件,通过组件的多样化选择,快速搭建出不同的页面,以满足不同用户的需求。 那么,我们是怎么来定义游戏中心的组件呢? 在原有系统的基础上,结合游戏中心app各个位置的形态以及未来定位,把游戏中心首页按照横向划分,每一行细化为一个组件。虽然大家可能不太了解游戏中心,但是对于市面上大部分分发类的产品来说,它们每个页面里面的UI样式是系列化的,比如视频样式,图片样式等,变化比较多的是内容,所以我们可以以行来定义组件。另一方面,组件粒度的粗细,和组件的灵活性成反向关系,但是和运营的配置能力成正向关系,即组件粒度越细,越基础,那么组件的灵活度就越高,运营的配置配置成本也越高。所以,选择什么粒度的基础组件,是需要结合实际业务需求,综合分析后确定的。不是粒度越细越好,也不是粒度越粗越好。 确定好组件的粒度后,我们通过一个例子来详解拆分一下组件的构成。大家请看图1,这是一个专题组件。其形式为左上是标题,右上是跳转二级页的按钮,多个游戏横向并列成一行,最下面是安装按钮。将这种组合形式抽象为一个多游戏并列(1*4)展示的基础模板,基础模板同时也叫元组件,那么就可以把这类基础模板命名为专题元组件。运营希望该元组件可以配置标题,跳转链接,行数和展示游戏个数。这些是静态的基础配置。同时,专题组件需要配置一个数据源,这个数据源决定专题里面游戏的内容和展示顺序,这个数据可以是运营配置的或者实时推荐的。例如,在推荐场景下,用户发起请求的时候,可以由推荐实时返回,那么这就是动态数据。Banner同理。因此可以认为:组件是由元组件和数据构成的。 图1 那么元组件是怎么定义的呢?在后台,我们可以新增一个元组件,填写完卡片编号名称描述等信息后,再上传图片保存。这里的元组件卡片编号作为该组件的唯一标识,是有相应的业务含义,由该编号确定当前组件展示数据的格式,即通过编号确定处理数据的流程类。上传图片主要方面运营配置页面。因为在页面搭建过程中,运营配置多个组件的时候,后台会实时显现客户端的效果。展示的组件图片就是在此处上传的。 图2 配置完元组件的基础信息后,在元组件配置管理的左边,如图3,通过编辑schema,右边就会出现当前元组件能够配置的标题,跳转链接,行数以及单行游戏的个数。这些配置是运营在配置组件的时候可以动态配置的;配置完这些数据后,在页面管理后台,添加该类组件的时候,红框中出现的就是我们能够配置的基础属性了,如图4。到这一步,元组件的配置就完成了。 图3 图4 接下来就是数据源的配置。在运营点击数据选择的时候(图5),弹出的就是动态数据的配置,那这些数据是怎么来的呢? 我们需要从两个维度来看数据:第一个,数据有哪些;第二个,数据怎么交互。我们从两个角度来看数据有哪些。从数据类型角度划分,有运营配置数据和系统自动数据。运营配置数据为运营人员为了达到某一个目的而手动配置或者干预的数据,而系统自动数据为从系统某一个源自动获取的数据,不需要人工介入。从数据来源划分,有内部数据和外部数据。通过不同的数据类型和来源,我们切分为不同的调用方式,这样能最大限度地保证系统的扩展性和维护性。 随着业务的发展,平台会不断吸取其他业务数据,来丰富当前业务的形态,但是获取外部数据的方式只有两种:http和dubbo协议。通过这两个协议的配合,能够标准化获取外部数据。我们说完了元组件和数据,那么他们是怎么绑定的呢?在后台数据管理中,我们会按照某个运营目的,来确定一个组件的应用场景,比如专题组件的应用场景就是为用户推荐某一类型的游戏集合。通过定义组件的应用场景,我们把元组件和数据绑定在一起。 整体过程如下: 确定组件的应用场景名和编号; 选择一个或者多个元组件; 确定数据源类型,调用类型和数据业务方; 确定调用的http和dubbo接口。通过http接口,可以生成运营能够配置的数据,即点击选择后弹出的列表,点击选择后,即可将数据绑定到组件上,如图5;在用户调用流程中,通过dubbo接口,利用后台配置的数据,可以请求获取到更加详细的数据。 此时一个组件就配置完成了。 图5 在前台数据的调用方式中,使用了阿里的QLExpress。QLExpress由阿里的电商业务规则、表达式(布尔组合)、特殊数学公式计算(高精度)、语法分析、脚本二次定制等强需求而设计的一门动态脚本引擎解析工具。它的特性优势和运行原理可以在GitHub上找到,在此不赘述,感兴趣的同学可以自行搜索。利用其弱类型脚本的特性,将运营配置的数据转换成调用外部接口的参数,通过dubbo泛化调用技术,获取到具体的数据。同时,还是利用弱类型脚本的特性,转换返回结果,控制业务逻辑和数据范围。采用QLExpress和dubbo泛化调用的方式,可以减少代码开发,增加数据灵活性。 最后,来了解一下整个页面的配置过程。前面讲述到,元组件是最基础的组件,通过元组件,我们配置一些基础信息,并关联一些动态数据,构成了一个组件。通过把多个组件拖拽到页面上,就可以实现运营配置生成页面的效果。同时,页面也可以直接拖拽已经配置好的组件,这就是一个组件被多个页面引用的情况,实现了组件级的复用。在页面之上,我们还引入了方案的概念。方案,即多个页面的集合。通过页面的组合,首页可以实现多个页面的展示,既能展示游戏中心的门户,又能个性化运营。 如图6,从下到上,通过数据和元组件,可以构成一个组件,通过多个组件的选择,可以构成一个页面,多个页面构成一个方案。如图7,从上到下,通过多层实验框架,确定需要展示给用户的方案。接下来,通过dmp用户画像,确定展示个性化的页面。每一个页面都是由若干个组件构成的。每个组件是由元组件和数据构成。 图6 图7 三、成果展示 罗马不是一天建成的,游戏中心低代码平台也不是一蹴而就的。平台20年就上线了,由于缺少运营场景,功能也不是很完善,能够带来的效益微乎其微,甚至内部也产生过质疑,是不是不值得花这么多的时间精力建设平台,但经过时间的沉淀,游戏中心低代码平台的效果愈发明显。 首先,研发流程和原先不一样了。当我们在新增/修改组件的时候,客户端同学通过flutter等动态化技术,完成新组件的开发修改,并且在后台上传flutter的更新包或者差分包。 服务器同学需要在后台配置元组件的信息,配置组件应用场景,绑定元组件和数据关系,就可以生成运营可以配置的组件。运营配置完组件,页面,方案,点检完毕审核通过后就可以上线了,如图8。 图8 其次,研发效率提升了。大家注意到,最大的一个变化是,客户端不需要发版了。在一些特殊场景下,服务器也不需要开发。对比原先的研发流程,效率发生了质的飞越。针对不同的角色,提升的效率是不一样的;对于客户端来说功能全量上线周期可缩短15天以上,有较高的容错性,对于服务器来说,开发效率提升4倍以上,对于测试来说,无需回归老版本,测试效率提升30%-50%;对于运营来说,可视化的操作降低30%的学习成本,提升10%的配置效率。 最后,项目周期缩短了。原先如果运营做一个功能,首先得把需求提给产品(其实在提需求之前,还有一个需求讨论的过程,非需求评审),再进行需求评审,评审完毕后需要根据各个需求的优先级进行排序。而此类需求由于效果不明显,且论证数据不好收集,往往其优先等级就比较低。需求评审完毕后,还需要策划评审,概要设计评审等等诸多流程,上线完毕还需要灰度一周,有了上线报告之后才可以全量。但是,有了低代码平台后,流程就没有这么复杂了。最简单的流程,无需更改组件,运营自己就能操作。还有一些简单的场景,服务器修改配置就能完成组件的修改。最复杂的就是全新场景,但由于之前的基础在,开发效率也是非常的高。整体流程至少可以缩短为原来的1/4。接下来用一个例子来说明一下。 图9 图10 四、未来展望 游戏中心低代码平台的建设标准,和通常意义上低代码平台的建设存在差异。游戏中心低代码平台由“游戏中心业务”衍生,慢慢演变到可以适配vivo生态内分发类app的终端解决方案。这符合我们的业务发展,也为低代码的演变提供了养分。通过不断的适配和演变,我们希望能够将低代码的解决方案普惠安卓生态。因此,在未来的建设思路上,我们的目标是能够解放生产力,提升用户体验,做最好用的安卓低代码平台。 五、总结 低代码的概念最近很火,争议也很大。有人认为以后“人人都是程序员”,也有人认为是新瓶装旧酒。但作为技术人,最重要的还是通过技术解决业务问题,驱动业务发展。游戏中心低代码平台旨在提高开发效率,帮助业务取得更好的结果。未来,我们也会投入更多的精力优化系统,不断为用户创造惊喜,为行业带来革新。
2024-01-19
154
0
0
网络聚合
2024-01-19
【机器学习】李宏毅——AE自编码器(Auto-encoder)
1、What 在自编码器中,有两个神经网络,分别为Encoder和Decoder,其任务分别是: Encoder:将读入的原始数据(图像、文字等)转换为一个向量 Decoder:将上述的向量还原成原始数据的形式 而目标是希望还原出来的结果能够与原始数据尽可能的接近。其中的向量可称为Embedaing、Representation、Code。而它的主要用处就是将原始数据(高维、复杂)经过Encoder后得到的向量(经过处理,低纬度)作为下游任务的输入。 2、Why 因为例如图像这种原始数据它的变化是有限的(不可能每一个像素点都是完全随机的,这不是我们可能看到的图片),因此如果AutoEncoder能够找到它们之间的变化规律(通常是比原始数据更简单的)那么就可以用更加简便的表达形式来表示数据,那么在下游任务训练的时候就可能可以用更简单的数据、更少的数据来学习到原来想要让机器学习到的东西了。 3、De-noising Auto-encoder 这个和普通的Auto-encoder的区别在于,Encoder的输入并不是原始的图像,而是将图像加上一定的噪声之后再作为Encoder的输入,而在输出的时候是要求Decoder输出能够与未加噪声之前的图像越接近越好,即: 而如果我们回顾一下之前学习过的BERT,可以发现BERT实际上就是De-noising Auto-encoder,可以看下图: 4、Feature Disentangle 特征区分技术可以用于上文介绍的Auto-encoder,具体上可以这么理解:在Auto-encoder中我们将图片、文字、语音等放入Encoder得到的输出向量Embedaing中就包含了这些输入的特征信息,但是一个输入可能存在不同的特征信息,例如一段语音就包含语音的内容、说话者的特征等等,那么有没有可能在Embedaing中将这些特征分别提取出来呢?这就是Feature Disentangle想要实现的事情。 5、Voice Conversion 语者转换这个例子就是学习完模型之后,将A说话的内容用B的声音复述出来作为输出,就好像柯南的领带变声器一般神奇。那么Auto-encoder如何来实现这个任务呢? 实际上这就需要借助Feature Disentangle。首先如果将该任务作为一个监督学习的任务,那我们就需要A和B两个人分别来说同样的句子同样的内容,产生大量的样本从而来进行训练,但是这显然是不可能的!因此如果我们利用Auto-encoder和Feature Disentangle,可以有这样的思路: 训练完Auto-encoder后,将A说话的语音和B说话的语音都输出Encoder得到对应的Embedaing输出 运用特征提取技术,将A和B对应的Embedaing分别提取出说话的内容和语者的特征两部分 再将A说话的特征和B的特征互换,让B的特征和A的内容拼接在一起,这样就实现了用B语者来说出A的内容。 6、Discrete Representation 上述我们说到的Embedaing是一个向量,其中每一个维度都是可以连续变化的数值。那么有没有可能我们强迫这个Embedaing是用离散的数值来表示呢?例如表示为二进制,只有0和1,每个维度表示是否含有某个特征;或者表示为One-hat-vector,来表示对物品的分类(这样就不需要标签)了,因为在学习的过程中就会自动将类似的物品归于同一类,就类似于聚类算法了。 那么这种想法比较有代表性的技术为VQVAE,其具体的流程为: 将输入经过Encoder之后得到Embedaing,然后现在有一排向量Codebook(里面向量的个数也是你指定的) 将Embedaing逐一与Codebook中的向量进行计算相似度,并取其中相似度最高的来作为Decoder的输入 训练的时候我们会要求Decoder的输出要与Encoder的输入越接近越好,从而来不断地改进Codebook中的各个向量 这样最终的结果就是让你Decoder的输入是离散的,只能在Codebook中进行选取,而且例如应用在语音的例子中,有可能最终学习得到的Codebook中的各个向量的不同维度可能会代表不同音标等等。 但这里我有一个问题就是如上图应用在图像上,那么训练完成后如果放入Encoder的是之前训练从未见过的图像,那么输出还能够与输入相接近吗? 7、令Embedaing是一段文字 如果天马行空一点,能否让Embedaing是一段文字呢?例如我们给Encoder一篇文章,然后希望它输出一段文字,而Decoder再由这段文字来还原回原来的文章。那么此时这个Embedaing是否可以认为是文章的摘要呢? 如果真的将这个想法进行实现会发现:Embedaing虽然确实是一段文字,但是它经常是我们人类看不懂的文字,即在我们看来是毫无逻辑的文字无法作为摘要,但这可以认为是En和De之间发明的暗号,它们用这些文字就可以实现输入和输出的文章都极其相似。那么如果希望中间的Embedaing是我们能够看得懂的文字,我们可以加上GAN的思想,即加上一个辨别器,该辨别器是学习了很多人类写文章的句子,它能够分辨一段文字是否是人类能够理解的逻辑,那么这就会使得En不断地调整自己的输出,希望能够欺骗过辨别器,让它认为是人类写出来的句子,因此Embedaing也就越来越接近于摘要的功能了! 8、其他应用 8.1、生成器 训练完Auto-encoder后,由于Decoder是接受一个向量,生成一个输出(例如图像),那么就可以认为这个Decoder就是一个生成器,因此可以单独拿出来作为一个生成器使用: 8.2、压缩 将Encoder训练完成后它相当于接受一个输入(例如图片)然后得到向量,那么这个向量通常是低维度的,那么我们可以认为是进行了压缩,而Decoder就是进行了解压缩。但需要注意的是由于De输出的结果无法与原始的输入一模一样,因此这样的压缩是有损的。 8.3、异常检测 我们如果想要做一个异常检测系统,那我们需要很多的资料来进行训练,而在某些应用场景中很可能我们只有非常多的正常的数据而只有非常少的异常数据,甚至于说有些异常的数据混杂在正常的数据中都分辨不出来,那么这时候Auto-encoder就可以派上用场了!如下图,我们先用正常的数据来训练我们的Auto-encoder,例如正常的数据是人脸: 那么训练完成之后,如果你进行检测时输入的也是相似的人脸,那么Auto-encoder就有较大的可能,使得输入与输出之间较为接近,即计算相似度就会较大;但是如果输入不是人脸,例如动漫人物,那么因为Auto-encoder没有看过这样的图片因此很难正确的将其还原,那么再计算输入与输出之间的相似度时就会较小,即:
2024-01-19
306
0
0
网络聚合
2024-01-19
学习.NET MAUI Blazor(三)、创建.NET MAUI Blazor应用并使用AntDesignBlazor
大致了解了Blazor和MAUI之后,尝试创建一个.NET MAUI Blazor应用。 需要注意的是: 虽然都叫MAUI,但.NET MAUI与.NET MAUI Blazor 并不相同,MAUI还是以xaml为主,而MAUI Blazor则是以razor为主。 这个系列还是以MAUI Blazor为主,要创建一个MAUI Blazor应用,需要安装Visual Studio 2022 17.3 或更高版本,并在安装程序上,勾选.NET Multi-platform App UI 开发!最好是升级到最新的.NET 7。 创建.NET MAUI Blazor应用 打开Visual Studio 2022,选择创建新项目 在搜索框输入MAUI,选择.NET MAUI Blazor应用,点下一步! 给项目起一个好听的名字,选择项目存在的位置,点下一步! 选择目标框架,这里选择的是.NET 7,点击创建。 等待创建项目及其依赖项还原。完成后的目录结构如下: .NET MAUI Blazor 需要注意的地方 .NET MAUI Blazor 运行在WebView2上,WebView2是微软推出的新一代用于桌面端混合开发的解决方案。它可以让本地应用程序(WinForm、WPF、WinUI、Win32)、移动应用程序(MAUI)轻松嵌入Web技术。WebView2 控件使用 Microsoft Edge 作为呈现引擎在客户端应用程序及App中显示 Web 内容。使用 WebView2 可以将 Web 代码嵌入到客户端应用程序及App中的不同部分,或在单个 WebView 实例中构建所有本机应用程序。 可以这么看MAUI Blazor, .NET MAUI 包含 BlazorWebView 控件,该控件运行将 Razor 组件呈现到嵌入式 Web View 中。 通过结合使用 .NET MAUI 和 Blazor,可以跨移动设备、桌面设备和 Web 重复使用一组 Web UI 组件。 说人话就是,它就是一个Hybrid App(混合应用) ! 调试.NET MAUI Blazor 在windows上调试 MAUI Blazor应用,需要Windows 10 1809及更高版本上,并打开开发者模式。 windows 11上,位于设置->隐私和安全性->开发者选项->开发人员模式 点击Windows Machine,运行程序! 如无意外,运行成功! 这时,MAUI Blazor使用的是bootstrap样式以及open-iconic图标。 在wwwroot/index.html中也可以看到 <link rel="stylesheet" href="css/bootstrap/bootstrap.min.css" /> 现在已经有个很多基于Blazor的组件库,所以暂时把默认的bootstrap替换成第三方组件库,这里使用的是AntDesignBlazor。 使用AntDesignBlazor 组件库 安装依赖: PM> NuGet\Install-Package AntDesign.ProLayout -Version 0.13.1 注入AntDesign 在MauiProgram.cs注入AntDesign 服务与设置基本配置,完整的MauiProgram.cs代码 using Microsoft.Extensions.Logging; using MauiBlazorApp.Data; namespace MauiBlazorApp; public static class MauiProgram { public static MauiApp CreateMauiApp() { var builder = MauiApp.CreateBuilder(); builder .UseMauiApp<App>() .ConfigureFonts(fonts => { fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular"); }); builder.Services.AddMauiBlazorWebView(); #if DEBUG builder.Services.AddBlazorWebViewDeveloperTools(); builder.Logging.AddDebug(); #endif builder.Services.AddSingleton<WeatherForecastService>(); //注入AntDesign builder.Services.AddAntDesign(); //基本配置 builder.Services.Configure<ProSettings>(settings => { settings.NavTheme = "light"; settings.Layout = "side"; settings.ContentWidth = "Fluid"; settings.FixedHeader = false; settings.FixSiderbar = true; settings.Title = "DotNet宝藏库"; settings.PrimaryColor = "daybreak"; settings.ColorWeak = false; settings.SplitMenus= false; settings.HeaderRender= true; settings.FooterRender= false; settings.MenuRender= true; settings.MenuHeaderRender= true; settings.HeaderHeight = 48; }); return builder.Build(); } } 配置项都写上了。参数含义从表达的意思就能看出来,不做注释了! 引入样式 打开wwwroot/index.html。由于我们使用的是AntDesign,所以需要改造下index.html,修改后内容如下: <!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" /> <title>DotNet宝藏库</title> <base href="/" /> <link href="_content/AntDesign/css/ant-design-blazor.css" rel="stylesheet" /> <link rel="stylesheet" href="_content/AntDesign.ProLayout/css/ant-design-pro-layout-blazor.css" /> </head> <body> <div class="status-bar-safe-area"></div> <div id="app"> <style> html, body, #app { height: 100%; margin: 0; padding: 0; } #app { background-repeat: no-repeat; background-size: 100% auto; } .page-loading-warp { padding: 98px; display: flex; justify-content: center; align-items: center; } .ant-spin { -webkit-box-sizing: border-box; box-sizing: border-box; margin: 0; padding: 0; color: rgba(0, 0, 0, 0.65); font-size: 14px; font-variant: tabular-nums; line-height: 1.5; list-style: none; -webkit-font-feature-settings: 'tnum'; font-feature-settings: 'tnum'; position: absolute; display: none; color: #1890ff; text-align: center; vertical-align: middle; opacity: 0; -webkit-transition: -webkit-transform 0.3s cubic-bezier(0.78, 0.14, 0.15, 0.86); transition: -webkit-transform 0.3s cubic-bezier(0.78, 0.14, 0.15, 0.86); transition: transform 0.3s cubic-bezier(0.78, 0.14, 0.15, 0.86); transition: transform 0.3s cubic-bezier(0.78, 0.14, 0.15, 0.86), -webkit-transform 0.3s cubic-bezier(0.78, 0.14, 0.15, 0.86); } .ant-spin-spinning { position: static; display: inline-block; opacity: 1; } .ant-spin-dot { position: relative; display: inline-block; font-size: 20px; width: 20px; height: 20px; } .ant-spin-dot-item { position: absolute; display: block; width: 9px; height: 9px; background-color: #1890ff; border-radius: 100%; -webkit-transform: scale(0.75); -ms-transform: scale(0.75); transform: scale(0.75); -webkit-transform-origin: 50% 50%; -ms-transform-origin: 50% 50%; transform-origin: 50% 50%; opacity: 0.3; -webkit-animation: antSpinMove 1s infinite linear alternate; animation: antSpinMove 1s infinite linear alternate; } .ant-spin-dot-item:nth-child(1) { top: 0; left: 0; } .ant-spin-dot-item:nth-child(2) { top: 0; right: 0; -webkit-animation-delay: 0.4s; animation-delay: 0.4s; } .ant-spin-dot-item:nth-child(3) { right: 0; bottom: 0; -webkit-animation-delay: 0.8s; animation-delay: 0.8s; } .ant-spin-dot-item:nth-child(4) { bottom: 0; left: 0; -webkit-animation-delay: 1.2s; animation-delay: 1.2s; } .ant-spin-dot-spin { -webkit-transform: rotate(45deg); -ms-transform: rotate(45deg); transform: rotate(45deg); -webkit-animation: antRotate 1.2s infinite linear; animation: antRotate 1.2s infinite linear; } .ant-spin-lg .ant-spin-dot { font-size: 32px; width: 32px; height: 32px; } .ant-spin-lg .ant-spin-dot i { width: 14px; height: 14px; } .status-bar-safe-area { display: none; } @supports (-webkit-touch-callout: none) { .status-bar-safe-area { display: flex; position: sticky; top: 0; height: env(safe-area-inset-top); background-color: #f7f7f7; width: 100%; z-index: 1; } .flex-column, .navbar-brand { padding-left: env(safe-area-inset-left); } } @media all and (-ms-high-contrast: none), (-ms-high-contrast: active) { .ant-spin-blur { background: #fff; opacity: 0.5; } } @-webkit-keyframes antSpinMove { to { opacity: 1; } } @keyframes antSpinMove { to { opacity: 1; } } @-webkit-keyframes antRotate { to { -webkit-transform: rotate(405deg); transform: rotate(405deg); } } @keyframes antRotate { to { -webkit-transform: rotate(405deg); transform: rotate(405deg); } } </style> <div style=" display: flex; justify-content: center; align-items: center; flex-direction: column; min-height: 420px; height: 100%; "> <div class="page-loading-warp"> <div class="ant-spin ant-spin-lg ant-spin-spinning"> <span class="ant-spin-dot ant-spin-dot-spin"> <i class="ant-spin-dot-item"></i><i class="ant-spin-dot-item"></i><i class="ant-spin-dot-item"></i><i class="ant-spin-dot-item"></i> </span> </div> </div> <div style="display: flex; justify-content: center; align-items: center;"> <div class="loading-progress-text"></div> </div> </div> </div> <script src="_framework/blazor.webview.js" autostart="false"></script> <script src="_content/AntDesign/js/ant-design-blazor.js"></script> </body> </html> 加入命名空间 在_Imports.razor添加AntDesign命名空间: @using System.Net.Http @using Microsoft.AspNetCore.Components.Forms @using Microsoft.AspNetCore.Components.Routing @using Microsoft.AspNetCore.Components.Web @using Microsoft.AspNetCore.Components.Web.Virtualization @using Microsoft.JSInterop @using MauiBlazorApp @using MauiBlazorApp.Shared //引入AntDesign @using AntDesign 设置容器 在Main.razor中加入<AntContainer /> <Router AppAssembly="@typeof(Main).Assembly"> <Found Context="routeData"> <RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" /> <FocusOnNavigate RouteData="@routeData" Selector="h1" /> </Found> <NotFound> <LayoutView Layout="@typeof(MainLayout)"> <p role="alert">Sorry, there's nothing at this address.</p> </LayoutView> </NotFound> </Router> <!--设置容器--> <AntContainer /> 修改 MainLayout 修改MainLayout.razor。 把MainLayout.razor中默认布局删除 引入AntDesign.ProLayout 设置布局为AntDesign.ProLayout 构造菜单、页脚的链接、版权 wwwroot目录下新建个文件夹images,把提前准备好的logo放进去 完整代码如下: @using AntDesign.ProLayout @inherits LayoutComponentBase <AntDesign.ProLayout.BasicLayout Logo="@("images/logo.png")" MenuData="MenuData"> <ChildContent> @Body </ChildContent> <FooterRender> <FooterView Copyright="MauiBlazorApp" Links="Links"></FooterView> </FooterRender> </AntDesign.ProLayout.BasicLayout> <SettingDrawer /> @code { private readonly MenuDataItem[] MenuData = { new MenuDataItem { Path = "/", Name = "Home", Key = "Home", Icon = "home" }, new MenuDataItem { Path = "/Counter", Name = "Counter", Key = "Counter", Icon = "plus" }, new MenuDataItem { Path = "/FetchData", Name = "FetchData", Key = "FetchData", Icon = "cloud" } }; private readonly LinkItem[] Links = { new LinkItem { Key = "DotNet宝藏库", Title = "基于Ant Design Blazor", Href = "https://antblazor.com", BlankTarget = true } }; } 这时可以把项目中无用的内容删除掉了,如Shared/NavMenu.razor、wwwroot/css文件。 由于删除掉了css文件夹,页面元素肯定没有样式了。那么就简单的改造下默认的几个页面! 改造默认页面 index.razor 打开Pages/Index.razor,将演示组件SurveyPrompt 删掉。顺便把Shared/SurveyPrompt.razor也删除掉。将<h1>Hello, world!</h1> 替换为Ant Design组件。 @page "/" <Title Level="1">Hello,DotNet宝藏库</Title> <br /> <Text Type="success">欢迎关注我的公众号!</Text> Counter.razor 打开 Pages/Counter.razor,将代码改为如下: @page "/counter" <Title Level="2">HCounter</Title> <Divider /> <p role="status">Current count: @currentCount</p> <Button @onclick="IncrementCount" Type="primary">AntDesign 按钮</Button> @code { private int currentCount = 0; private void IncrementCount() { currentCount++; } } FetchData.razor 打开Pages/FetchData.razor,将数据表格替换为Ant Design,删除页面所有代码,替换为Ant Design的示例! @page "/fetchdata" @using System.ComponentModel @using AntDesign.TableModels @using System.Text.Json @using MauiBlazorApp.Data @inject WeatherForecastService ForecastService <Table @ref="table" TItem="WeatherForecast" DataSource="@forecasts" Total="_total" @bind-PageIndex="_pageIndex" @bind-PageSize="_pageSize" @bind-SelectedRows="selectedRows" OnChange="OnChange"> <Selection Key="@(context.Id.ToString())" /> <PropertyColumn Property="c=>c.Id" Sortable /> <PropertyColumn Property="c=>c.Date" Format="yyyy-MM-dd" Sortable /> <PropertyColumn Property="c=>c.TemperatureC" Sortable /> <PropertyColumn Title="Temp. (F)" Property="c=>c.TemperatureF" /> <PropertyColumn Title="Hot" Property="c=>c.Hot"> <Switch @bind-Value="@context.Hot"></Switch> </PropertyColumn> <PropertyColumn Property="c=>c.Summary" Sortable /> <ActionColumn> <Space> <SpaceItem><Button Danger OnClick="()=>Delete(context.Id)">Delete</Button></SpaceItem> </Space> </ActionColumn> </Table> <br /> <p>PageIndex: @_pageIndex | PageSize: @_pageSize | Total: @_total</p> <br /> <h5>selections:</h5> @if (selectedRows != null && selectedRows.Any()) { <Button Danger Size="small" OnClick="@(e => )">Clear</Button> @foreach (var selected in selectedRows) { <Tag @key="selected.Id" Closable OnClose="e=>RemoveSelection(selected.Id)">@selected.Id - @selected.Summary</Tag> } } <Button Type="@ButtonType.Primary" OnClick="()=> { _pageIndex--; }">Previous page</Button> <Button Type="@ButtonType.Primary" OnClick="()=> { _pageIndex++; }">Next Page</Button> @code { private WeatherForecast[] forecasts; IEnumerable<WeatherForecast> selectedRows; ITable table; int _pageIndex = 1; int _pageSize = 10; int _total = 0; protected override async Task OnInitializedAsync() { forecasts = await GetForecastAsync(1, 50); _total = 50; } public class WeatherForecast { public int Id { get; set; } [DisplayName("Date")] public DateTime? Date { get; set; } [DisplayName("Temp. (C)")] public int TemperatureC { get; set; } [DisplayName("Summary")] public string Summary { get; set; } public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); public bool Hot { get; set; } } private static readonly string[] Summaries = new[] { "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" }; public void OnChange(QueryModel<WeatherForecast> queryModel) { Console.WriteLine(JsonSerializer.Serialize(queryModel)); } public Task<WeatherForecast[]> GetForecastAsync(int pageIndex, int pageSize) { var rng = new Random(); return Task.FromResult(Enumerable.Range((pageIndex - 1) * pageSize + 1, pageSize).Select(index => { var temperatureC = rng.Next(-20, 55); return new WeatherForecast { Id = index, Date = DateTime.Now.AddDays(index), TemperatureC = temperatureC, Summary = Summaries[rng.Next(Summaries.Length)], Hot = temperatureC > 30, }; }).ToArray()); } public void RemoveSelection(int id) { var selected = selectedRows.Where(x => x.Id != id); selectedRows = selected; } private void Delete(int id) { forecasts = forecasts.Where(x => x.Id != id).ToArray(); _total = forecasts.Length; } } 运行效果: 总结 暂无,下次再会 欢迎大家关注我的微信公众号,一起进步,一起成长
2024-01-19
144
0
0
网络聚合
2024-01-19
Rust-01 启航
安装 所谓工欲善其事必先利其器,我们学习Rust当然需要安装Rust。我们可以从Rust官网下载rustup工具进行rust的安装。安装完成后,我们在命令行中输入rustc --version便可以查看我们所安装rust的版本。 笔者推荐在Windows平台使用PowerShell Core和Windows Terminal Rust相关命令 安装好 rust 后,我们需要了解几个东西 rustc rustc是 rust 的编译器,负责将 rs 文件源码编译到可运行文件或者库的二进制代码 rustup rustup是 rust 的升级管理工具,负责升级 rust 的版本,常用命令rustup update用来升级 rust cargo 一般都不直接使用rustc来直接编译 rs 文件,而是选择cargo。 cargo是 Rust 的包管理器,可以用来创建项目、安装依赖、类型检测、编译、运行以及测试项目等功能。cargo是一个功能强大的工具。cargo run能够自动化调用rustc` 对 rs 文件进行编译产出二进制文件并运行二进制文件。 常用cargo命令 命令 说明 示例 cargo new 创建项目,默认创建 binary 项目,添加参数可以选择 lib 和 bin 两种项目 cargo new hello --lib(默认--bin) cargo check 类型检测 cargo check cargo build 编译项目 cargo build [--release] cargo run 运行项目,可以带参数 cargo run [--release] cargo test 对项目进行单元测试 cargo test 详细的说明可以查看官方文档。 IDE 现在能够开发 rust 的工具已经有很多了,比如 Jetbrains 家的Clion(需要添加 rust 插件)或者 Microsoft家的VS Code(需要安装rust-analyzer扩展) Hello World! 既然已经有了 rust 的运行环境,那么我们便开始创建第一个 rust 项目。使用cargo new hello-world命令创建一个名为hello-world的项目,然后cd hello-world进入项目目录后可以看到项目的结构 C:. │ .gitignore │ Cargo.toml │ └─src main.rs 项目根路径会有一个Cargo.toml和一个src\main.rs,其中Cargo.toml文件是负责配置 cargo 和项目依赖项,main.rs 文件则是程序的入口点,main.rs 里的代码 fn main() { println!("Hello, world!"); } 简单易懂,关键字fn用来声明这是一个无返回值名称为main的 function,然后函数体内调用 rust 的输出宏println!输出了Hello, world! 在 Rust 中所有的函数调用都是必须有返回值的表达式,无返回值的返回一个空的 tuple 表示或者省略不写。 fn foo() -> () {} // 二者等效 fn foo() {} 我们运行cargo build,对项目进行编译,默认情况下是生成带有 debug 信息并且没有优化的代码,可以得到下图所示内容 而添加--release参数后,则会生成不带 debug 信息且优化后的代码,如下图所示(这一般是在正式发布时使用) 执行cargo build命令后会在项目目录下生成一个 target 文件夹,target 文件夹中的内容就是编译生成的结果。根据cargo build后面添加的参数会生成两个文件夹,即release和debug。 也可以直接运行cargo run命令直接运行项目,这个命令就相当于cargo build && ./debug/hello-world.exe先编译项目然后再执行编译后的可执行文件。和cargo build一样默认是生成 debug 代码,带上--release参数后则是 release 代码。 下图是cargo run所显示信息 下图是cargo run --release所显示信息 依赖项 在项目文件夹中找到Cargo.toml文件,这个文件便是 Rust 项目的依赖项配置文件 在这个文件里对项目进行配置,比如我现在需要使用随机数,那么我只需要在[dependencies]下面添加rand = "0.8.3",然后项目运行时,cargo 会自己进行依赖还原,将详细的包写入Cargo.lock文件中并且会自动去解析引入包的依赖,这和 npm 相似。 猜数字游戏 我们现在可以写一个小游戏来走一遍 Rust 项目创建到运行的流程 使用cargo new创建一个新的项目,然后在Cargo.toml文件中的[dependencies]下面添加rand = "0.8.3" 打开main.rs,并添加以下代码 use rand::Rng; use std::cmp::Ordering; use std::io; fn guess() { println!("Guess the number!"); let secret_number = rand::thread_rng().gen_range(1..101); loop { println!("Please input your guess."); let mut buffer = String::new(); io::stdin() .read_line(&mut buffer) .expect("Failed to read line!"); let number: i32 = match buffer.trim().parse() { Ok(num) => num, Err(_) => continue, }; match number.cmp(&secret_number) { Ordering::Less => println!("Too small!"), Ordering::Greater => println!("Too big!"), Ordering::Equal => { println!("You win!"); break; } } } } fn main() { guess(); } 这些代码也很容易理解。use std::io;、use std::cmp::Ordering;和use rand::Rng;分别引入我们需要的模块。 std::io用来获取输入的数字,std::cmp::Ordering用来比较输入的数字和随机数的大小,rand::Rng用来生成随机数。 然后我们声明了一个无参数和无返回值的函数 guess。函数里主要是输出一行提示用户输入的提示信息和生成范围在[1,101)的随机整数,并根据用户输入的数字与随机数进行比较,直到用户猜测数字等于随机数字后结束程序。 我们需要处理用户的输入,在 rust 中使用io::stdin().read_line()从标准输入流中获取用户的输入,因为read_line()返回的是Result<usize>,在 rust 中Result<T>都可以使用 match(模式匹配)来对结果进行处理。loop在 rust 中是开启一个无线循环,根据内部的 break 来跳出循环,这里我们根据用户输入的数字和产生的随机数字比较结果作为是否结束循环的条件。在 rust 中,我们需要着重的学习match模式匹配,这里number.cmp返回一个Ordering,我们可以根据不同的结果进行不同处理,这和 if 条件判断类似,但是代码的可读性变高了,更利于理解,在 rust 中尽量使用模式匹配来进行逻辑判断以便减少 bug。main函数就是简单地调用guess函数。 使用cargo run后,我们会得到以下信息 总结 至此,我们了解了 rust 的安装和各种命令以及创建并运行一个 rust 项目的所有流程。 引用 部分内容和代码参考Rust官网
2024-01-19
104
0
0
网络聚合
2024-01-19
前端报表如何实现无预览打印解决方案或静默打印
在前端开发中,除了将数据呈现后,我们往往需要为用户提供,打印,导出等能力,导出是为了存档或是二次分析,而打印则因为很多单据需要打印出来作为主要的单据来进行下一环节的票据支撑, 而前端打印可以说是非常令人头疼的一件事。 为什么令大家头疼呢? 因为前端打印,要强依赖与浏览器的打印预览页面,会天然存在以下弊端: 每一次打印都要弹出来打印预览对话框,如果前端需要批量打印,那么意味着客户要点击无数个关闭按钮,才能实现批量打印,如果一次性打印几百张上千张的报表,则会成为“NightMare”。 前端打印强依赖于浏览器,主流的思路是先将内容转换为PDF文件,再调用浏览器的打印功能进行打印,而生成PDF文件是依赖于浏览器对于字体,边线等的处理,因此浏览器的异同则直接导致打印出来的效果差距很大,有的边线加粗,有的1页数据,打印出来呈现2页,也是让开发者十分苦恼的事情,对于一些打印要求比较高的行业,这就是灾难。 因此如何在前端实现无预览打印,也就是用户点击打印之后直接就使用默认打印机打印出来。针对这个需求,我们验证了一个解决该问题的方案,本贴就来介绍该方案如何实现。 实现思路如下: 后端实现一个接口,接收Blob类型PDF流,然后调用系统默认打印机,将PDF进行静默打印。 前端利用ACTIVEREPORTSJS自带的导出PDF,导出Blob类型,然后通过POST请求调用后端接口将Blob流传给后端进行打印。 具体实现步骤: 前端实现方法: 前端利用ActivereportsJS的PDF.exportDocument无预览导出PDF,该接口返回的result包含data属性和download方法,然后调用后端接口,将result.data传递给后端。 function printPDF() { var ACTIVEREPORTSJS = GC.ActiveReports.Core; var PDF = GC.ActiveReports.PdfExport; var settings = { info: { title: "test", author: "GrapeCity inc.", }, pdfVersion: "1.7", }; var pageReport = new ACTIVEREPORTSJS.PageReport(); pageReport .load("1.rdlx-json") .then(function () { return pageReport.run(); }) .then(function (pageDocument) { return PDF.exportDocument(pageDocument, settings); }) .then(function (result) { let formData = new FormData(); formData.append("file", result.data); fetch("http://localhost:8088/print", { method: 'POST', mode: 'cors', body: formData }) }); } 具体PDF.exportDocument可以参考文档: https://demo.grapecity.com.cn/activereportsjs/demos/api/export/purejs 后端实现方式: 我这边是采用python实现了一个接口,接收前端传递的Blob文件流,然后调用后端部署的服务器默认打印机直接进行静默打印。 后端程序可以部署到服务器上,如果是windows服务器,可以直接下载exe,在服务器上运行。 下载链接: https://pan.baidu.com/s/1De2VdhrGTqX9tHub8gYrSg 提取码: 569c 下载下来是2个exe程序,需要放在同一个文件夹,然后运行PrintAgent.exe,切记这两个程序需要放在同一个文件夹。 注意:如果exe只给服务器上部署,那么前端在打印时调用服务器地址接口打印,最终都会从服务器上连接的打印机打出来。 如果exe给客户端部署了,那么前端打印就可以代码调用localhost地址去打印,最终就会从客户端所连接的默认打印机打印出来; 切换打印机的话,就调整windows的默认打印机就可以。 Linux服务器的话需要将源码拷贝到服务器去运行。 源码如下,也可以根据自己需要进行调整和修改: https://gcdn.grapecity.com.cn/forum.php?mod=attachment&aid=MjUzNTMyfGE0YTE2ZDY5fDE2NzM0MTk2ODZ8NjI2NzZ8OTk3MTg%3D 另附 前端100张数据可视化大屏模板,按需取用: https://www.grapecity.com.cn/solutions/wyn/demo
2024-01-19
256
0
0
网络聚合
2024-01-19
对比 elasticsearch 和 mysql
最近阅读了elasticsearch的官方文档,学习了它的很多特性,发现elasticsearch和mysql有很多地方类似,也有很多地方不同。这里做一个对比,帮助大家加深对elasticsearch的理解。 特性 elasticsearch mysql 备注 场景 全文搜索,日志处理,空间数据分析 表结构存储 es 不适合做join操作,mysql 不适合做全文检索 扩展性 动态扩展,能够通过添加node快速提升性能 mysql cluster master 选举 bully 算法,比较id选出master master-slave结构,无需选举 es中master选举可能会出现脑裂问题,配置 minimum_master_nodes参数确保过半选举决定机制 路由算法 routing_factor = num_routing_shards / num_primary_shards shard_num = (hash(_routing) % num_routing_shards) / routing_factor 指定路由分片: my-index-000001/_doc/1?routing=user1&refresh=true 手动路由,或者使用路由组件sharding-jdbc 可靠性 Cross-cluster replication (CCR), 双集群设计 主从复制,双数据中心 内存配置 heap size 推荐 32g,但不要超过内存的一半, 其他需要用到堆外内存的地方,网络,文件缓存,jvm的栈 物理内存的80% 单独的服务器 缓存 filesystem cache, request cahce, query cache 所有cache都是基于node query cache (deprecated) 数据块大小 分片大小 几g ~ 几十g, time based data, 20g ~ 40g 分片数量,每g内存小于20分片 shard越多,维护索引成本越高 shard越大,rebalance越慢 单表数据不超过2kw,3层b+树能存储的数据大概是2kw,如果b+层级变高,查询速度会显著降低 数据结构 json,底层是lucene table,底层是b+ tree 索引 倒排表,fst 正向文件,分块 + 压缩 DocValues, 映射文件 + 压缩 b+数,聚簇/非聚簇索引 定义数据结构的方式 mapping (dynamic mapping & static mapping) schema 支持自动创建数据结构 是 否 事务 near real-time,需要refresh才可以查询到 reaptable read,高级事务 锁 Index blocks,比如 index.blocks.read_only,索引只读 丰富的锁机制,表锁,行锁,间隙锁 文件系统 默认mmapfs,采用内存映射方式访问文件,也支持其他的文件系统,比如fs, niofs, hybirdfs fs 数据恢复 es在写入之前会先将数据写入到translog,用来对异常情况进恢复 flush,lucene 进行提交,并且同时重新开启一段 translog index.translog.sync_interval,持久化translog 间隔,5s index.translog.flush_threshold_size, flush translog阈值大小,512m redo log采用的是WAL(Write-ahead logging,预写式日志),所有修改先写入日志,再更新到Buffer Pool,保证了数据不会因MySQL宕机而丢失,从而满足了持久性要求 es 和 mysql 处理数据恢复的模式基本一致 flush机制 从内存缓存写入磁盘缓存memorybuffer -> filesystem cache(refresh) 刷盘,filesystem cache -> disk ( flush) 定时触发或者 translog > 512M buffer pool -> disk 当redo log满了,或者buffer pool空间不足 es 和 mysql 刷盘模式基本一致 备份 snapshot mysqldump -u root -h host -p --all-databases > backdb.sql 慢日志 比如 index.search.slowlog.threshold.query.warn: 10s long_query_time=10 服务调用方式 rest api mysql connection + sql 数据类型 较为丰富的数据类型,boolean, keyword, long, data, object, nested, range, ip, text, arrays int, data, varchar es 提供了非常多的数据类型,一些是为了支持全文检索,一些能够方便查询,比如range,ip 数据属性 analyzer,分词器 index,是否被索引,没有被索引的字段不可查询 fielddata,如果想对text类型的字段进行聚合,排序,或者执行脚本,就必须设置fielddata属性 doc_values,将_source 转化为表结构放在磁盘上,方便聚合,排序,或者脚本操作,默认支持除了text类型的所有类型 ... 主键索引, 可空,唯一值,自增,默认值 es的数据属性更复杂 查询超时 设置 query timeout set wait_timeout = 10 context es查询需要区分query context, 还是 filter context,前者会进行打分,后者只进行过滤 不需要区分 打分查询 比如match,match_phrase 不支持 runtime field 使用script 创建临时字段 语法支持 select concat (a, b) as c script更灵活,但是性能会降低 精确查询 比如term, terms, ids, exists 语法支持 mysql使用起来更方便 分组聚合查询 比如histogram aggs,terms aggs group by es支持的类型稍微丰富一些,方便开发 指标聚合查询 avg, max, min, sum ,count, cardinality aggs,percentile aggs 语法支持, count(*), distinct es是分布式的,聚合的时候存在一些精度问题 分页 from + size (不适合深分页,有去重问题) search_after + PIT (推荐) scroll (不适合深分页) limit + size 或者进行条件关联,书签 在深分页上的处理方案上基本一致 profile { "profile": true, "query" : { "match" : { "message" : "GET /search" } } } explain script支持 painless script 不支持
2024-01-19
167
0
0
网络聚合
2024-01-19
遗传算法求TSP问题
一、实验内容及目的 本实验以遗传算法为研究对象,分析了遗传算法的选择、交叉、变异过程,采用遗传算法设计并实现了商旅问题求解,解决了商旅问题求解最合适的路径,达到用遗传算法迭代求解的目的。选择、交叉、变异各实现了两种,如交叉有顺序交叉和部分交叉。 二、实验环境 Windows10 开发环境Python 3/Flask 三、实验设计与实现 图1软件结构图 图1软件结构图 Flask.py是后端核心代码,里面是遗传算法实现,index.html为首页,即第一次进入网页的页面,进入之后可以进行参数设置,之后点击开始,参数会传到Flask.py中进行解析和算法运行,最终将迭代结果存到result(存储迭代结果图)和result_path(存储最短路径图)在返回给display.html页面显示。 图2系统界面图 图2系统界面图 输入种群规模、迭代次数、变异概率、选择比例、交叉概率并选择变异方法、选择个体方法、交叉方法。点击开始即可运行该系统。 具体算法流程图: 图3核心算法流程图 流程图描述:首先根据参数城市数量和种群规模初始一个城市坐标矩阵的列表并计算城市间的距离存到矩阵,最后生成一个路径矩阵,这样就可以进入下一步计算适应度,每一条路径都有其路径距离值和适应度,接下来一次进行选择,交叉,变异操作,循环往复,直至达到了参数中的迭代次数限制。 选择—轮盘赌:(这里我的算法选出的种群数量不一定就恰好是根据比例算出的数量) 图3核心算法流程图 图4轮盘赌流程图 选择—锦标赛: 图5三元锦标赛流程图 交叉—顺序交叉: 1、 选切点X,Y 2、 交换中间部分 3、 从第二个切点Y后第一个基因起列出原顺序,去掉已有基因 4、 从第二个切点Y后第一个位置起,将获得的无重复顺序填入 图6顺序交叉动态图 图7顺序交叉静态图 交叉—部分交叉: 1、 选切点oop 2、 选取oop到oop+3部分交换(我这里就是三个,你可以做成随机的几个) 3、 判断是否有重复的,若重复则进行映射,保证形成的新一对子代基因无冲突。 图8部分交叉动态图 变异—两点交换 1、 随机选取两点 2、 两点进行交换 变异—相邻交换 1、 随机选取一点 2、 和该点的后面点进行交换 适应度函数:经过测试得A取5,B取0效果好,所以实验中直接取了A=5,B=0运行 借鉴了sigmoid函数的形式,并对数据做了最大最小标准化,A、B是人为给定的常系数mean、max、min是种群所有个体的目标函数值的均值、最大值、最小值图像如下A=5,B=0 适应值较大的更容易进入下一代种群中 图9适应度函数算术表达式 四、实验结果与测试 表1 遗传算法解决TSP问题的测试用例 测试内容 测试用例 预期结果 实际结果 种群规模 1.不输入2.输入除数字其他3.输入整数数字4.输入小数或者负数 失败失败成功失败 与预期相同 迭代次数 5.不输入6.输入除数字其他7.输入整数数字8.输入小数或者负数 失败失败成功失败 与预期相同 变异方法 9.选择两点交换10.选择相邻交换 成功成功 与预期相同 选择个体方法 11.选择轮盘赌12.选择锦标赛 成功成功 与预期相同 交叉方法 13.选择部分交叉14.选择顺序交叉 成功成功 与预期相同 变异概率 15.不输入16.输入除数字其他17.输入小于1的小数18.输入非小于1的小数或者整数 失败失败成功失败 与预期相同 选择比例 19.不输入20.输入除数字其他21.输入小于1的小数22.输入非小于1的小数或者整数 失败失败成功失败 与预期相同 交叉概率 23.不输入24.输入除数字其他25.输入小于1的小数26.输入非小于1的小数或者整数 失败失败成功失败 与预期相同 随机产生多少个城市 27.不输入28.输入除数字其他29.输入整数数字30. 输入小数或者负数 失败失败成功失败 与预期相同 图10参数设置图 在上述参数设置好之后,即可开始运行系统,最后产生如图11的迭代结果图,最上面是自己的参数设置和最后生成的最小路径min_dist,图示整体为每次迭代的路径距离,可见随着迭代次数增加,路径距离一直减小最后趋于稳定。图12为用python画的路径图,图中横轴纵轴为城市位置的X,Y坐标。 图11 迭代结果图 图12最短路径图 接下来重新选择其他参数来运行一下,看一下有没有区别。 图13参数设置图 图14迭代结果图 图15最短路径图 可以从迭代图像看出,参数不同会导致迭代中结果的不同,第一次参数设置的迭代中在前段迭代不稳定,忽上忽下,之后稳定,而第二次参数设置后迭代很快就稳定,没有忽上忽下的现象,所以不同的选择、变异、交叉方法会使迭代结果不同。所以可以根据随机设定让计算机找到最合适的参数设置。 欢迎关注我的知乎平台,我将持续为您解答一系列问题!
2024-01-19
99
0
0
网络聚合
2024-01-19
concurrent-map 和 sync.Map,我该选择哪个?
concurrent-map 和 sync.Map,我该选择哪个? 官方的map并不是线程安全的,如果我们在多线程中并发对一个map进行读写操作,是会引发panic的。解决方案除了使用锁来对map进行保护外,还有两种方式: 一,开源项目 concurrent-map 提供了可以用来做并发安全的map 二,Go1.9之后,标准库提供了一个sync.Map 这两种并发安全的map,我们应该怎么选择呢? 在concurrent-map我看到这么一段话: 标准库中的sync.Map是专为append-only场景设计的。因此,如果您想将Map用于一个类似内存数据库,那么使用我们的版本可能会受益。你可以在golang repo上读到更多,这里 and 这里 译注:sync.Map在读多写少性能比较好,否则并发性能很差 concurrent-map为什么会有这种表述呢?这篇文章就来庖丁解牛下。 concurrent-map concurrent-map是Golang中一个流行的并发安全的哈希表库,它允许多个goroutine同时对哈希表进行读写操作,而不需要使用显式的锁或同步原语。 该库的核心原理是使用分片锁,将哈希表分成多个小的哈希表片段,并为每个片段分配一个独立的锁。当多个goroutine尝试同时读写同一个片段时,只有该片段上的锁会被锁住,而其他片段的锁则不受影响,从而避免了整个哈希表被锁住的情况。 当进行写操作时,只需要锁住要写入的片段的锁,以确保原子性操作。当进行读操作时,则不需要锁住片段的锁,只需要对该片段上的读取操作进行同步即可。 此外,concurrent-map库还使用了一些优化策略,如缓存哈希值和桶的地址,以减少计算和查找时间,从而提高并发读写性能。 总之,concurrent-map库的原理是基于分片锁和其他优化策略来实现高效的并发安全哈希表。 我们先看它的使用方式: // 创建一个新的 map. m := cmap.New[string]() // 设置变量m一个键为“foo”值为“bar”键值对 m.Set("foo", "bar") // 从m中获取指定键值. bar, ok := m.Get("foo") // 删除键为“foo”的项 m.Remove("foo") 它的New方法创建了一个ConcurrentMap结构 type ConcurrentMap[K comparable, V any] struct { shards []*ConcurrentMapShared[K, V] sharding func(key K) uint32 } 我们看ConcurrentMap结构中的shards,是用来代表map分片之后的这些存储分片ConcurrentMapShared。 而sharing这个匿名函数代表的是分配的hash函数。 而存储分片是一个基础的,带有互斥锁的map type ConcurrentMapShared[K comparable, V any] struct { items map[K]V sync.RWMutex } 所以看到这里我们其实心里明白了个七七八八了,再看下它的New/Set/Get的流程如下: flowchart LR cmap.New --> 创建一个ConcurrentMap --> 初始化ConcurrentMapShared cmap.Set --> 根据需要设置的key查找对应的ConcurrentMapShared --> 加锁写分片中的map cmap.Get --> 根据需要查找的key找出对应分片ConcurrentMapShared --> 加读锁读取分片中的map 是的,基本原理就是如上图所示。concurrent-map就是将一个大map拆分成若干个小map,然后用若干个小mutex 对这些小map进行保护。这样,通过降低锁的粒度提升并发程度。毕竟嘛,一个诸葛亮不如十个臭皮匠。 sync.Map sync.Map是Golang标准库中提供的一个并发安全的哈希表,它与常规的map相比,可以在多个goroutine并发访问时,保证数据的安全性和一致性。 理解sync.Map,最关键就是理解Map结构。 type Map struct { mu Mutex //互斥锁,用于锁定dirty map //优先读map,支持原子操作,注释中有readOnly不是说read是只读,而是它的结构体。read实际上有写的操作 read atomic.Value // readOnly // dirty是一个当前最新的map,允许读写 dirty map[any]*entry // 主要记录read读取不到数据加锁读取read map以及dirty map的次数,当misses等于dirty的长度时,会将dirty复制到read misses int } 这里的sync.Map的逻辑还是比较复杂的。我们再看它的Store函数和Load函数。 func (m *Map) Store(key, value any) func (m *Map) Load(key any) (value any, ok bool) 我们先把Store的代码流程图画出来 flowchart TD Store-->判断read中是否有key{判断read中是否有key} 判断read中是否有key{判断read中是否有key}--有key-->在read中tryStore-->CompareAndSwapPointer-->原子替换read中对应指针 判断read中是否有key{判断read中是否有key}--没有key-->加锁-->判断key的位置 判断key的位置--在read中存在-->dirty中存入这对keyvalue-->read中原子替换指针-->解锁 判断key的位置--在read中不存在\n在dirty中存在-->dirty中原子替换指针-->解锁 判断key的位置--在read中不存在\n在dirty中不存在-->read中所有元素复制到dirty一份-->read中增加这个keyvalue-->dirty中增加这个keyvalue-->解锁 我们看下,这里面有几个步骤是非常有细节的。 首先,第一次判断read中是否有key的时候是没有加锁的,所以当第一次判断结束后,一旦明确read中没有key,要做后续的操作之前,先做一次加锁操作,做完加锁操作之后,又判断了一次key是否在read中。这是为什么呢?其实是由于在加锁这个操作的前后,map还是有可能有变化的,人不可能两次踏入同一个河流,map也不可能在加锁前后两次都不变,所以这里必须进行二次判断,这里可以说是非常细节了。 其次,在判断read或者dirty中已经有key的时候,Store做的操作不是复制一份value到目标结构,而是使用原子替换atomic.StorePointer 来将目标map中key对应的value指针替换为参数value。为什么呢? - 这是极致的性能优化写法,原子替换能减少一次值拷贝操作,做一次指针赋值就能替换拷贝内存操作。从这里我们也能理解为什么这个并发map会放在atomic包中,因为它的实现大量依赖atomic的原子操作。 同样,我们将Load的代码转化为流程图如下, flowchart TD Load --> 判断read中是否有key{判断read中是否有key} 判断read中是否有key{判断read中是否有key}--有key-->直接返回对应的value 判断read中是否有key{判断read中是否有key}--没有key-->加锁-->再次判断read中是否有key{再次判断read中是否有key} 再次判断read中是否有key{再次判断read中是否有key} --有key-->直接返回对应的value 再次判断read中是否有key{再次判断read中是否有key} --没有key-->返回dirty中是否有key-->标记map的miss值加一-->如果miss值大于dirty的个数-->将dirty中的map通过指针切换到read-->dirty置空-->标记map的miss值为0 从Load中我们大致能看出sync.Map的思路。 sync.Map内部使用两个map,read和dirty。其实read的map的作用是挡在读写操作的第一个屏障。如果读写在这个read中能直接操作的话,我们就直接在read中读写,那么就可以完全避免使用锁,性能自然就提升了。 而dirty的作用就相当于是一个缓冲区,一旦要写的key在read中找不到,我们就会先写dirty中。这个好处是什么?也是不去影响读read的操作,不会出现并发读写一个数据结构的情况。 而什么时候dirty的缓存清空同步到read中呢?就是“当map的miss标记大于dirty的个数的时候”。 这里我读的时候也确实有这个疑问,为什么是“当miss标记个数大于dirty个数”。而不是当miss标记个数大于某个值呢?我是这么理解,miss是代表读操作在read中失效的数量,而dirty个数代表写操作在read中失效的数量。如果使用固定值来比对miss个数,那么这个固定值是不好定的,比如一个有10个key的map和一个有10000个key的map如果都是一样的固定值,那是明显不合适的。所以就找了这么个“浮动阈值”。 concurrent-map和sync.map的比较 我们再回到最开始的那一段话: 标准库中的sync.Map是专为append-only场景设计的。因此,如果您想将Map用于一个类似内存数据库,那么使用我们的版本可能会受益。你可以在golang repo上读到更多,这里 and 这里 译注:sync.Map在读多写少性能比较好,否则并发性能很差 通过以上的代码分析,我们看出sync.Map的这个机制,是一个想追求无锁读写的结构,它最好的运行方式是读永远都命中read,写只命中dirty,这用能不用任何锁机制就能做到map读写。而它最差的运行状态是read和dirty不断做替换和清理动作,性能就无法达到预期。而什么时候可能出现最差运行状态呢?- 大量的写操作和大量的读操作。大量读写会导致“map的miss标记大于dirty的个数”。 这个时候sync.Map中第一层屏障会失效,dirty就会频繁变动。 而current-map就相当于是一个比较中等中规中矩的方案。它的每次读写都会用到锁,只是这个锁的粒度比较小。它的最优运行方式是我们的所有并发读写都是分散在不同的hash切片中。它的最差运行方式就是我们所有的并发读写都集中在一个hash切片。但是按照实际运行逻辑,这两种极端情况都不会发生。 所以总结下来,concurrent-map 的这段话确实没有骗我们: sync.Map在读多写少性能比较好,而concurrent-map 在key的hash度高的情况下性能比较好。 在无法确定读写比的情况下,建议使用 concurrent-map。 最后说一句:世上本没有烦恼,选择多了,便有了幸福的烦恼。 参考 https://segmentfault.com/a/1190000015242373
2024-01-19
202
0
0
网络聚合
64
65
66
67
68