zoukankan      html  css  js  c++  java
  • Rust-线程:共享状态并发

    虽然消息传递是一个很好的处理并发的方式,但并不是唯一一个。再一次思考一下Go编程语言文档中口号的这一部分:“不要通过共享内存来通讯”

    通过共享内存通讯看起来如何?除此之外,为何消息传递的拥护者并不使用它并反其道而行之呢?

    在某种程度上,任何编程语言中的通道都类似于单所有权的,因为一旦将一个值传送到通道中,将无法再使用这个值。共享内存类似于多所有权:多个线程可以同时访问相同的内存位置。Rust的类型系统和所有权规则极大的协助了正确地管理这些所有权。让我们看看互斥器,一个更为常见的共享内存并发原语。

    互斥器一次只允许一个线程访问数据

    互斥器(mutex)是mutual exclusion的缩写,也就是说,任意时刻,其只允许一个线程访问某些数据。为了访问互斥器中的数据,线程首先需要通过获取互斥器的(lock)来表明其希望访问数据。锁是一个作为互斥器一部分的数据结构,它记录谁有数据的排他访问权。因此,我们描述互斥器为通过锁系统保护(guarding)其数据。

    互斥器以难以使用著称,因为你不得不记住:

    1. 在使用数据之前尝试获取锁。
    2. 处理完被互斥器所保护的数据之后,必须解锁数据,这样其它线程才能够获取锁。

    正确的管理互斥器异常复杂,这也是许多人之所以热衷于通道的原因。然而,在Rust中,得益于类型系统的所有权,我们不会在锁和解锁上出错。

    Mutex<T>的API

    作为展示如何使用互斥器的例子,让我们从在单线程上下文使用互斥器开始,如示例1所示:

        let m = Mutex::new(5);
        {
            let mut num = m.lock().unwrap();
            *num = 6;
        }
        println!("num = {:?}", m);

    示例1:在一个单线程上下文探索Mutex<T>的api

    像很多类型一样,我们使用关联函数 new 来创建一个Mutex<T>。使用lock方法获取锁,以访问互斥器中的数据。这个调用会阻塞当前线程,直到我们拥有锁为止。

    如果另一个线程拥有锁,并且那个线程panic了,则lock调用会失败。在这种情况下,没人能够再获取锁,所以这里选择unwrap并在遇到这种情况时使线程panic。

    一旦获取了锁,就可以将返回值(在这里是num)视为一个其内部数据的可变引用了。类型系统确保了我们在使用 m 中的值之前获取锁:Mutex<i32>并不是一个i32,所以必须获取锁才能使用这个i32值。我们是不会忘记这么做的,因为反之类型系统不允许访问内部的i32值。

    正如你所怀疑的,Mutex<T> 是一个智能指针。更准确的说,lock 调用返回一个叫做 MutexGuard 的智能指针。这个智能指针实现了 Deref 来指向其内部数据;其也提供了一个 Drop 实现当 MutexGuard 离开作用域自动释放锁,这正发生于示例1内部作用域的结尾。为此,我们不会冒忘记释放锁并阻塞互斥器为其它线程所用的风险,因为锁的释放是自动发生的。

    丢弃了锁之后,可以打印出互斥器的值,并发现能够将其内部的i32改为6。

    在线程间共享Mutex<T>

    现在让我们尝试使用Mutex<T>在多个线程间共享值。我们将启动10个线程,并在各个线程中对同一个计数器值加1,这样计数器将从0变为10。示例2中的例子会出现编译错误,而我们将通过这些错误来学习如何使用Mutex<T>,以及Rust又是如何帮助我们正确使用的。

      let counter = Mutex::new(0);
        let mut handles = vec![];
    
        for _ in 0..10 {
            let handle = thread::spawn(move || {
                let mut num = counter.lock().unwrap();
                *num += 1;
            });
            handles.push(handle);
        }
    
        for handle in handles {
            handle.join().unwrap()
        }
    
        println!("Result: {}", *counter.lock().unwrap());

    示例2启动了10个线程,每个线程都通过Mutex<T>来增加计数器的值

    这里创建了一个counter变量来存放内含i32的Mutex<T>,类型示例1那样。接下来遍历range创建了10个线程。使用了 thread::spawn 并对所有线程使用了相同的闭包:他们每一个都将调用lock方法来获取Mutex<T>上的锁,接着将互斥器中的值加一。当一个线程结束执行,num会离开闭包作用域并释放锁,这样另一个线程就可以获取它了。

    在主线程中,我们收集了join句柄,调用它们的join方法来确保所有线程都会结束。这时,主线程会获取锁并打印出程序的结果。

    但这个例子不能编译:

    error[E0382]: use of moved value: `counter`
      --> src/main.rs:64:36
       |
    60 |     let counter = Mutex::new(0);
       |         ------- move occurs because `counter` has type `Mutex<i32>`, which does not implement the `Copy` trait
    ...
    64 |         let handle = thread::spawn(move || {
       |                                    ^^^^^^^ value moved into closure here, in previous iteration of loop
    65 |             let mut num = counter.lock().unwrap();
       |                           ------- use occurs due to use in closure

    错误信息表明counter值在上一次循环中被移动。所以Rust告诉我们不能将counter锁的所有权移动到多个线程中。让我们通过多所有权手段来修复这个编译错误。

    多线程和多所有权

    在这里,我们了解到使用智能指针 Rc<T> 来创建引用计数的值,以便拥有多所有者。让我们看看在这也这么做看看会发生什么。在示例3中的Mutex<T>封装进Rc<T>中并在将所有权移入线程这前克隆了 Rc<T>。现在我们理解了所发生的错误,同时也将代码改回使用for循环,并保留闭包的move关键字:

        let counter = Rc::new(Mutex::new(0));
        let mut handles = vec![];
    
        for _ in 0..10 {
            let counter = Rc::clone(&counter);
            let handle = thread::spawn(move || {
                let mut num = counter.lock().unwrap();
                *num += 1;
            });
            handles.push(handle);
        }
    
        for handle in handles {
            handle.join().unwrap()
        }
    
        println!("Result: {}", *counter.lock().unwrap());

    示例3:尝试使用Rc<T>来允许多个线程拥有Mutex<T>

    再一次编译,出现了不同的错误。。。编译器教会了我们很多。

    error[E0277]: `Rc<Mutex<i32>>` cannot be sent between threads safely
       --> src/main.rs:65:22
        |
    65  |           let handle = thread::spawn(move || {
        |  ______________________^^^^^^^^^^^^^_-
        | |                      |
        | |                      `Rc<Mutex<i32>>` cannot be sent between threads safely
    66  | |             let mut num = counter.lock().unwrap();
    67  | |             *num += 1;
    68  | |         });
        | |_________- within this `[closure@src/main.rs:65:36: 68:10]`
        | 
       ::: /Users/johnny/.rustup/toolchains/stable-x86_64-apple-darwin/lib/rustlib/src/rust/library/std/src/thread/mod.rs:617:8
        |
    617 |       F: Send + 'static,
        |          ---- required by this bound in `spawn`
        |
        = help: within `[closure@src/main.rs:65:36: 68:10]`, the trait `Send` is not implemented for `Rc<Mutex<i32>>`
        = note: required because it appears within the type `[closure@src/main.rs:65:36: 68:10]`

    错误信息有点长!这里是一些需要注意的重要部分:第一行错误表明 '`Rc<Mutex<i32>>` cannot be sent between threads safely'。

     不幸的是,Rc<T>并不能安全的在线程间共享。当Rc<T>管理引用计数时,它必须在每个clone调用时增加计数,并在每一个克隆被丢弃时减少计数。Rc<T>并没有使用任何并发原语,来确保改变计数的操作不会被其他线程打断。在计数出错时可能会导致诡异的bug,比如可能会造成内存泄漏,或在使用结束之前就丢弃一个值。我们所需要的是一个完全类似 Rc<T>,又以一种线程安全的方式改变引用计数的类型。

    原子引用计数 Arc<T>

    所幸 Arc<T> 正是这么一个类似 Rc<T> 并可以安全用于并发环境的类型。字母"a"代表原子性(atmic),所以这是一个原子引用计数(atomically reference counted)类型。原子性是另一类这里还没涉及到的并发原语:我们可以查看标准库中std::sync::atomic的文档来获取更多细节。其中的要点就是:原子性类型工作起来类型原始类型,不过可以安全的在线程间共享。

    我们可能会好奇为什么不是所有的原始类型都是原子性的?为什么不是所有标准库中的类型都默认使用Arc<T>实现?原因在于线程安全带有性能惩罚,我们希望只在必要时才为此买单。如果只是在单线程中对值进行操作,原子性提供的保证并无必要,代码可以因此运行的更快。

    回到上面的例子:Arc<T>和Rc<T>有着相同的api,所以修改程序中的use行和new调用:

        let counter = Arc::new(Mutex::new(0));
        let mut handles = vec![];
    
        for _ in 0..10 {
            let counter = Arc::clone(&counter);
            let handle = thread::spawn(move || {
                let mut num = counter.lock().unwrap();
                *num += 1;
            });
            handles.push(handle);
        }
    
        for handle in handles {
            handle.join().unwrap()
        }
    
        println!("Result: {}", *counter.lock().unwrap());

    示例4,使用Arc<T>包装一个Mutex<T>能够实现在多线程之间共享所有权

    这会打印出:

    Result: 10

    成功了!我们从0数到了10,这可能并不显眼,不过我们学习了很多关于Mutex<T>和线程安全的内容。使用这个策略,可将计算分成独立的部分,分散到多个线程中,接着使用Mutex<T>使用各自的结算结果更新最终的结果。

    RefCell<T> / Rc<T> 与 Mutex<T> / Arc<T>的相似性

    你可能注意到了,因为counter是不可变的,不过可以获取其内部值的可变引用;这意味着Mutex<T>提供了内部可变性,就像Cell系列类型那样。

    正如使用RefCell<T>可以改变Rc<T>中的内容那样,同样的可使用Mutex<T>来改变Arc<T>中的内容。

    Mutex<T>也有造成死锁(deadlock)的风险。这发生于当一个操作需要锁住两个资源而两个线程各持一个锁,这会造成它们永远相互等待。

  • 相关阅读:
    java.lang.OutOfMemoryError: Java heap space解决办法
    android网络图片加载缓存,避免重复加载。
    Android控制软键盘拉起
    mysql create database 指定utf-8编码
    仿黑客帝国文字雨效果
    C++循环去掉文件的后缀名
    使用python读取多重文件夹下的word(doc、docx)文件,并处理存储到excel(xls、xlsx)文件
    Python操作文件夹
    (转载)Python:列表作为参数
    595. Big Countries (Easy)
  • 原文地址:https://www.cnblogs.com/johnnyzhao/p/15374821.html
Copyright © 2011-2022 走看看