Rust~二刷异步逻辑

Pin

Pin 是 Rust 标准库中的一个类型,定义在 std::pin::Pin。它的主要作用是固定一个值的内存地址,防止该值被移动(即内存地址发生改变)。在 Rust 里,一般情况下变量是可以在内存中移动的,但在某些场景下,不希望值被移动,例如当一个结构体包含自引用时,移动这个结构体可能会导致引用失效,这时就需要使用 Pin 来固定其内存地址。

为什么需要 Pin

自引用结构体问题

rust 复制代码
struct SelfReferential {
    data: String,
    reference: &'static str,
}

impl SelfReferential {
    fn new() -> Self {
        let data = "hello".to_string();
        let reference = &data;
        Self { data, reference }
    }
}

上述代码无法编译通过,因为 reference 引用了 data,但 data 是在栈上分配的

当结构体移动时,data 的地址会改变,reference 就会变成悬空引用

使用 Pin 可以解决这个问题,确保结构体在内存中的位置固定不变

如何使用Pin

创建 Pin

可以使用 Pin::new 或 Pin::new_unchecked 来创建 Pin 类型的值

rust 复制代码
use std::pin::Pin;

#[derive(Debug)]
struct MyStruct {
    value: i32,
}

fn main() {
    let mut my_struct = MyStruct { value: 42 };
    let pinned: Pin<&mut MyStruct> = Pin::new(&mut my_struct);
    println!("{:?}", pinned)
}

Pin::new 会检查传入的引用是否为空,如果不为空则返回一个 Pin 类型的值

Pin::new_unchecked 则不会进行检查,需要使用者自己确保传入的引用不为空且后续不会被移动,使用时要格外小心

实现 Unpin trait

Unpin 是一个自动实现的标记 trait,它表示一个类型可以安全地被移动。如果一个类型实现了 Unpin,那么对它使用 Pin 就没有实际意义,因为它本来就可以被移动。

rust 复制代码
use std::pin::Pin;
use std::any::type_name;

#[derive(Debug)]
struct MyUnpinStruct {
    value: i32,
}

fn print_type_of<T>(_: &T) {
    println!("{}", type_name::<T>());
}

// MyUnpinStruct 自动实现了 Unpin
impl Unpin for MyUnpinStruct {}

fn main() {
    let mut my_struct = MyUnpinStruct { value: 42 };
    let pinned: Pin<&mut MyUnpinStruct> = Pin::new(&mut my_struct);
    // 由于实现了 Unpin,可以直接获取内部引用
    let unpinned = Pin::into_inner(pinned);
    println!("{:?}", unpinned);
    print_type_of(&unpinned);
}

在异步编程中使用 Pin

在异步编程中,Pin 经常用于异步任务和 Future。很多异步操作需要固定内存地址,以确保在等待过程中不会被移动。

例如,Future trait 的 poll 方法接收一个 Pin<&mut Self> 类型的参数,这意味着在调用 poll 方法时,Future 的内存地址必须是固定的。

rust 复制代码
use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll};

struct MyFuture {
    counter: u32,
}

impl Future for MyFuture {
    type Output = u32;

	// poll 方法是 Future trait 中唯一需要实现的方法,用于推进 Future 的执行
	// self: Pin<&mut Self>:self 参数是一个 Pin<&mut Self> 类型
	// Pin 用于固定 MyFuture 实例的内存地址,防止其在异步操作过程中被移动,确保内部引用的有效性。
	// &mut Self 表示对 MyFuture 实例的可变引用
	// cx: &mut Context<'_>:cx 是一个 Context 类型的可变引用
	// Context 包含一个 Waker,Waker 用于唤醒等待该 Future 的任务
	// Poll<Self::Output>:poll 方法的返回值类型是 Poll<Self::Output>,Poll 是一个枚举类型,有两个变体:
	// ---> Poll::Pending:表示 Future 尚未完成,需要稍后再次轮询
	// ---> Poll::Ready(T):表示 Future 已经完成,并返回一个 T 类型的值(这里 T 就是 Self::Output,即 u32)

	// 在很多情况下,具体的生命周期细节并不重要,使用 '_ 可以避免显式声明具体的生命周期名称
	// Context 类型在 Rust 的异步编程中用于传递 Waker 等信息,Waker 用于唤醒等待的任务
	// poll 方法里的 Context 引用的生命周期是由调用 poll 方法的上下文决定的
	// 使用 '_ 让代码不必显式声明这个生命周期,编译器会根据实际情况自动推断
    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
    	// 由于 self 是 Pin<&mut Self> 类型,不能直接对其进行解引用操作
    	// get_mut 方法用于获取 Pin 内部的可变引用,将其赋值给 this 变量,后续可以通过 this 来修改 MyFuture 实例的字段
        let this = self.get_mut();
        if this.counter < 10 {
            this.counter += 1;
            // 调用 cx.waker().wake_by_ref() 唤醒等待该 Future 的任务
            // 这是因为 MyFuture 还未完成,需要通知运行时在合适的时候再次调用 poll 方法
            //
            // 当 Future 阻塞等待的资源已经准备好时(例如 socket 中有了可读取的数据)
            // 该资源可以调用 wake() 方法,来通知执行器可以继续调用该 Future 的 poll 函数来推进任务的执行
            // 当一个 Future 处于 Poll::Pending 状态时,它通常会保存一个 Waker 实例
            // 当该 Future 所依赖的资源准备好时,就会调用 Waker 的方法来唤醒等待的任务
            cx.waker().wake_by_ref();
            Poll::Pending
        } else {
            Poll::Ready(this.counter)
        }
    }
}

wake 和 wake_by_ref的区别

wake 和 wake_by_ref 是 Waker 类型提供的两个重要方法,它们都用于唤醒等待的任务,但在使用方式和效果上存在一些区别

Waker 简介

Waker 是 Rust 异步编程中的一个核心概念,它是一个句柄,用于通知执行器某个任务已经准备好继续执行。当一个 Future 处于 Poll::Pending 状态时,它通常会保存一个 Waker 实例,当该 Future 所依赖的资源准备好时,就会调用 Waker 的方法来唤醒等待的任务。

wake 方法

rust 复制代码
fn wake(self);

所有权转移:wake 方法会消耗 Waker 实例本身,也就是会转移 Waker 的所有权。这意味着在调用 wake 之后,原来的 Waker 实例就不能再使用。

使用场景:当确定不再需要使用当前的 Waker 实例时,可以使用 wake 方法。例如,当一个 Future 完成了它的任务,并且不再需要这个 Waker 来唤醒自己时,就可以调用 wake 方法。

wake_by_ref 方法

rust 复制代码
fn wake_by_ref(&self);

借用引用:wake_by_ref 方法接收 Waker 实例的不可变引用,不会转移所有权。这意味着在调用 wake_by_ref 之后,仍然可以继续使用原来的 Waker 实例。

使用场景:当后续还需要使用同一个 Waker 实例来唤醒任务时,应该使用 wake_by_ref 方法。例如,在一个循环中,每次检测到资源准备好时都需要唤醒任务,这时就可以多次调用 wake_by_ref 方法。

rust 复制代码
use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll, Waker};
use std::thread;
use std::time::Duration;

struct MyFuture {
	// 一个可选的 Waker 实例
    waker: Option<Waker>,
    counter: u32,
}

impl Future for MyFuture {
	// Future trait 有一个关联类型 Output,用于指定该 Future 完成时产生的值的类型
	// Output 被指定为 (),表示该 Future 完成时不产生有意义的值
    type Output = ();

    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
        let this = self.get_mut();
        if this.counter < 3 {
            // 保存 Waker 实例
            // is_none 方法主要用于 Option<T> 类型
            // Option<T> 是 Rust 标准库中的一个枚举类型,它有两个变体:
            // ---> Some(T) 表示包含一个值
            // ---> None 表示没有值
            // is_none 方法用于检查 Option<T> 实例是否为 None 变体
            // 原型:pub const fn is_none(&self) -> bool
            if this.waker.is_none() {
                this.waker = Some(cx.waker().clone());
            }
            this.counter += 1;

            // 使用 wake_by_ref 方法唤醒任务,因为后续可能还需要使用该 Waker 实例
            if let Some(waker) = &this.waker {
                waker.wake_by_ref();
            }

            // 模拟耗时操作
            thread::sleep(Duration::from_secs(1));
            Poll::Pending
        } else {
            // 使用 wake 唤醒任务
            // 使用 wake 方法唤醒任务,并使用 take 方法将 waker 字段的值取出,因为此时不再需要该 Waker 实例
            if let Some(waker) = this.waker.take() {
                waker.wake();
            }
            Poll::Ready(())
        }
    }
}

#[tokio::main]
async fn main() {
    let mut my_future = MyFuture {
        waker: None,
        counter: 0,
    };
    let _ = Pin::new(&mut my_future).await;
}

在 poll 方法中,当 counter 小于 3 时,使用 wake_by_ref 方法多次唤醒任务,因为后续还需要继续使用 Waker 实例

当 counter 达到 3 时,使用 wake 方法唤醒任务,并且使用 take 方法将 Waker 实例从 Option 中取出,因为此时不再需要该 Waker 实例

示例

rust 复制代码
use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll, Waker};
use std::thread;
use std::time::Duration;

struct MyFuture {
    waker: Option<Waker>,
    counter: u32,
}

impl Future for MyFuture {
    type Output = ();

    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
        let this = self.get_mut();
        if this.counter < 3 {
            // 保存 Waker 实例
            if this.waker.is_none() {
                this.waker = Some(cx.waker().clone());
            }
            this.counter += 1;

            // 使用 wake_by_ref 唤醒任务
            if let Some(waker) = &this.waker {
                waker.wake_by_ref();
            }

            // 模拟耗时操作
            thread::sleep(Duration::from_secs(1));
            Poll::Pending
        } else {
            // 使用 wake 唤醒任务
            if let Some(waker) = this.waker.take() {
                waker.wake();
            }
            Poll::Ready(())
        }
    }
}

#[tokio::main]
async fn main() {
    let mut my_future = MyFuture {
        waker: None,
        counter: 0,
    };
    let _ = Pin::new(&mut my_future).await;
}

总结

wake 方法会消耗 Waker 实例的所有权,适用于不再需要使用该 Waker 实例的场景

wake_by_ref 方法借用 Waker 实例的引用,不会转移所有权,适用于需要多次使用同一个 Waker 实例来唤醒任务的场景

clone

clone 方法的行为取决于实现了 Clone trait 的具体类型,它既可以是浅拷贝,也可以是深拷贝,并且不同类型的指针在使用 clone 方法时表现也有所不同

Clone 是 Rust 标准库中的一个 trait,定义如下:

rust 复制代码
pub trait Clone {
    fn clone(&self) -> Self;

    fn clone_from(&mut self, source: &Self) {
        *self = source.clone()
    }
}

任何实现了 Clone trait 的类型都可以调用 clone 方法来创建自身的副本。clone 方法的具体实现由类型的开发者决定,因此它既可以执行浅拷贝,也可以执行深拷贝。

指针类型的 clone 方法

原始指针 (*const T 和 *mut T)

原始指针没有实现 Clone trait,因此不能直接调用 clone 方法。这是因为原始指针是非常底层的概念,Rust 不鼓励直接对其进行操作,而是推荐使用更安全的智能指针

Box

Box 是 Rust 中的一种智能指针,用于在堆上分配内存并持有一个值。Box 的 clone 方法执行深拷贝,它会在堆上为新的 Box 分配一块新的内存,并将原始 Box 中的值复制到新的内存中

rust 复制代码
fn main() {
    let box1 = Box::new(42);
    let box2 = box1.clone();

    println!("box1: {}", *box1); // 输出: box1: 42
    println!("box2: {}", *box2); // 输出: box2: 42
}

在这个例子中,box2 是 box1 的一个深拷贝,它们分别指向堆上不同的内存位置

Rc 和 Arc

Rc(引用计数指针)和 Arc(原子引用计数指针)用于实现共享所有权

它们的 clone 方法执行浅拷贝,不会复制指针指向的数据,而是增加引用计数

rust 复制代码
use std::rc::Rc;

fn main() {
    let rc1 = Rc::new(42);
    let rc2 = rc1.clone();

    println!("rc1 strong count: {}", Rc::strong_count(&rc1)); // 输出: rc1 strong count: 2
    println!("rc2 strong count: {}", Rc::strong_count(&rc2)); // 输出: rc2 strong count: 2
}

在这个例子中,rc2 是 rc1 的一个浅拷贝,它们共享同一个堆上的数据,引用计数变为 2

&T(不可变引用)和 &mut T(可变引用)

引用类型没有实现 Clone trait,因为引用本质上只是一个指针,复制引用并不会复制指向的数据

如果需要复制引用指向的数据,应该复制被引用的对象本身

总结

clone 方法的行为取决于具体类型的实现,可以是浅拷贝或深拷贝

原始指针不能直接调用 clone 方法

Box 的 clone 方法执行深拷贝

Rc 和 Arc 的 clone 方法执行浅拷贝,增加引用计数

引用类型没有实现 Clone trait,不能调用 clone 方法

代码解读

如下代码定义了 MiniTokio 结构体的一个方法 spawn,该方法用于将一个 Future 放入 MiniTokio 实例的任务队列中

rust 复制代码
impl MiniTokio {
    /// 生成一个 Future并放入 mini-tokio 实例的任务队列中
    fn spawn<F>(&mut self, future: F)
    where
        F: Future<Output = ()> + Send + 'static,
    {
        self.tasks.push_back(Box::pin(future));
    } 

spawn 方法的主要功能是接收一个实现了 Future trait 的对象,并将其包装成一个固定内存位置的 Pin 类型,然后添加到 MiniTokio 实例的任务队列中。这个任务队列通常用于存储待执行的异步任务,后续可以通过某种调度机制依次执行这些任务。

rust 复制代码
fn spawn<F>(&mut self, future: F)

fn spawn:定义了一个泛型方法 spawn,其中 表示这是一个泛型类型参数,F 可以是任何满足特定约束条件的类型

(&mut self, future: F):方法接收两个参数,&mut self 表示对 MiniTokio 实例的可变引用,这意味着该方法可以修改 MiniTokio 实例的内部状态;future: F 表示一个类型为 F 的 Future 对象,该对象将被添加到任务队列中

rust 复制代码
where
    F: Future<Output = ()> + Send + 'static,

F: Future<Output = ()>:要求泛型类型 F 必须实现 Future trait,并且该 Future 完成时产生的值的类型为 ()(空元组)。这意味着传入的 Future 对象最终不会返回有意义的值。

F: Send:要求泛型类型 F 必须实现 Send trait。Send 是一个标记 trait,用于表示该类型的实例可以安全地在不同线程之间转移所有权。这确保了传入的 Future 对象可以在多线程环境下安全地使用。

F: 'static:要求泛型类型 F 必须具有 'static 生命周期。'static 表示该类型的实例不包含任何非 'static 的引用,即其生命周期可以和整个程序的生命周期一样长。这保证了传入的 Future 对象在放入任务队列后,不会因为引用失效而导致问题。

rust 复制代码
self.tasks.push_back(Box::pin(future));

Box::pin(future):使用 Box::pin 函数将传入的 Future 对象 future 包装成一个 Pin<Box> 类型。Box 用于在堆上分配内存,pin 用于固定 Future 对象的内存地址,防止其在异步操作过程中被移动,确保内部引用的有效性。

self.tasks.push_back(...):将包装后的 Future 对象添加到 MiniTokio 实例的 tasks 任务队列的末尾。tasks 通常是一个 Vec 或其他支持 push_back 方法的集合类型。

rust 复制代码
use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll};

// 定义 MiniTokio 结构体
struct MiniTokio {
    tasks: Vec<Pin<Box<dyn Future<Output = ()> + Send>>>,
}

impl MiniTokio {
    // 生成一个 Future 并放入 mini-tokio 实例的任务队列中
    fn spawn<F>(&mut self, future: F)
    where
        F: Future<Output = ()> + Send + 'static,
    {
        self.tasks.push_back(Box::pin(future));
    }
}

// 定义一个简单的 Future 类型
struct SimpleFuture;

impl Future for SimpleFuture {
    type Output = ();

    fn poll(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Self::Output> {
        Poll::Ready(())
    }
}

fn main() {
    let mut mini_tokio = MiniTokio { tasks: vec![] };
    let future = SimpleFuture;
    mini_tokio.spawn(future);
}

spawn 方法允许用户将一个满足特定约束条件的 Future 对象添加到 MiniTokio 实例的任务队列中,为后续的异步任务调度做好准备。通过泛型约束和 Box::pin 的使用,确保了传入的 Future 对象可以安全地在多线程环境下执行,并且其内存地址固定不变。

rust 复制代码
mini_tokio.spawn(async {
        let when = Instant::now() + Duration::from_millis(10);
        let future = Delay { when };

        let out = future.await;
        assert_eq!(out, "done");
    });

为什么上述代码中的异步函数定义就可以作为参数传递给 spawn?

(因为spawn要求参数实现了 Future<Output = ()> + Send + 'static)

async 块会自动实现 Future trait,这使得它能够作为参数传递给 spawn 方法,并且满足 spawn 方法对参数的约束条件

async 块是一种创建异步代码的语法糖,当编写一个 async 块时,编译器会将其转换为一个实现了 Future trait 的类型

这个 Future 类型代表了异步代码的执行过程,并且会在合适的时候产生一个结果

这里的 async 块会被编译成一个实现了 Future trait 的匿名类型,该类型的 Output 类型取决于 async 块中的返回值

async 块没有显式的返回语句,所以它的 Output 类型是 (),满足 spawn 方法要求的 Future<Output = ()>

Send 是一个标记 trait,用于表示一个类型的实例可以安全地在不同线程之间转移所有权

只要 async 块中使用的所有类型都实现了 Send,那么这个 async 块生成的 Future 类型也会实现 Send

Instant、Duration 和 Delay 类型(假设 Delay 类型实现了 Send)都实现了 Send

所以整个 async 块生成的 Future 类型也实现了 Send,满足 spawn 方法的约束条件

满足 'static 约束

'static 生命周期表示一个类型的实例不包含任何非 'static 的引用,即其生命周期可以和整个程序的生命周期一样长

对于 async 块,如果它不捕获任何非 'static 的引用,那么它生成的 Future 类型就会实现 'static

async 块没有捕获任何非 'static 的引用,所以它生成的 Future 类型实现了 'static,满足 spawn 方法的约束条件

相关推荐
茂桑几秒前
MVCC(多版本并发控制)
java·开发语言·数据库
customer0816 分钟前
【开源免费】基于SpringBoot+Vue.JS医疗报销系统(JAVA毕业设计)
java·vue.js·spring boot·后端·开源
B站计算机毕业设计超人19 分钟前
计算机毕业设计SpringBoot+Vue.jst房屋租赁系统(源码+LW文档+PPT+讲解)
vue.js·spring boot·后端·eclipse·intellij-idea·mybatis·课程设计
thinkMoreAndDoMore44 分钟前
深度学习(3)-TensorFlow入门(常数张量和变量)
开发语言·人工智能·python
蓝桉8021 小时前
图片爬取案例
开发语言·数据库·python
逸狼2 小时前
【JavaEE进阶】Spring DI
java·开发语言
m0_748248652 小时前
SpringBoot整合easy-es
spring boot·后端·elasticsearch
m0_748240542 小时前
数据库操作与数据管理——Rust 与 SQLite 的集成
数据库·rust·sqlite
一个热爱生活的普通人2 小时前
golang的切片(Slice)底层实现解析
后端·go
红目香薰2 小时前
Trae——慧码速造——完整项目开发体验
后端