
在tokio::spawn中起了一个新协程,然后在代码中执行sleep死循环,发现程序运行时,主线程中无法通过ctrl+c终止进程。代码如下
#[actix_web::main]
async fn main() -> std::io::Result<()> {
// 解析命令行参数
let args = Arguments::parse();
if args.version {
print_ver();
return Ok(());
}
// 初始化摄像头
spawn(async move {
info!("camera capture started");
camera_capture().await;
});
let c = config::get_config();
let listen = c.tlps.as_ref().unwrap().listen.as_ref().unwrap();
println!("listen http://{}", listen);
HttpServer::new(|| {
App::new().service(camera_settings).service(
SwaggerUi::new("/swagger-ui/{_:.*}").url("/api-docs/openapi.json", ApiDoc::openapi()),
)
})
.bind(listen)?
.run()
.await
}
async fn camera_capture() {
let tlps_cfg = config::get_config().tlps.as_ref().unwrap();
loop {
std::thread::sleep(std::time::Duration::from_secs(1));
print_ver();
}
}
现象,在终端中按下Ctrl+C无法退出进程,如下

最后发现原因是tokio拉起的线程(协程)中不能使用std::thread的sleep方法进行休眠,而是应该使用tokio的sleep方法休眠。我这里是actix-web,虽然它是基于tokio的,但是,他也有对应的sleep方法,应该改为如下方式
loop {
actix_web::rt::time::sleep(std::time::Duration::from_secs(1)).await;
print_ver();
}
再使用ctrl-c,能够正常终止进程了。
rust中,trait相当于其他语言中的接口。例如json序列化,需要实现Serialize这个trait,但是,这里存在如下三个限制:
也就是说,你想要对一个结构体去实现某个trait,要么,这个结构体是你的(也就是在你当前crate包中),要么,这个trait是你定义的,两者至少有一个是你自己的,你才能对这个结构体实现这个指定的trait。
如果,trait和结构体都不是你实现的,你将无法为这个外部结构体实现外部trait。例如,我想为TcpStream实现Serialize,直接写,会报错

`mio::net::TcpStream` is not defined in the current craterustcE0117
main.rs(24, 1): original diagnostic
只能通过包裹TcpStream来实现了,总之,为外部结构体实现外部trait就是行不通。
数字类型转换是所有语言中存在的一项操作。比如golang的float64转float32,用float32(v64)即可实现,但是这个转换在v64的值只要不超过float32的上限,就可以安全转换。但是,这在rust中似乎行不通。
我在一个实践程序里在结构体中定义了一系列的f32,i32字段,然后用parse::<f32>()从字符串解析出f32,这一切都正常,我单独单测代码打印过这个结果,正确的读取了数字;但是当我将其进行了一系列的累加之后,转为f64再json序列化,发现居然变成了null,然后我debug打印发现,数字变成inf了,也就是f64::INFINITY,我类型转换过程中用的就是as f64。
后来我索性将定义的f32改为了f64,最后它又正常了。这说明as操作并没有想象中那么安全。
参考上一篇《初学rust,如何实现在运行时对全局变量设置和读取》,文章中说,可以用OnceCell来定义全局变量,然后就可以对全局变量进行读写,实际上,我发现OnceCell的set()方法只能调用一次,如果你试图第二次调用set()来修改已经设定好的值,将会报错,设置值就失败了。
由于这一切都是为了读写的并发安全。所以OnceCell是无法实现读写全局变量的。要实现读写全局变量,应该使用RwLock。如下
定义全局变量
static WORKER_PID: RwLock<Option<u32>> = RwLock::new(None);
读写全局变量
// 写入
fn store_worker_pid(pid: u32) {
let mut data = WORKER_PID.write().unwrap();
*data = Some(pid);
}
// 读取
let data = WORKER_PID.read().unwrap();
let cpid = data.unwrap();
这里的读写就可以多次进行了。可以看到,这里使用了读写锁,写的时候会先通过write()方法获取写入锁实例,然后对其赋值写入。读取的时候会先通过read()方法获取读取锁实例,然后取里面的值。这样一来就实现了全局变量的读写。
先看一段代码
#[test]
fn test_hash_map() {
let mut mp: HashMap<&str, &str> = HashMap::new();
mp.insert("k", "v");
mp.insert("k1", "v1");
let x = mp.keys().clone();
for k in x {
mp.insert("k2", "v1");
mp.insert("k3", "v1");
}
mp.insert("k2", "v1");
mp.insert("k3", "v1");
}
看,它报错
error[E0502]: cannot borrow `mp` as mutable because it is also borrowed as immutable
--> src/process.rs:153:9
|
151 | let x = mp.keys().clone();
| -- immutable borrow occurs here
152 | for k in x {
| - immutable borrow later used here
153 | mp.insert("k2", "v1");
| ^^^^^^^^^^^^^^^^^^^^^ mutable borrow occurs here
报错告诉我们,不允许将mp作为mutable,因为它已经用于immutable了。难道,HashMap插入数据完毕,开始读取数据之后,不能再次插入数据了?我一开始这么怀疑,不应该啊,于是代码改成这样
#[test]
fn test_hash_map() {
let mut mp: HashMap<&str, &str> = HashMap::new();
mp.insert("k", "v");
mp.insert("k1", "v1");
mp.keys().clone();
mp.insert("k2", "v1");
mp.insert("k3", "v1");
}
正常了,没报错了。看样子问题出在这个for ..in..中。仔细分析代码,我在for中遍历了它的key,或者说我正在将这个HashMap中的数据拿出来,这时候,我在for中尝试往这个HashMap中写入数据,写入数据会让这个HashMap发生变更,这里第一感让我觉得可能有问题。比如我HashMap中有10个元素,我在遍历它,然后在for中间插入新元素,那么是不是有下列问题:1,我的HashMap会不会越遍历越多,会不会永远无法遍历完;2,HashMap是无序的,我将新元素插入HashMap中,会不会导致我已经遍历过的数据由于插入新数据,导致再次被读取出来,因为它可能位置发生变化了嘛。
其实这就是数据竞争和它带来的不确定性问题,rust作为一个内存安全第一的编程语言,编译器会教你做人。
于是,我再改
#[test]
fn test_hash_map() {
let mut mp: HashMap<&str, &str> = HashMap::new();
mp.insert("k", "v");
mp.insert("k1", "v1");
let x: Vec<&str> = mp.keys().map(|k| *k).collect();
for k in x {
mp.insert("k2", "v1");
mp.insert("k3", "v1");
}
mp.insert("k2", "v1");
mp.insert("k3", "v1");
}
这回正常了。可以看到我的操作let x: Vec<&str> = mp.keys().map(|k| *k).collect();是将keys()拿到的Keys迭代器(仍旧从前面的HashMap里迭代)通过.collect()方法将迭代器里的元素“倒”入到Vec<&str>,这样这个Vec就是一个独立与HashMap内存空间之外的变量,再基于这个Vec进行遍历,就可以避免“边遍历边修改的”的情况了。
那么大家可以思考一下,其他语言,比如golang,遇到这种情况是怎么处理的呢。
最近尝试在一个项目中引用一个私有crate,这个crate是一个lib,然后存储在私有git仓库中,并且这个git平台不支持rust的crate-index.
于是我在我的项目中这样引用外部依赖包
[package]
name = "las"
version = "0.1.1"
authors = ["...."]
edition = "2018"
[dependencies]
elasticsearch = { version = "8.5.0-alpha.1", default-features = false, features = [
"rustls-tls",
] }
logwatcher = "0.1.1"
json = "*"
reqwest = { version = "0.11.12", default-features = false, features = [
"blocking",
"rustls-tls",
] }
toml = "0.8.6"
serde_derive = "1.0.177"
serde = "1.0.177"
serde_json = "1.0.104"
once_cell = "1.18.0"
futures = "0.3"
tokio = { version = "*", features = ["full"] }
uuid = { version = "1.4", features = ["v4", "fast-rng", "macro-diagnostics"] }
chrono = "0.4"
core_affinity = "0.8.0"
geoip = { git = "ssh://git@git.xxxx.com/xxxx/tgeoip.git", branch = "master", package = "geoip" }
nix = "0.26"
libc = "0.2.146"
clap = { version = "4.4.2", features = ["derive"] }
log4rs = { version = "1.2.0" }
log = { version = "0.4.20" }
[build-dependencies]
regex = "1.6.0"
需要注意的是,我git@git.xxxx.com:xxxx/tgeoip.git仓库中是一个workspace,里面有2个子项,一个是可执行bin项目,叫做regen,一个是外部可依赖包,叫做geoip(后续实践表明同一个workspace里面的子项引用就应该这么干,git路径是同一个,使用package指定子项的包名),我这样引入之后,发现死活拉取不了私有git依赖。报错没权限拉取,git鉴权失败
error: failed to get `geoip` as a dependency of package `las v0.1.1 (/data/code/rust/las)`
Caused by:
failed to load source for dependency `geoip`
Caused by:
Unable to update ssh://git@git.xxxx.com/xxxx/tgeoip.git?branch=master#e41c5279
Caused by:
failed to clone into: /root/.cargo/git/db/tgeoip-9094aceea5940357
Caused by:
failed to authenticate when downloading repository
* attempted ssh-agent authentication, but no usernames succeeded: `git`
if the git CLI succeeds then `net.git-fetch-with-cli` may help here
https://doc.rust-lang.org/cargo/reference/config.html#netgit-fetch-with-cli
Caused by:
no authentication methods succeeded
各方查询后,都是建议添加这个选项,我开始还没搞清楚加在哪儿,但是发现使用环境变量好使。各方尝试后:
添加到~/.cargo/config文件中,如下
[net]
git-fetch-with-cli = true
然后再次执行cargo build无需环境变量即可。(前提是你本地已经配置好了访问ssh://git@git.xxxx.com/xxxx/tgeoip.git的ssh key)
rlib我在这个bin项目中引用geoip,发现一直报错
failed to resolve: use of undeclared crate or module `geoip`
百思不得其解,最后发现是我依赖包申明类型的问题,我Cargo.toml申明的是
[package]
....
[lib]
name = "geoip"
crate-type = ["staticlib", "cdylib"]
[dependencies]
bincode = "1.3.3"
serde = { version = "1.0.193", features = ["derive"] }
实际lib应该申明rlib类型,才能被正常引用,改为如下,更新依赖,就没报错了
[package]
....
[lib]
name = "geoip"
crate-type = ["staticlib", "cdylib", "rlib"]
[dependencies]
bincode = "1.3.3"
serde = { version = "1.0.193", features = ["derive"] }
最近入手了一块riscv64的开发板,是某宝上入手的StarFive2,准备拿它当做riscv64架构的服务器用来编译程序。开始刷上的是官方给的debian系统,成功启动了,nvme正常,但是有两个问题,第一个是apt update会报签名错误,第二个是不支持docker,想要让它支持docker,需要自己编译升级内核,非常麻烦。于是决定换用ubuntu版本的镜像。
根据官方教程,需要先将板载固件刷新到ubuntu社区的指定版本,从这里下载,需要注意的是,目前(2023/12/18)最新版的固件(u-boot-starfive-2024.01~rc4-0ubuntu1~ppa1这个)会导致kernel panic,需要下载下面的版本
,展开后,用wget下载deb安装包,找个ubuntu机器,用dpkg -x解包,将里面的下面两个文件上传到sdcard.img刷机包启动的系统里面

更新SPL
flashcp -v u-boot-spl.bin.normal.out /dev/mtd0
更新U-Boot
flashcp -v u-boot.itb /dev/mtd2
然后安装ubuntu系统,注意从这里选择下面版本的系统

使用etcher将固件写入到sd卡,插入sd卡,启动,正常情况就可以启动成功。
在这中间我踩过一些坑,在此也一起分享一下。首先是hdmi插在4k屏无法显示,这样一来,我就不知道当前系统启动得怎么样了,因此kernel panic了,我也不知道,后来只能使用usb转串口用电脑连接串口,将串口控制台当做显示器用,才知道启动过程中发生了什么,串口连接如下图

从串口控制台看到如下图(学到了,串口还能当显示器用)

可以看到,启动过程卡死在kernel panic,我这才知道是内核崩溃了。看起来是u-boot-starfive-2024.01~rc4-0ubuntu1~ppa1这个版本的固件,对现有的23.04版本的系统兼容性有问题,所以我换了上个版本的固件,成功启动。
到此,在StarFive2上成功的安装上了ubuntu系统,docker也成功的安装,不过这个版本的板载固件 有个问题,就是无法识别nvme硬盘,希望下个正常版本能够支持。
rust中如何比较优雅的进行错误处理,这是一直以来困扰我的一个问题。最近写了一个ip地址库查询包,于是在其中实践了一下自定义错误、错误抛出等处理。
rust中,如果一个函数需要返回错误,那么应该用Result包裹返回值,Result定义如下
enum Result<T, E> {
Ok(T),
Err(E),
}
通常,Result中的第二个参数就是错误。比如Result<String, String>,那么它的错误就是一个String类型的值,如下
// 返回了正常值hi
fn find() -> Result<String, String> {
Ok("hi".to_string())
}
// 返回了错误信息error
fn find() -> Result<String, String> {
Err("error".to_string())
}
那么有另外一个问题,就是,我们如何处理其他库或者第三方函数抛出给我们的错误呢?我们可以用expect(),unwrap()来解决可以解决的错误,但是有时候我们不希望处理错误,希望能够将错误抛出给上层,让上层调用者去处理。这时候,我们应该怎么定义错误类型?你当然可以在你的函数中进行错误处理之后,抛出String类型的错误,以便继续使用String类型作为错误类型。
#[test]
fn test_find() {
let x = foo();
println!("{:?}", x.unwrap())
}
fn foo() -> Result<File, String> {
let x = bar();
match x {
Err(e) => Err(e.to_string()),
Ok(f) => Ok(f),
}
...
}
fn bar() -> Result<File, io::Error> {
File::open("regions.txt")
}
你可以看到,我在foo()中调用bar(),bar()中返回了一个包含io::Error的错误,而我的foo()中要求返回的是String,我在foo中用match处理了错误的情况,并且foo中可能调用其他第三方库函数,返回的错误类型不尽相同,我每种错误类型都可以使用match解开,然后返回String类型的错误。但是这样处理起来,代码看起来就非常乱,到处都是match错误。
其实我们有另一种方法,可以自己定义一个Error类型,然后对Error类型进行扩展,让它兼容其他类型,如下
use std::{fmt, io};
#[derive(Debug)]
pub enum Error {
ParseError,
ReadError,
InvalidIPError,
}
impl std::error::Error for Error {}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Error::ParseError => write!(f, "Parse Error"),
Error::ReadError => write!(f, "Read Error"),
Error::InvalidIPError => write!(f, "Invalid IP Error"),
}
}
}
impl From<io::Error> for Error {
fn from(_: io::Error) -> Self {
Error::ReadError
}
}
impl From<bincode::Error> for Error {
fn from(_: bincode::Error) -> Self {
Error::ParseError
}
}
impl From<std::net::AddrParseError> for Error {
fn from(_: std::net::AddrParseError) -> Self {
Error::InvalidIPError
}
}
我们的Error类型,实现了fmt::Display,让它能够处理我的包项目中可能出现的第三方错误,并且针对这些错误实现了各自的From<T>的from方法,这样,这些第三方错误就可以直接以我的Error返回,使用?简写之后,代码就改成这样了
#[test]
fn test_find() {
let x = foo();
println!("{:?}", x.unwrap())
}
fn foo() -> Result<File, Error> {
let x = bar()?;
Ok(x)
}
fn bar() -> Result<File, io::Error> {
File::open("regions.txt")
}
对比可以看到,我在foo中处理bar抛出的错误变得简单了,直接一个问号就将可能得错误抛出到Error了,然后再将Ok(x)返回。这样一来,错误的抛出和处理就变得优雅多了。
事情的背景是这样了,我在gin框架的action中有个异步调用逻辑,然后异步调用需要使用context.Context接口作为参数传入,异步调用的模块中会从context中取request_id作为追踪追踪标记。于是我就直接讲*gin.Context作为参数传入了异步调用中。
然后灾难就发生了,我日志记录中能查到当前请求的request_id,但是发现条数不对,异步请求中记录的日志条数有10条,我用request_id去搜索,只查到了4条,最后我只能添加另外标记,通过另外的标记查询到10条日志,让我奇怪的是,另外6条日志的request_id却不是当前请求的request_id。
最后,我发现gin.Context专门有个Copy方法,是将context拷贝一份,我调用Copy拷贝一份context之后将拷贝的context传入异步调用,果然10条日志的request_id一致了。看来,从gin框架传入进来的context在不同的action中是复用的。然后在请求处理完毕之后,会给其他请求复用,这样传给异步调用模块的context中的request_id已经变了。
r.GET("/async", func(c *gin.Context) {
// 需要搞一个副本
copyContext := c.Copy()
// 异步处理
go func() {
time.Sleep(3 * time.Second)
log.Println("异步执行:" + copyContext.Request.URL.Path)
}()
})
这里必须要谨记,如果需要在action中异步调用并使用context传参,必须要将context.Copy()之后再传入。否则context就会被其他协程修改。