共享状态并发

消息传递是处理并发的一种很好的方法,但它并不是唯一的 一。另一种方法是让多个线程访问同一个共享的 数据。考虑一下 Go 语言文档中的这部分口号 “又说:”不要通过共享内存来交流。

通过共享内存进行通信会是什么样子?此外,为什么会 消息传递爱好者注意不要使用内存共享?

在某种程度上,任何编程语言中的频道都类似于单一所有权, 因为一旦你把一个值传输到一个通道上,你就不应该再使用它 价值。共享内存并发类似于多重所有权:多个线程 可以同时访问相同的内存位置。正如你在第 15 章中看到的, 智能指针使多个所有权成为可能,而多个所有权可以 增加复杂性,因为这些不同的所有者需要管理。Rust 的类型系统 所有权规则极大地有助于正确进行这种管理。对于 示例,让我们看看互斥锁,这是更常见的并发基元之一 用于共享内存。

使用互斥锁允许一次从一个线程访问数据

Mutex互斥的缩写,例如,mutex 只允许 一个线程在任何给定时间访问一些数据。要访问 mutex 的 Mutex 中,线程必须首先通过请求获取 mutex 的。锁是一种数据结构,它是互斥锁的一部分,它 跟踪当前谁对数据具有独占访问权限。因此, Mutex 被描述为通过锁定系统保护它保存的数据。

互斥锁以难以使用而闻名,因为您必须 记住两条规则:

  • 在使用数据之前,必须尝试获取锁。
  • 处理完互斥锁保护的数据后,必须解锁 data 的 URL,以便其他线程可以获取锁。

对于互斥锁的真实比喻,想象一下在 只有一个麦克风的会议。在小组成员可以发言之前,他们必须 询问或示意他们想要使用麦克风。当他们获得 microphone 的 Mic,他们可以随心所欲地说话,然后递上 麦克风给下一位请求发言的答疑者。如果答疑者忘记 用完后把麦克风交给别人,其他人都无法 说。如果共享麦克风的管理出错,面板将无法工作 如期而至!

正确管理互斥锁可能非常棘手,这就是原因 许多人对频道充满热情。然而,多亏了 Rust 的类型 system 和 ownership 规则,则不能出错的锁定和解锁。

Mutex<T 的 API >

作为如何使用互斥锁的示例,让我们首先在 单线程上下文,如示例 16-12 所示:

文件名: src/main.rs

use std::sync::Mutex;

fn main() {
    let m = Mutex::new(5);

    {
        let mut num = m.lock().unwrap();
        *num = 6;
    }

    println!("m = {m:?}");
}

示例 16-12:><在 单线程上下文,简单易行

与许多类型一样,我们使用关联的函数 . 要访问互斥锁内的数据,我们使用该方法获取 锁。此调用将阻止当前线程,因此它无法执行任何工作,直到 轮到我们上锁了。Mutex<T>newlock

如果另一个持有该锁的线程出现紧急情况,则 的调用将失败。在 那样的话,没有人能够获得锁,所以我们选择在那种情况下让这个线程 panic。lockunwrap

获取锁后,我们可以处理 在这种情况下,作为对内部数据的可变引用。类型系统确保 ,我们在使用 中的值之前获取一个锁。的类型是 ,而不是 ,因此我们必须调用才能使用该值。我们不能忘记;否则,类型系统不允许我们访问 inner。nummmMutex<i32>i32locki32i32

正如您可能怀疑的那样,是一个智能指针。更准确地说,调用 to 返回一个名为 的智能指针 ,该指针包装在我们通过调用 .智能 pointer 实现指向我们的内部数据;智能指针还 具有一个 implementation ,当 a 超出范围时自动释放锁,这发生在内部范围的末尾。如 因此,我们不会冒着忘记释放锁和阻止互斥锁的风险 以免被其他线程使用,因为会发生锁释放 自然而然。Mutex<T>lockMutexGuardLockResultunwrapMutexGuardDerefDropMutexGuard

放下锁后,我们可以打印互斥锁值,并查看我们是否能够 将 inner 更改为 6。i32

在多个线程之间共享 mutex<T>

现在,让我们尝试使用 在多个线程之间共享一个值。 我们将启动 10 个线程,并让每个线程将 counter 值增加 1,因此 计数器从 0 到 10。示例 16-13 中的下一个例子将具有 编译器错误,我们将使用该错误来了解有关使用以及 Rust 如何帮助我们正确使用它的更多信息。Mutex<T>Mutex<T>

文件名: src/main.rs

use std::sync::Mutex;
use std::thread;

fn main() {
    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());
}

示例 16-13:10 个线程每个线程增加一个计数器 由 Mutex<T 守卫>

我们创建一个变量来将 an 保存在 中,就像我们所做的那样 在示例 16-12 中。接下来,我们通过迭代一系列 数字。我们使用并给所有线程相同的闭包:一个 将计数器移动到线程中,获取 BY 上的 调用该方法,然后将 1 添加到互斥锁中的值。当 thread 完成运行其闭包,将超出范围并释放 lock 以便另一个线程可以获取它。counteri32Mutex<T>thread::spawnMutex<T>locknum

在主线程中,我们收集所有联接句柄。然后,就像我们在 清单 中所做的那样 16-2 时,我们调用每个句柄以确保所有线程都完成。在 此时,主线程将获取锁并打印此 程序。join

我们暗示此示例不会编译。现在让我们找出原因!

$ cargo run
   Compiling shared-state v0.1.0 (file:///projects/shared-state)
error[E0382]: borrow of moved value: `counter`
  --> src/main.rs:21:29
   |
5  |     let counter = Mutex::new(0);
   |         ------- move occurs because `counter` has type `Mutex<i32>`, which does not implement the `Copy` trait
...
8  |     for _ in 0..10 {
   |     -------------- inside of this loop
9  |         let handle = thread::spawn(move || {
   |                                    ------- value moved into closure here, in previous iteration of loop
...
21 |     println!("Result: {}", *counter.lock().unwrap());
   |                             ^^^^^^^ value borrowed here after move
   |
help: consider moving the expression out of the loop so it is only moved once
   |
8  ~     let mut value = counter.lock();
9  ~     for _ in 0..10 {
10 |         let handle = thread::spawn(move || {
11 ~             let mut num = value.unwrap();
   |

For more information about this error, try `rustc --explain E0382`.
error: could not compile `shared-state` (bin "shared-state") due to 1 previous error

错误消息指出该值在之前的 循环的迭代。Rust 告诉我们,我们不能移动所有权 的 of 转换为多个线程。让我们用 多重所有权方法,我们在第 15 章中讨论。countercounter

具有多个线程的多个所有权

在第 15 章中,我们通过使用智能指针创建一个引用计数值,为值提供了多个所有者。让我们在这里做同样的事情,看看 会发生什么。我们将在示例 16-14 中包装并克隆 在将所有权移动到线程之前。Rc<T>Mutex<T>Rc<T>Rc<T>

文件名: src/main.rs

use std::rc::Rc;
use std::sync::Mutex;
use std::thread;

fn main() {
    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());
}

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

再一次,我们编译并获取...不同的错误!编译器正在教我们 好多。

$ cargo run
   Compiling shared-state v0.1.0 (file:///projects/shared-state)
error[E0277]: `Rc<Mutex<i32>>` cannot be sent between threads safely
  --> src/main.rs:11:36
   |
11 |           let handle = thread::spawn(move || {
   |                        ------------- ^------
   |                        |             |
   |  ______________________|_____________within this `{closure@src/main.rs:11:36: 11:43}`
   | |                      |
   | |                      required by a bound introduced by this call
12 | |             let mut num = counter.lock().unwrap();
13 | |
14 | |             *num += 1;
15 | |         });
   | |_________^ `Rc<Mutex<i32>>` cannot be sent between threads safely
   |
   = help: within `{closure@src/main.rs:11:36: 11:43}`, the trait `Send` is not implemented for `Rc<Mutex<i32>>`, which is required by `{closure@src/main.rs:11:36: 11:43}: Send`
note: required because it's used within this closure
  --> src/main.rs:11:36
   |
11 |         let handle = thread::spawn(move || {
   |                                    ^^^^^^^
note: required by a bound in `spawn`
  --> /rustc/eeb90cda1969383f56a2637cbd3037bdf598841c/library/std/src/thread/mod.rs:688:1

For more information about this error, try `rustc --explain E0277`.
error: could not compile `shared-state` (bin "shared-state") due to 1 previous error

哇,那个错误信息太冗长了!以下是需要关注的重要部分: .编译器是 还告诉我们原因:.我们将在下一节中讨论:它是 确保我们与线程一起使用的类型用于 并发情况。`Rc<Mutex<i32>>` cannot be sent between threads safelythe trait `Send` is not implemented for `Rc<Mutex<i32>>` Send

不幸的是,跨线程共享不安全。当管理引用计数时,它会添加到对 和 的每次调用的计数中 在删除每个克隆时从计数中减去。但它没有使用任何 并发基元来确保对 count 的更改不能 被另一个线程打断。这可能会导致错误的计数 — 细微的错误 反过来可能会导致内存泄漏或在完成之前删除值 与它。我们需要的是一个与 type 完全相同的类型,但 type 会进行更改 以线程安全的方式添加到引用计数中。Rc<T>Rc<T>cloneRc<T>

使用 Arc<T 进行原子引用计数>

幸运的是,像这样的类型可以安全地在 并发情况。a 代表 atomic,意味着它在 atom 引用计数类型。原子是另一种并发 我们在这里不详细介绍的 Primitive:参见 Standard 库 std::sync::atomic 文档了解更多信息 详。在这一点上,你只需要知道 atomics 的工作方式类似于 primitive 类型,但可以安全地跨线程共享。Arc<T>Rc<T>

然后,您可能想知道为什么所有原始类型都不是原子的,为什么是标准的 默认情况下,库类型未实现为使用。原因是 线程安全会带来性能损失,您只想在以下情况下支付 你真的需要。如果您只是对 单线程,则您的代码可以运行得更快,如果它不必强制执行 Atomics 提供的保证。Arc<T>

让我们回到我们的例子:并使用相同的 API,因此我们修复了 我们的程序,方法是将 line 、 call 更改为 ,并将 call 更改为 。示例 16-15 中的代码最终将编译并运行:Arc<T>Rc<T>usenewclone

文件名: src/main.rs

use std::sync::{Arc, Mutex};
use std::thread;

fn main() {
    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());
}

示例 16-15:使用 Arc<T> 包装 Mutex<T>以便能够在多个线程之间共享所有权

此代码将打印以下内容:

Result: 10

我们成功了!我们从 0 数到 10,这可能看起来不是很令人印象深刻,但它 确实教会了我们很多关于线程安全的知识。您也可以使用这个 程序的结构来执行更复杂的作,而不仅仅是递增 计数器。使用此策略,您可以将计算划分为独立的 parts 中,将这些部分拆分到线程中,然后使用 a 使每个 thread 更新最终结果及其部分。Mutex<T>Mutex<T>

请注意,如果您正在执行简单的数值运算,则有更简单的类型 than 类型由 标准库。这些类型提供安全、并发、 对原始类型的原子访问。我们选择将 primitive type 这样我们就可以专注于工作原理。Mutex<T>Mutex<T>Mutex<T>

RefCell<T>/Rc<T>Mutex<T>/Arc<T 之间的相似之处>

你可能已经注意到它是不可变的,但我们可以在其 引用其中的值;这意味着提供内部 可变性,就像家庭一样。就像我们在 第 15 章允许我们改变 中的内容,我们用来改变 中的内容。counterMutex<T>CellRefCell<T>Rc<T>Mutex<T>Arc<T>

另一个需要注意的细节是 Rust 无法保护您免受各种逻辑的影响 使用 .回想一下第 15 章 using came 存在创建引用循环的风险,其中两个值引用 彼此,导致内存泄漏。同样,也存在 创建死锁。当作需要锁定两个资源时,会发生这种情况 两个线程各自获取了其中一个锁,导致它们等待 永远彼此。如果您对死锁感兴趣,请尝试创建一个 Rust 具有死锁的程序;然后研究死锁缓解策略 mutex 的 Mutex 中,并尝试在 Rust 中实现它们。这 标准库 API 文档和优惠 有用的信息。Mutex<T>Rc<T>Rc<T>Mutex<T>Mutex<T>MutexGuard

我们将通过讨论 和 traits 和 我们如何将它们与自定义类型一起使用。SendSync

本文档由官方文档翻译而来,如有差异请以官方英文文档(https://doc.rust-lang.org/)为准