• 2311rust无畏并发.


    原文

    Rust无畏并发

    Rust是为了解决两个麻烦问题:
    1,如何安全系统编程
    2,如何无畏并发

    最初,这些问题似乎是无关的,但令惊讶的是,方法竟然是相同的:使Rust安全的相同工具也可帮助解决并发问题.

    内存安全和并发错误,一般认为是代码在不应访问数据时访问数据.Rust依靠所有权为你静态检查.

    内存安全,即可在无垃集时编程,也不必担心段错误,因为Rust会发现你的错误.

    并发性,即可从(传递消息,共享状态,无锁,纯函数式)中选择,而Rust帮助你避免常见的陷阱.

    以下是Rust中的并发性:

    1,通道转移了发送消息的所有权,因此可从一个线程发送指针到另一个线程,而不必担心线程竞争.Rust通道强制隔离线程.这里

    2,知道它保护了哪些数据,且Rust保证,只有在持有锁时,才能访问数据.而不会共享状态.在Rust中强制"锁定数据,而不是代码".

    3,在多线程之间,每种数据类型都知道它是否可安全发送或访问,且Rust强制,即使对无锁数据结构,也无数据竞争.线安不仅是文档;也是规则.

    4,甚至可在线程共享栈帧这里,Rust静态地确保,在其他线程使用它们时,这些帧活跃.即使是最大胆的共享形式,在Rust中也能保证安全.

    这些好处都来自Rust所有权模型,事实上,锁,通道,无锁数据结构等都是在库中而不是核心语言中定义的.
    Rust并发方法是开放的:新库可带有新的范式并抓新的错误,只需添加使用Rust所有权功能的API.

    背景:所有权

    Rust中,每个值都有个"物主域",传递或返回值表明从旧所有权转移("移动")到新域.在结束时,此时自动析构仍拥有的值.
    看看简单示例.假设创建一个向量并推送一些元素到它上面:

    fn make_vec() {
        let mut vec = Vec::new();
     //归`make_vec`的域所有
        vec.push(0);
        vec.push(1);
        //域结束,析构`"vec"`
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    创建值的域最初也拥有它.此时,make_vec的主体是vec的物主域.物主可用vec干活.
    在域结束时,仍归域所有vec,因此会自动释放.
    如果返回或传递向量,会更有趣:

    fn make_vec() -> Vec<i32> {
        let mut vec = Vec::new();
        vec.push(0);
        vec.push(1);
        vec //转让`所有权`给调用者
    }
    fn print_vec(vec: Vec<i32>) {
        //`"vec"`参数是此域的一部分,因此归`"print_vec"`所有
        for i in vec.iter() {
            println!("{}", i)
        }
        //现在,释放`"vec"`
    }
    fn use_vec() {
        let vec = make_vec(); //取向量所有权,
        print_vec(vec);       //传递所有权给`"print_vec"`
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    现在,在make_vec域结束前,vec返回它来出域;不会析构它.然后,像use_vec此调用者获得向量所有权.
    另一方面,print_vec函数带vec参数,由其调用者把向量的所有权转移给它.因为print_vec不会进一步转移所有权,因此在其域结束时,就析构向量.
    一旦放弃所有权,就不能再使用该值.如,请考虑以下use_vec变体:

    fn use_vec() {
        let vec = make_vec();  //取`VectorPass`所有权
        print_vec(vec);        //传递所有权给`"print_vec"`,
        for i in vec.iter() {  //继续使用`"vec"`
            println!("{}", i * 2)
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    编译器说不再可用vec;已转移所有权.这非常好,因此时已释放了向量!避免了灾难.

    借贷

    目前,并不满意,因为无意print_vec析构向量.真正想要的是临时授予print_vec访问向量,然后继续使用向量.

    这就要靠借贷了.如果有权访问Rust中的某个值,可把该权限借给调用函数.Rust检查这些生命期不会超过被借对象.
    借用一个值,可用&符号引用它(一个指针):

    fn print_vec(vec: &Vec<i32>) {
        //`"vec"`参数是`此域`借用的
        for i in vec.iter() {
            println!("{}", i)
        }
        //现在,借期结束了
    }
    fn use_vec() {
        let vec = make_vec();  //取向量的所有权
        print_vec(&vec);       //借出`"print_vec"`权限
        for i in vec.iter() {  //继续使用`"vec"`
            println!("{}", i * 2)
        }
        //在此析构`VEC`
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    现在print_vec接受向量引用,use_vec通过编写&vec来借出向量.因为是临时的,use_vec保留了向量所有权;

    可在调用print_vec返回后继续使用它.

    每个引用有限域内有效,编译器自动确定该域.有两种引用形式:

    1,不变引用&T,允许共享禁止改变.可同时有多个对同一值的&T引用,但当这些引用活动时,不能更改该值.
    2,可变引用&mut T,允许改变不共享.如果存在对某个值的&mut T引用,则此时不能有其他活动引用,但可更改该值.

    Rust在编译时检查这些规则;借用没有运行时成本.
    为什么有两类引用?考虑此函数:

    fn push_all(from: &Vec<i32>, to: &mut Vec<i32>) {
        for i in from.iter() {
            to.push(*i);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    此函数遍历向量的每个元素,把它推送另一个向量上.迭代器在当前和最终位置保持向量指针,挨个前进.
    如果用相同向量,为两个参数调用此函数怎么办?

    push_all(&vec, &mut vec)
    
    • 1

    这将是一场灾难!推送元素到向量上时,它偶尔要调整,分配大量新内存并复制进元素.迭代器会剩下旧内存指针的悬挂指针,导致内存不安全(段错误则更糟).

    幸好,Rust确保每当可变借用活动时,其他借用都不会活动,从而产生以下消息:
    错误:不能按可变借用"vec",因为它也按不变借用.

    push_all(&vec, &mut vec);
                        ^~~
    
    • 1
    • 2

    传递消息

    并发编程有多种风格,特别简单方式是线程或参与者相互发送消息来通信的传递消息.
    不通过共享内存交流;相反,通过交流共享内存.

    Rust所有权使得很容易检查规则.考虑以下通道API(Rust标准库中的通道略有不同):

    fn send<T: Send>(chan: &Channel<T>, t: T);
    fn recv<T: Send>(chan: &Channel<T>) -> T;
    
    • 1
    • 2

    通道在它们传输的数据类型(API部分)上是通用的.Send部分表明T必须是安全的,可在线程之间发送;
    VecSend.

    Rust中一样,传递Tsend函数表明转移它的所有权.这一事实深远影响:即,下面代码生成编译器错误.

    //假设`chan:Channel>`
    let mut vec = Vec::new();
    //做一些计算
    send(&chan, vec);
    print_vec(&vec);
    
    • 1
    • 2
    • 3
    • 4
    • 5

    在此,线程创建了一个向量,并发送它到另一个线程,然后继续使用它.当该线程继续运行时,接收向量线程可能会更改它,因此调用print_vec,可能会导致竞争,因此,导致释放后使用错误.

    相反,在调用print_vec时,Rust编译器会生成错误消息:
    错误:使用移动"vec"值.
    避免了灾难.

    锁,被动共享状态来通信的方式.
    共享状态并发有个缺点.很容易忘记取锁,或在错误时间改变错误数据,导致灾难.

    Rust的观点是:
    然而,共享状态并发是基本编程风格,系统代码,最大性能及实现其他并发风格都需要它.
    问题与意外共享状态有关.

    无论使用有锁还是无锁技术,Rust旨在为你提供直接征服共享状态并发的工具.

    Rust中,因为所有权,线程会自动相互"隔离".无论是拥有数据,还是可变借用数据,仅当线程可变权限时,才会写入.

    总之,保证该线程是当时唯一有权限的线程.
    请记住,不能同时有可变借用与其他借用.锁通过运行时同步提供相同的保证("互斥").这导致直接勾挂到Rust所有权系统的锁API.
    如下是简化版本:

    //创建新的互斥锁
    fn mutex<T: Send>(t: T) -> Mutex<T>;
    //取锁
    fn lock<T: Send>(mutex: &Mutex<T>) -> MutexGuard<T>;
    //访问受锁保护的数据
    fn access<T: Send>(guard: &mut MutexGuard<T>) -> &mut T;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    此锁API的不寻常点.
    1,首先,在锁保护数据T类型上,互斥类型通用的.创建互斥锁时,转移该数据所有权到互斥锁中,立即放弃了所有权.(在首次创建时解锁).
    2,稍后,你可锁(lock)阻止线程,直到获得.在析构MutexGuard时自动释放锁;没有单独的解锁(unlock)函数.
    3,只能通过访问(access)函数访问锁,该函数把守卫可变借用转换为数据的可变借用(短期借用):

    fn use_lock(mutex: &Mutex<Vec<i32>>) {
        //获得锁,拥有警卫;在域的其余部分持有锁
        let mut guard = lock(mutex);
        //通过可变借用`Guard`来访问数据
        let vec = access(&mut guard);
        //`vec`的类型为`"&mut Vec"`
        vec.push(3);
        //析构`"守卫"`时,会自动在此处释放锁
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    两个关键要素:
    1,访问(access)返回的可变引用不能超过比它借用的MutexGuard.
    2,仅当析构MutexGuard时,才会释放锁.

    结果是Rust强制保证锁规则:除非持有锁,否则禁止访问受锁保护数据.否则生成编译器错误.如,考虑以下有缺陷的"重构":

    fn use_lock(mutex: &Mutex<Vec<i32>>) {
        let vec = {
            //取锁
            let mut guard = lock(mutex);
            //试返回借用数据
            access(&mut guard)
            //在此析构`守卫`,释放了锁
        };
        //试访问锁外数据.
        vec.push(3);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    Rust生成错误来说明问题:
    错误:"guard"的生命期不够长

    access(&mut guard)
                ^~~~~
    
    • 1
    • 2

    避免了灾难.

    线安和"发送"

    一般区分某些数据类型为"线安",而其他数据类型则不是.线安数据结构内部有足够同步,以便可同时安全地使用多线程.
    如,Rust附带了两个来引用计数的"灵针":
    1,Rc通过正常读/写提供引用计数.它不是线安的.
    2,Arc通过原子操作提供引用计数.它是线安的.

    Arc使用的硬件原子操作比Rc使用的普通操作更贵,因此使用Rc而不是Arc是有利的.另一方面,重点,永远不要从一个线程迁移Rc到另一个线程,因为会导致破坏引用计数的竞争.

    Rust中,世界分为两个数据类型:一个是Send,即可安全地从一个线程移动到另一个线程,其余是!Send(不安全).

    如果某个类型的所有组件都是Send,则该类型也是Send,它涵盖了大多数类型.但是,某些基本类型不是线安的,因此也可按Send显式标记Arc等类型,对编译器说:相信我;已在此验证了必要的同步.

    当然,ArcSend,而Rc不是.

    可见,通道和互斥API仅适合发送(Send)数据.因为它们是跨越线程边界的数据点,因此它们也是Send强制点.

    综上,Rust可自信地获得Rc和其他线程不安全类型的好处,因为,如果不小心试发送一个线程到另一个线程,Rust编译器会说:
    无法安全地在线程之间发送"Rc>".
    这避免了灾难.

    共享栈:"scoped"

    注意:这里提到的API是一个旧的API,已从标准库中移出.你可在横梁(scope()文档)和scoped_threadpool(scoped()文档)中找到等效的函数.

    目前,所有模式都涉及在上创建,在线程共享的数据结构.但是,如果想启动一些线程来利用栈帧中的数据,则可能会很危险:

    fn parent() {
        let mut vec = Vec::new();
        //填充向量
        thread::spawn(|| {
            print_vec(&vec)
        })
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    子线程接受vec引用,而vec保留在父线程的栈帧中.父线程退出时,会弹出栈帧,但子线程并不知道.哎呀!

    为了排除该内存不安全,Rust的基本线程生成API如下:

    fn spawn<F>(f: F) where F: 'static, ...
    
    • 1

    "静态约束"即,指在闭包禁止借用数据.即像上面此parent函数会生成错误:
    错误:"vec"的生命期不够长.

    基本上抓住了弹出父栈帧的可能性.避免了灾难.

    还有另一个方法可保证安全性:直到子线程完成,确保父栈帧保持原位.这是分叉连接编程的模式,一般用于分而治之的并行算法.
    Rust通过提供线程生成的"域"变体来支持它:

    fn scoped<'a, F>(f: F) -> JoinGuard<'a> where F: 'a, ...
    
    • 1

    与上面的spawn接口有两个主要区别:
    1,使用'a参数,而不是'static.
    2,JoinGuard返回值.即,JoinGuard通过在其析构器隐式连接(如果尚未显式)来确保父线程加入(等待)其子线程.

    JoinGuard中包含'a可确保JoinGuard无法逃脱闭包借用的数据的域.即,Rust保证在弹出子线程可能访问的栈帧前,父线程等待子线程完成.

    因此,调整之前示例,可如下修复错误并满足编译器:

    fn parent() {
        let mut vec = Vec::new();
        //填充向量
        let guard = thread::scoped(|| {
            print_vec(&vec)
        });
        //在此析构`守卫`,隐式合并
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    因此,在Rust中,可自由地把栈数据借用到子线程中,编译器确保检查是否有足够同步.

    数据竞争

    Rust使用所有权和借用来保证:
    1,内存安全,无垃集.
    2,无并发数据竞争.

  • 相关阅读:
    OpenRemote: Java 开源 IoT 物联网开发平台,匹配智慧城市、智能家居、能源管理
    springboot源码理解八、run方法执行过程(刷新应用上下文)
    D基础_VOC2007 解析
    【题解 && 线段树】[蓝桥杯 2022 省 A] 选数异或
    MyBatis大数据量插入方案
    excel转换成pdf格式怎么操作?这3招教你Excel怎么转PDF
    单代号搭接网络计划:时间参数的计算
    redis的性能管理
    Ubuntu 手动安装 gdal 指定版本
    Windows云服务器 PHP搭建网站外网无法访问的问题
  • 原文地址:https://blog.csdn.net/fqbqrr/article/details/134293963