前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >Rust学习笔记Day18 智能指针Cow/MutexGuard

Rust学习笔记Day18 智能指针Cow/MutexGuard

作者头像
用户1072003
发布2023-02-23 16:59:53
发布2023-02-23 16:59:53
75600
代码可运行
举报
文章被收录于专栏:码上读书码上读书
运行总次数:0
代码可运行

昨天我们一起学习了智能指针 Box。今天再学习2个智能指针 Cow和。

Cow<'a, B>

这是用于提供写时克隆(Clone-on-Write)的一个智能指针,和虚拟内存管理的写时复制很像。

代码定义如下:

代码语言:javascript
代码运行次数:0
运行
复制
pub enum Cow<'a, B> where B: 'a + ToOwned + ?Sized {
  Borrowed(&'a B),
  Owned(<B as ToOwned>::Owned),
}

包含一个只读借用,如果调用方需要所有权做修改操作,他就会clone借用的数据。

这里发现2个新的trait:

  1. ToOwned
  2. Borrow

这2个trait的代码定义:

代码语言:javascript
代码运行次数:0
运行
复制
pub trait ToOwned {
    type Owned: Borrow<Self>;
    #[must_use = "cloning is often expensive and is not expected to have side effects"]
    fn to_owned(&self) -> Self::Owned;

    fn clone_into(&self, target: &mut Self::Owned) { ... }
}

pub trait Borrow<Borrowed> where Borrowed: ?Sized {
    fn borrow(&self) -> &Borrowed;
}

可以看到Owned是关联类型,需要使用者定义,和之前接触的关联类型不同的是,这里Owned不能是任意类型,需要满足Borrow<T> trait。

看一下str 对 ToOwned trait的实现:

代码语言:javascript
代码运行次数:0
运行
复制
impl ToOwned for str {
    type Owned = String;
    #[inline]
    fn to_owned(&self) -> String {
        unsafe { String::from_utf8_unchecked(self.as_bytes().to_owned()) }
    }

    fn clone_into(&self, target: &mut String) {
        let mut b = mem::take(target).into_bytes();
        self.as_bytes().clone_into(&mut b);
        *target = unsafe { String::from_utf8_unchecked(b) }
    }
}

这里Owned被定义为String,而String必须定义Borrow<T>。ToOwned要求是Borrow<Self>,这里实现ToOwned的主体是str,Borrow<Self>就是Borrow<str>, 也就是说String 要实现 Borrow,看代码也确实实现了。

代码语言:javascript
代码运行次数:0
运行
复制
impl Borrow<str> for String {
    #[inline]
    fn borrow(&self) -> &str {
        &self[..]
    }
}
代码语言:javascript
代码运行次数:0
运行
复制

通过这张图,我们可以更好地搞清楚 Cow 和 ToOwned / Borrow之间的关系。

那么为何 Borrow 要定义成一个泛型 trait 呢?

代码语言:javascript
代码运行次数:0
运行
复制
use std::borrow::Borrow;

fn main() {
    let s = "hello world!".to_owned();

    // 这里必须声明类型,因为 String 有多个 Borrow<T> 实现
    // 借用为 &String
    let r1: &String = s.borrow();
    // 借用为 &str
    let r2: &str = s.borrow();

    println!("r1: {:p}, r2: {:p}", r1, r2);
}

从这个例子可以看到 String可以Borrow出来&String,也可以Borrow出来&str。因为它有多个Borrow<T>的实现。

Cow是智能指针,就必须实现Deref trait:

代码语言:javascript
代码运行次数:0
运行
复制
impl<B: ?Sized + ToOwned> Deref for Cow<'_, B> {
    type Target = B;

    fn deref(&self) -> &B {
        match *self {
            Borrowed(borrowed) => borrowed,
            Owned(ref owned) => owned.borrow(),
        }
    }
}

根据 self 是 Borrowed 还是 Owned,我们分别取其内容,生成引用:

  • 如果是Borrowed,直接就是引用。
  • 如果是Owned,调用它的borrow方法,获得引用。

这样,虽然 Cow 是一个 enum,但是通过 Deref 的实现,我们可以获得统一的体验。这种根据 enum 的不同状态来进行统一分发的方法是第三种分发手段,之前讲过可以使用泛型参数做静态分发和使用 trait object 做动态分发。

使用场景

Cow可以在需要的时候 才进行内存分配和拷贝。如果Cow<'a, B> 中的 Owned 数据类型是一个需要在堆上分配内存的类型,如 String、Vec等,还能减少堆内存分配的次数

我们知道堆内存的分配/释放效率远不及栈内存,减少不必要的堆内存分配是提升软件效率的关键手段,Cow<'a, B>就可以达到这个效果,说体验还非常舒服。(但是我到目前为止,舒服是没有感觉到,别扭到感觉到不少)。

作者为了证明Cow的带来的舒服,还举了一个解析url参数和jsonDecode的例子:说大多数做法是把key->value 都是新的字符串。请求大起来就会有很多堆内存的分配。(话说难道,不应该预先分配内存,做池化处理的吗?)

然后作者说明了一下Cow的过程:

  • 解析出key value后,用&str指向URL中的位置,然后用Cow封装它。
  • 当解析出来的内容不能直接使用,需要加decode时,可以用Cow封装。(不能直接使用是指被转义过后的?)

解析url:

代码语言:javascript
代码运行次数:0
运行
复制
use std::borrow::Cow;

use url::Url;
fn main() {
    let url = Url::parse("https://tyr.com/rust?page=1024&sort=desc&extra=hello%20world").unwrap();
    let mut pairs = url.query_pairs();

    assert_eq!(pairs.count(), 3);

    let (mut k, v) = pairs.next().unwrap();
    // 因为 k, v 都是 Cow<str> 他们用起来感觉和 &str 或者 String 一样
    // 此刻,他们都是 Borrowed
    println!("key: {}, v: {}", k, v);
    // 当修改发生时,k 变成 Owned
    k.to_mut().push_str("_lala");

    print_pairs((k, v));

    print_pairs(pairs.next().unwrap());
    // 在处理 extra=hello%20world 时,value 被处理成 "hello world"
    // 所以这里 value 是 Owned
    print_pairs(pairs.next().unwrap());
}

fn print_pairs(pair: (Cow<str>, Cow<str>)) {
    println!("key: {}, value: {}", show_cow(pair.0), show_cow(pair.1));
}

fn show_cow(cow: Cow<str>) -> String {
    match cow {
        Cow::Borrowed(v) => format!("Borrowed {}", v),
        Cow::Owned(v) => format!("Owned {}", v),
    }
}

解析json的例子:

代码语言:javascript
代码运行次数:0
运行
复制
use serde::Deserialize;
use std::borrow::Cow;

#[derive(Debug, Deserialize)]
struct User<'input> {
    #[serde(borrow)]
    name: Cow<'input, str>,
    age: u8,
}

fn main() {
    let input = r#"{ "name": "Tyr", "age": 18 }"#;
    let user: User = serde_json::from_str(input).unwrap();

    match user.name {
        Cow::Borrowed(x) => println!("borrowed {}", x),
        Cow::Owned(x) => println!("owned {}", x),
    }
}

MutexGuard<T>

介绍

MutexGuard<T> 是另外一类很有意思的智能指针:它不但通过 Deref 提供良好的用户体验,还通过 Drop trait 来确保,使用到的内存以外的资源在退出时进行释放

MutexGuard这个结构是在调用 Mutex::lock 时生成的:

代码语言:javascript
代码运行次数:0
运行
复制
pub fn lock(&self) -> LockResult<MutexGuard<'_, T>> {
   unsafe {
       self.inner.raw_lock();
       MutexGuard::new(self)
   }
}
  1. 先拿锁,拿不到就等。
  2. 拿到了,就把Mutex的引用给MutexGuard。
定义及对Deref和Drop的实现。
代码语言:javascript
代码运行次数:0
运行
复制
// 这里用 must_use,当你得到了却不使用 MutexGuard 时会报警
#[must_use = "if unused the Mutex will immediately unlock"]
pub struct MutexGuard<'a, T: ?Sized + 'a> {
    lock: &'a Mutex<T>,
    poison: poison::Guard,
}

impl<T: ?Sized> Deref for MutexGuard<'_, T> {
    type Target = T;

    fn deref(&self) -> &T {
        unsafe { &*self.lock.data.get() }
    }
}

impl<T: ?Sized> DerefMut for MutexGuard<'_, T> {
    fn deref_mut(&mut self) -> &mut T {
        unsafe { &mut *self.lock.data.get() }
    }
}

impl<T: ?Sized> Drop for MutexGuard<'_, T> {
    #[inline]
    fn drop(&mut self) {
        unsafe {
            self.lock.poison.done(&self.poison);
            self.lock.inner.raw_unlock();
        }
    }
}

MutexGuard在结束时,会做unlock操作,Rust 有所有权机制,可以确保只要 MutexGuard 离开作用域,锁就会被释放。(好像在是Golang里 自动加了 defer unlock的功能。这样是不是还能避免多级锁 不小心,带来的死锁问题?)

使用场景

例子:

代码语言:javascript
代码运行次数:0
运行
复制
use lazy_static::lazy_static;
use std::borrow::Cow;
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use std::thread;
use std::time::Duration;

// lazy_static 宏可以生成复杂的 static 对象
lazy_static! {
    // 一般情况下 Mutex 和 Arc 一起在多线程环境下提供对共享内存的使用
    // 如果你把 Mutex 声明成 static,其生命周期是静态的,不需要 Arc
    static ref METRICS: Mutex<HashMap<Cow<'static, str>, usize>> =
        Mutex::new(HashMap::new());
}

fn main() {
    // 用 Arc 来提供并发环境下的共享所有权(使用引用计数)
    let metrics: Arc<Mutex<HashMap<Cow<'static, str>, usize>>> =
        Arc::new(Mutex::new(HashMap::new()));
    for _ in 0..32 {
        let m = metrics.clone();
        thread::spawn(move || {
            let mut g = m.lock().unwrap();
            // 此时只有拿到 MutexGuard 的线程可以访问 HashMap
            let data = &mut *g;
            // Cow 实现了很多数据结构的 From trait,
            // 所以我们可以用 "hello".into() 生成 Cow
            let entry = data.entry("hello".into()).or_insert(0);
            *entry += 1;
            // MutexGuard 被 Drop,锁被释放
        });
    }

    thread::sleep(Duration::from_millis(100));

    println!("metrics: {:?}", metrics.lock().unwrap());
}

MutexGuard 不允许 Send,只允许 Sync,也就是说,你可以把 MutexGuard 的引用传给另一个线程使用,但你无法把 MutexGuard 整个移动到另一个线程。

类似 MutexGuard 的智能指针有很多用途。比如要创建一个连接池,你可以在 Drop trait 中,回收 checkout 出来的连接,将其再放回连接池。(开源项目: r2d2)

小结

这2天我们介绍了三个重要的智能指针,它们有各自独特的实现方式和使用场景。

  • Box可以在堆上创建内存,是很多其他数据结构的基础。
  • Cow 实现了 Clone-on-write 的数据结构,让你可以在需要的时候再获得数据的所有权。Cow 结构是一种使用 enum 根据当前的状态进行分发的经典方案。甚至,你可以用类似的方案取代 trait object 做动态分发,其效率是动态分发的数十倍。
  • MutexGuard 把从 Mutex 中获得的锁包装起来,实现只要 MutexGuard 退出作用域,锁就一定会释放。如果你要做资源池,可以使用类似 MutexGuard 的方式。

明天我们继续学习集合容器。

如果你觉得有点收获,欢迎点个关注, 也欢迎分享给你身边的朋友。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2023-02-02,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 码上读书 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • Cow<'a, B>
  • 使用场景
  • MutexGuard<T>
    • 介绍
    • 定义及对Deref和Drop的实现。
    • 使用场景
  • 小结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档