rust tokio

1,119 阅读11分钟

reference: skyao.io/learning-to…

Tokio简介

异步

异步代码指的是使用async/await语言特性的代码,它允许许多任务在几个线程(甚至是一个单线程)上并发运行

异步就是在IO完成以后通过回调函数,中断通知你当前的程序执行,去处理IO事件,然后继续接着往下执行。

Future

future也有一个poll方法,它使操作继续进行,直到它需要等待某些东西,比如网络连接。Future通常是通过在一个异步块中使用.await来组合多个Future来创建的

执行器/调度器

执行器或者调用器是通过poll方法来执行Future中的方法。

tokio::spawnRuntime::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 或者其他没有实现 Send trait 的类型是不安全的,包括那些指向 没有 Sync trait 类型的引用。

    //多个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是一种未来才用到的值类型

在不同连接之间共享数据

对于共享状态的管理

  1. 用Mutex来保护共享状态。
  2. 生成一个任务来管理状态,并使用消息传递来操作它。(一般在异步工作中会选择使用第二种)

将一个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的利用率不足。