reference: skyao.io/learning-to…
Tokio简介
异步
异步代码指的是使用async/await语言特性的代码,它允许许多任务在几个线程(甚至是一个单线程)上并发运行
异步就是在IO完成以后通过回调函数,中断通知你当前的程序执行,去处理IO事件,然后继续接着往下执行。
Future
future也有一个poll方法,它使操作继续进行,直到它需要等待某些东西,比如网络连接。Future通常是通过在一个异步块中使用.await来组合多个Future来创建的
执行器/调度器
执行器或者调用器是通过poll方法来执行Future中的方法。
tokio::spawn或 Runtime::block_on 函数创建任务(任务就是执行的操作)
通道
通道是一种工具,允许代码的一个部分向其他部分发送消息。Tokio提供了许多通道,每个通道都有不同的用途。
且channel中不允许使用阻塞线程来等待消息。
基础语法
.await和async
一次处理一个连接的速度有限,扩展到一次并发处理多个连接。tokoi 让你能够同时做很多事情,
tikio不适合做的事情:
- 并行运行计算: 当我们并行执行一个计算的时候是不适合的(有并行计算的库),适合于IO类型的任务。
- 读取大量的文件:理由是操作系统不提供异步文件API,没有线程池有优势。
- 发送单个网络请求:当你同时需要做很多的事情的时候tokio,但是若是一次只发送一个请求,使用阻塞的方式会更好,tokio没有提供阻塞式的API
let mut client = client::connect("127.0.0.1:6379").await?;
//.await 表示的异步操作
**在同步操作中若是程序不能够完成操作就会被阻塞,直到操作完成。**通过异步编程,不能立即完成的操作被暂停到后台。线程没有被阻塞,可以继续运行其他事情。一旦操作完成,任务就会被取消暂停,并继续从它离开的地方处理。
(当前只有一个任务,所有不明显,异步编程一般用在有许多个任务的时候)
async fn 转化为一个异步运行的routine。在 async fn 中对 .await 的任何调用都会将控制权交还给线程。当操作在后台进行时,线程可以做其他工作。
这因为 rust的异步编程,一种lazy方式的编程方式,与其他的语言最不一样的地方
async 体以及其他 future 类型是惰性的。要让async代码和future运行的起来
- 调用async的函数或者代码块,在返回的future没有执行,函数也是什么也不做。使用
.await操作符执行。
main 函数:
- 它是
async fn: 进入一个异步上下文。然而,异步函数必须由一个运行时来执行。运行时包含异步任务调度器,提供事件化I/O、计时器等。运行时不会自动启动,所以主函数需要启动它。 - 它被注解为
#[tokio::main]:#[tokio::main]函数是一个宏。它将async fn main()转换为同步fn main(),初始化一个运行时实例并执行异步main函数。
async fn world() -> String {
"world".to_string()
}
async fn hello() -> String {
// 使用 .await 关键字调用 world() 函数
let w = world().await;
format!("hello {} async from function", w)
}
fn main() {
// 创建运行时
let rt = tokio::runtime::Runtime::new().unwrap();
// 使用 block_on 调用 async 函数
let msg = rt.block_on(hello());
println!("{}", msg);
// 使用 block_on 调用 async block
let _ = rt.block_on(async {
println!("hello world async from block");
});
}
#[tokio::main]
//不再需要使用创建实例和block_on才能够的运行异步的代码
async fn main() {
println!("hello");
}
总结
async 标识的函数: 返回值都会被Future包裹(返回值类似都是Future)。
- 想要运行async方法必须要 创建一个运行实例,使用block_on方法。或者直接使用宏
.await 相当于解构Future取出其中的Output。hello().await 等待函数执行完之后解构其中的内容 (也就是hello().await的 值类型是String,让当前的等待Future执行结束)
-
spawn 的 JoinHandle ,并使用 await,即可以等待该任务结束。 .await 同时在异步编程中.await也会让出处理器。
-
await让出当前线程控制权给另外一个Future
提醒一下,在使用多线程的
Future执行器时,一个Future可能在线程间移动,所以任何在async体中使用的变量必须能够穿过线程,因为任何.await都有可能导致线程切换。这意味着使用
Rc,&RefCell或者其他没有实现Sendtrait 的类型是不安全的,包括那些指向 没有Synctrait 类型的引用。//多个future中切换。 //1. 在阻塞的异步方法中我们需要执行.await让着线程的控制权 //2.异步方法执行的 block_on 和.await ,否则话不会执行 async fn async_main() { //方法没有执行 let f1 = learn_and_sing(); let f2 = dance(); // `join!` 类似于 `.await` ,但是可以等待多个 future 并发完成 // 如果学歌的时候有了短暂的阻塞,跳舞将会接管当前的线程,如果跳舞变成了阻塞 // 学歌将会返回来接管线程。如果两个futures都是阻塞的, // 这个‘async_main'函数就会变成阻塞状态,并生成一个执行器 //future访问时候才会执行异步方法 futures::join!(f1, f2); // f1, f2 并行完成 }
任务
任务给tokio::spawn提供一个异步的代码块来实现的。任务是由调度器管理的执行单位。生成的任务提交给 Tokio 调度器,然后确保该任务在有工作要做时执行。生成的任务可以在它被生成的同一线程上执行,也可以在不同的运行时线程上执行。任务在被催生后也可以在线程之间移动。
tokio::spawn 函数返回 JoinHandle,调用者可以用它来与生成的任务进行交互
调用者可以使用 JoinHandle 上的 .await 获取返回值 (.await以后才会执行函数)。
.await会提取Future包裹的Output值。
async fn main(){
let handle = tokio::spawn(async { "return vale" });
//JoinHandler类型
let out= handle.await.unwrap();
println!("GOT :{:?}",out);
}
当你在Tokio运行时中催生一个任务时,它的类型必须是 'static 的。这意味着生成的任务不能包含对任务之外拥有的数据的任何引用。(可能因为‘static的生命周期更长,若是它依赖了外部的数据引用的话,就会出现悬空指针问题)
所以: 通过 move。将代码块拥有数据的所有权,使得数据变成'static
如果数据必须被多个任务同时访问,那么它必须使用同步原语(如Arc)进行共享。
因为 'static 生命周期一直持续到程序结束。
“bounded by 'static” 的术语,而不是 “its type outlives 'static” 或 “the value is 'static” for T: 'static。这些都是同一个意思,与 &'static T 中的注解 'static 是不同的
任务中的就是 “bounded by 'static” 的术语。
这里没有太看懂
解释是,必须超过
'static生命周期的是类型,而不是值,而且值可能在其类型不再有效之前被销毁。
Send bound
由 tokio::spawn产生的任务必须实现 Send。这允许Tokio运行时在线程之间 move 任务,而这些任务在一个 .await 中被暂停。
.await 被调用时,任务就回到了调度器中。 下一次任务被执行时,它将从最后的让出点恢复。为了使其正常工作,所有在 .await 之后使用的状态都必须由任务保存。
tokio::spawn(async {
let rc = Rc::new("hello");
// `rc` is used after `.await`. It must be persisted to
// the task's state.
//就是为了保护状态值(切换线程的时候)因为rc不是Send类型的
yield_now().await;
println!("{}", rc);
});
//阶段性的代码
client
use mini_redis::{client, Result};
#[tokio::main]
async fn main() -> Result<()> {
// Open a connection to the mini-redis address.
let mut client = client::connect("127.0.0.1:6379").await?;
// Set the key "hello" with value "world"
client.set("hello", "world".into()).await?;
// Get key "hello"
let result = client.get("hello").await?;
println!("got value from the server; result={:?}", result);
Ok(())
}
//服务端
use tokio::net::{TcpListener,TcpStream};
use mini_redis::{Connection,Frame};
use tokio::task::yield_now;
use std::collections::HashMap;
#[tokio::main]
async fn main(){
let listener = TcpListener::bind("127.0.0.1:6379").await.unwrap();
loop{
let (socket,_) =listener.accept().await.unwrap();
//使用spaw增加并发性
tokio::spawn(async move{process(socket).await});
}
}
async fn process(socket :TcpStream){
use mini_redis::Command::{self,Get,Set};
let mut db=HashMap::new();
let mut connection=Connection::new(socket);
while let Some(frame) =connection.read_frame().await.unwrap(){
let response = match Command::from_frame(frame).unwrap(){
Set(cmd)=>{
db.insert(cmd.key().to_string(),cmd.value().to_vec());
Frame::Simple("OK".to_string())
},
Get(cmd)=>{
if let Some(value)=db.get(cmd.key()){
Frame::Bulk(value.clone().into())
}else{
Frame::Null
}
}
cmd => panic!("unimplemented {:?}",cmd)
};
connection.write_frame(&response).await.unwrap()
}
}
Future机制
future机制,让程序员只关心最终结果。
//rust中的poll机制
reponse->request->socket
从建立网络连接开始 调用链交给计算机去帮你完成,不再需要自己去维护。
传统的方式: 在连接完成以后, 需要调用发送函数 来发送请求。
还需要在处理响应处理中 注册接受请求函数
future底层的实现机制是基于linux的epollo来实现的,在事件poll管理器中直接拿到处理程序的句柄,不再遍历整个事件队列,而是直接在中断处理响应中把通知发给对应处理进程。
注意Tokio当中的channel管道与Rust原生channel和crossbeam提供的Channel不是同一个概念,Tokio中对于消费者来说,调用recv API返回的还是一个Future对象,recv接收消息操作并不会阻塞进程。
Future是一种未来才用到的值类型。
在不同连接之间共享数据
对于共享状态的管理
- 用Mutex来保护共享状态。
- 生成一个任务来管理状态,并使用消息传递来操作它。(一般在异步工作中会选择使用第二种)
将一个Arc句柄传递给 process 函数
Arc是被用来共享所有权,因为Arc中共享的引用是不可变的。所以Mutex、RwLock或Atomic类来改变共享的引用
一个很大的误区:对于Arc的线程安全,对于Arc线程安全是指的是所有权的线程安全,而不是里面的值的线程安全。一般是和Mutex搭配使用来保证线程安全的
Arc<RefCell<T>>。RefCell不是Sync,如果Arc总是Send,那么Arc<RefCell<T>也会是Send。RefCell不是线程安全的;它使用非原子操作来跟踪借数。
Arc<Mutex<_>> //将许多任务在许多线程之间共享
type Db=Arc<Mutex<HashMap<String,Bytes>>>;
bd.lock().unwrap().
tokio::sync::Mutex。异步Mutex是一个跨调用 .await 而被锁定的Mutex。(什么意思?)
同步的mutex在等待获得锁的时候会阻塞当前线程。这反过来又会阻塞其他任务的处理。然而,切换到 tokio::sync::Mutex 通常没有帮助,因为异步mutex内部使用同步mutex。
减少数据竞争
为了减少数据的竞争,我们将引入N个不同的实例,而不是一个Mutex<HashMap<_, _»实例
首先,key 被用来识别它属于哪个分片。然后,在HashMap中查找该 key。
type ShardedDb = Arc<Vec<Mutex<HashMap<String, Vec<u8>>>>>;
let shard = db[hash(key) % db.len()].lock().unwrap();
因为 std::sync::MutexGuard 类型不是 Send。这意味着你不能把一个mutex锁发送到另一个线程,而错误的发生是因为Tokio运行时可以在每个 .await 的线程之间移动一个任务。
use std::sync::{Mutex, MutexGuard};
// This fails too.
async fn increment_and_do_stuff(mutex: &Mutex<i32>) {
let mut lock: MutexGuard<i32> = mutex.lock().unwrap();
*lock += 1;
//下面 没有会报错,这一句很重要。
drop(lock);
do_something_async().await;
}
作用域信息来计算一个future是否是Send。显示的丢弃锁。
因为如果Tokio在任务持有锁的时候将你的任务暂停在一个.await,一些其他的任务可能会被安排在同一个线程上运行,而这个其他的任务也可能试图锁定那个突变体,这将导致一个死锁,因为等待锁定突变体的任务会阻止持有突变体的任务释放突变体。
将Guard mutex放到结构体中
struct CanIncrement {
mutex: Mutex<i32>,
}
//只在该结构的非同步方法中锁定mutex
impl CanIncrement {
// This function is not marked async.
fn increment(&self) {
let mut lock = self.mutex.lock().unwrap();
*lock += 1;
}
}
//因为mutex guard不会出现在异步函数的任何地方。
async fn increment_and_do_stuff(can_incr: &CanIncrement) {
can_incr.increment();
do_something_async().await;
}
为什么出现这种 问题?
生成任务来管理状态,并使用消息传递来操作它
use tokio::sync::Mutex; // note! This uses the Tokio mutex
// This compiles!
// (but restructuring the code would be better in this case)
async fn increment_and_do_stuff(mutex: &Mutex<i32>) {
let mut lock = mutex.lock().await;
*lock += 1;
do_something_async().await;
} // lock goes out of scope here
Tokio mutex的主要特点是,它可以跨 .await 持有,而没有任何问题。也就是说,异步的mutex比普通的mutex更昂贵
通道
使用通道来传递消息,实现多个请求统一被发送给redis并返回应答给调用者。发出请求的任务都会向 client 任务发送一个消息。
原因: 之前的使用Mutex ,一个连接只能处理一个请求,导致Mutex的利用率不足。