无畏并发
警告
此篇文章写于旧版本 Kovi ,其中的例子不可以直接用于新版本 Kovi。
前言
多线程并发是 Rust 的基础知识之一,Rust 能对你的多线程旅程保驾护航。因为 Kovi 框架监听消息是一定会用到闭包与多线程的,所以这里要提一嘴。
如果你学习过并发相关内容,可以跳过本章节。
还记得前文的生命周期吗,Kovi 会在启动时将所有插件的 main()
函数运行一遍。所有写在 main()
函数里的监听闭包都是惰性的,只会在消息来的那一刻运行一次。而这时, main()
函数可能早已运行结束。 main()
所有变量,会在 main()
结束的那一刻丢弃。
关于更多无畏并发,你可以查看这个。
move 关键字捕获变量
下面代码因为 bot
变量属于 main()
,所以闭包内无法使用闭包外的变量。
//此代码不可编译
use kovi::PluginBuilder;
#[kovi::plugin]
pub fn main(mut p: PluginBuilder) {
let bot = p.build_runtime_bot();
p.on_msg(|e| {
bot.send_private_msg(bot.main_admin, "msg");
})
}
编译器报错
error[E0373]: closure may outlive the current function, but it borrows `bot`, which is owned by the current function
--> plugins/testkovi/src/lib.rs:7:14
|
7 | p.on_msg(|e| {
| ^^^ may outlive borrowed value `bot`
8 | bot.send_private_msg(bot.main_admin, "msg");
| --- `bot` is borrowed here
|
note: function requires argument type to outlive `'static`
你可以通过 move
关键字来捕获闭包外的变量。此变量便归此闭包所有。
use kovi::PluginBuilder;
#[kovi::plugin]
pub fn main(mut p: PluginBuilder) {
let bot = p.build_runtime_bot();
p.on_msg(move |e| { // [!code ++]
bot.send_private_msg(bot.main_admin, "msg");
})
}
Arc 共享状态
学习了上面的 move
后,接下来我们看看如何在多个闭包里面使用同一个 RuntimeBot
。
下面代码因为 bot
被第一个闭包捕获了,所以第二个闭包不能使用 bot
。
//此代码不可编译
#[kovi::plugin]
pub fn main(mut p: PluginBuilder) {
let bot = p.build_runtime_bot();
p.on_msg(move |e| {
bot.send_private_msg(bot.main_admin, "msg");
});
p.on_msg(move |e| {
bot.send_private_msg(bot.admin[0], "msg");
})
}
Arc
是 Rust 提供的一种原子引用计数指针,它可以让你的变量在多个线程中共享。
每克隆一次 Arc
包裹的变量,计数便会增加 1 ,当计数为 0 时,该变量才会被抛弃。
所以应该这样写
use kovi::PluginBuilder;
use std::sync::Arc; // [!code ++]
#[kovi::plugin]
pub fn main(mut p: PluginBuilder) {
let bot = Arc::new(p.build_runtime_bot()); // [!code ++]
p.on_msg({
let bot = bot.clone();
move |e| {
bot.send_private_msg(bot.main_admin, "msg");
}
});
p.on_msg({
let bot = bot.clone();
move |e| {
bot.send_private_msg(bot.admin[0], "msg");
}
})
}
Mutex 互斥器
上面代码共享的 Bot
是不需要修改的,如果我们修改一个变量,那 Rust 编译器就会给我们警告了。
下面代码展示了,想要修改变量的错误示例。
// 下面代码不可编译
#[kovi::plugin]
pub fn main(mut p: PluginBuilder) {
let event_vec = Arc::new(Vec::new());
p.on_msg({
let event_vec = event_vec.clone();
move |e| {
event_vec.push(e.clone());
}
});
p.on_msg({
let event_vec = event_vec.clone();
move |e| {
event_vec.push(e.clone());
}
})
}
编译器报错
error[E0596]: cannot borrow data in an `Arc` as mutable
--> plugins/testkovi/src/lib.rs:10:13
|
10 | event_vec.push(e.clone());
| ^^^^^^^^^ cannot borrow as mutable
|
= help: trait `DerefMut` is required to modify through a dereference, but it is not implemented for `Arc<Vec<AllMsgEvent>>`
同时修改同一个变量的行为是糟糕的,这会发生不可预测的事情。所以 Rust 不允许这样做。
如何才能让代码正确呢。
Mutex
互斥器一次只允许一个线程访问数据。所有线程会按照前后请求访问循序,依次等待访问数据。
来修改一下代码:
use kovi::PluginBuilder;
use std::sync::{Arc, Mutex};
#[kovi::plugin]
pub fn main(mut p: PluginBuilder) {
let event_vec = Arc::new(Mutex::new(Vec::new()));
p.on_msg({
let event_vec = event_vec.clone();
move |e| {
event_vec.lock().unwrap().push(e.clone());
}
});
p.on_msg({
let event_vec = event_vec.clone();
move |e| {
event_vec.lock().unwrap().push(e.clone());
}
})
}
除了 Mutex
访问互斥器,还有 RwLock
读写互斥器。
Mutex
只允许有一个线程获得它里面的变量。其它线程都得乖乖排队。
你可以尝试 RwLock
,这是一个多读单写的互斥器。允许多个线程读。但是写的时候不允许第二个线程访问。
两者到底使用谁,如果不清楚的话,那就使用 Mutex
最好。
多线程并发的危险
尽可能早的释放锁。这样不仅可以让排队的线程更快的访问到数据,且减少了死锁的可能。尽量做到随锁随放。
下面是两种随锁随放的例子:
pub fn main(mut p: PluginBuilder) {
let event_vec = Arc::new(Mutex::new(Vec::new()));
p.on_msg({
let event_vec = event_vec.clone();
move |e| {
{
//使用作用域来让锁及时释放。
let mut event_vec_lock = event_vec.lock().unwrap();
event_vec_lock.push(e.clone());
}
}
});
p.on_msg({
let event_vec = event_vec.clone();
move |e| {
let mut event_vec_lock = event_vec.lock().unwrap();
event_vec_lock.push(e.clone());
drop(event_vec_lock); // [!code ++] //使用 drop() 来让锁及时释放。
}
});
}
两条线程互相请求对方拥有的资源,会导致死锁。这是必须要注意的。可以搜索学习,这里不再赘述。