@ -22,7 +22,7 @@
1. 必须记住在使用数据之前尝试获取锁。
2. 一旦处理完被互斥器所保护的数据之后,必须记得解锁数据这样其他线程才能够获取锁。
对于一个现实中的互斥器的例子,想象一下在一个会议中的专门小组讨论会上,不过只有一个麦克风。在一个小组成员可能发言之前,他们必须请求或示意他们需要使用麦克风。一旦得到了麦克风,他们可以发言任意长的时间,接着将麦克风交给系 一个希望讲话的小组成员。如果小组成员在没有麦克风的时候就开始叫喊或者在其他成员发言结束之前就取得麦克风将是很无理的。如果对这个共享的麦克风的管理因为任何这些原因出现问题,讨论会将无法如期进行。
对于一个现实中的互斥器的例子,想象一下在一个会议中的专门小组讨论会上,不过只有一个麦克风。在一个小组成员可能发言之前,他们必须请求或示意他们需要使用麦克风。一旦得到了麦克风,他们可以发言任意长的时间,接着将麦克风交给下 一个希望讲话的小组成员。如果小组成员在没有麦克风的时候就开始叫喊或者在其他成员发言结束之前就取得麦克风将是很无理的。如果对这个共享的麦克风的管理因为任何这些原因而 出现问题,讨论会将无法如期进行。
正确的管理互斥器是异常复杂的,这也就是为什么这么多人都热衷于通道。然而,在 Rust 中,得益于类型系统和所有权,我们不可能会在锁和解锁上出错。
@ -50,9 +50,9 @@ fn main() {
< span class = "caption" > Listing 16-12: Exploring the API of `Mutex<T>` in a
single threaded context for simplicity< / span >
与很多类型一样,我们通过叫做`new`的关联函数来创建一个`Mutex< T > `。为了访问互斥器中的数据,使用`lock`方法来获取锁。这个调用会阻塞到直到轮到我们拥有锁为止。如果另一个线程拥有锁接着那个线程 panic 了则这个调用会失败。类似于上一部分列表 16-6 那样,我们暂时使用`unwrap()`而不是更好的错误处理。 请查看第九章中提供的更好的工具。
与很多类型一样,我们通过叫做`new`的关联函数来创建一个`Mutex< T > `。为了访问互斥器中的数据,使用`lock`方法来获取锁。这个调用会阻塞到直到轮到我们拥有锁为止。如果另一个线程拥有锁接着那个线程 panic 了则这个调用会失败。类似于上一部分列表 16-6 那样,我们暂时使用`unwrap()`,至于更好的错误处理, 请查看第九章中提供的更好的工具。
一旦获取了锁,就可以将返回值(在这里是`num`)作为一个数据的可变引用使用了。类型系统是 Rust 如何保证使用值之前必须获取锁的:`Mutex< i32 > `并不是一个`i32`,所以**必须**获取锁才能使用这个`i32`值。我们是不会忘记这么做的;类否则 型系统是不会允许的。
一旦获取了锁,就可以将返回值(在这里是`num`)作为一个数据的可变引用使用了。类型系统是 Rust 如何保证使用值之前必须获取锁的:`Mutex< i32 > `并不是一个`i32`,所以**必须**获取锁才能使用这个`i32`值。我们是不会忘记这么做的;因为 类型系统是不会允许的。
与你可能怀疑的一样,`Mutex< T > `是一个智能指针。好吧,更准确的说,`lock`调用返回一个叫做`MutexGuard`的智能指针。类似我们在第十五章见过的智能指针,它实现了`Deref`来指向其内部数据。另外`MutexGuard`有一个用来释放锁的`Drop`实现。这样就不会忘记释放锁了。这在`MutexGuard`离开作用域时会自动发生,例如它发生于列表 16-12 中内部作用域的结尾。接着可以打印出互斥器的值并发现能够将其内部的`i32`改为 6。
@ -197,7 +197,7 @@ error[E0382]: use of moved value: `counter`
error: aborting due to 2 previous errors
```
啊哈! 在第一个错误信息中, Rust 表明了`counter`被移动进了`handle`所代表线程的闭包中。这个移动阻止我们在对其调用`lock`并将结果储存在`num2`中时捕获`counter`,这是已经在第二个线程中了 !所以 Rust 告诉我们不能将`counter`的所有权移动到多个线程中。这在之前很难看出是因为我们在循环中创建多个线程,而 Rust 无法在循环的迭代中指明不同的线程(没有临时变量)。
啊哈! 在第一个错误信息中, Rust 表明了`counter`被移动进了`handle`所代表线程的闭包中。这个移动阻止我们在第二个线程中 对其调用`lock`并将结果储存在`num2`中时捕获`counter`!所以 Rust 告诉我们不能将`counter`的所有权移动到多个线程中。这在之前很难看出是因为我们在循环中创建多个线程,而 Rust 无法在循环的迭代中指明不同的线程(没有临时变量`num2` )。
#### 多线程和多所有权
@ -254,15 +254,15 @@ std::marker::Send` is not satisfied
= note: required by `std::thread::spawn`
```
哇哦,太长不看!需要指出一些重要的部分:第一个提示表明`Rc< Mutex < i32 > >`不能安全的在线程间传递。理由也在错误信息中,经过提取之后,表明“不满足`Send` trait bound”( `the trait bound Send is not satisfied`)。下一部分将会讨论`Send`,它是一个确保确保用于线程的类型是适合并发环境的 trait 。
哇哦,太长不看!需要指出一些重要的部分:第一个提示表明`Rc< Mutex < i32 > >`不能安全的在线程间传递。理由也在错误信息中,经过提取之后,表明“不满足`Send` trait bound”( `the trait bound Send is not satisfied`)。下一部分将会讨论`Send`,它是许多确保用在多线程中的类型能够适合并发环境的 trait 之一 。
不幸的是,`Rc< T > `并不能安全的在线程间共享。当`Rc< T > `管理引用计数时,它必须在每一个`clone`调用时增加计数并在每一个克隆被丢弃时减少计数。`Rc< T > `并没有使用任何并发原语来确保改变计数的操作不会被其他线程打断。在计数出错时这 可能会导致诡异的 bug, 比如可能会造成内存泄漏或在使用结束之前就丢弃一个值。那么如果有一个正好与`Rc< T > `类似,不过以一种线程安全的方式改变引用计数的类型会怎么样呢?
不幸的是,`Rc< T > `并不能安全的在线程间共享。当`Rc< T > `管理引用计数时,它必须在每一个`clone`调用时增加计数并在每一个克隆被丢弃时减少计数。`Rc< T > `并没有使用任何并发原语来确保改变计数的操作不会被其他线程打断。在计数出错时可能会导致诡异的 bug, 比如可能会造成内存泄漏或在使用结束之前就丢弃一个值。那么如果有一个正好与`Rc< T > `类似,不过以一种线程安全的方式改变引用计数的类型会怎么样呢?
#### 原子引用计数`Arc< T > `
如果你思考过像之前那样的问题的话,你就是正确的。确实有一个类似`Rc< T > `并可以安全的用于并发环境的类型:`Arc< T > `。字母“a”代表**原子性**( *atomic*),所以这是一个**原子引用计数**( *atomically reference counted*)类型。原子性是另一类这里还未涉及到的并发原语;请查看标准库中`std::sync::atomic`的文档来获取更多细节。其中的要点就是:原子性类型工作起来类似原始类型,不过可以安全的在线程间共享。
那为什么不是所有的原始类型都是原子性的呢,然后为什么不是所有标准库中的类型都默认使用`Arc< T > `实现呢?线程安全伴随一些性能惩罚,我们只希望在需要时才为此付出代价。如果只是在单线程中会 值进行操作,因为并不需要原子性提供的保证代码可以运行的更快。
那为什么不是所有的原始类型都是原子性的呢,然后为什么不是所有标准库中的类型都默认使用`Arc< T > `实现呢?线程安全伴随一些性能惩罚,我们只希望在需要时才为此付出代价。如果只是在单线程中对 值进行操作,因为并不需要原子性提供的保证,所以 代码可以运行的更快。
回到之前的例子:`Arc< T > `和`Rc< T > `除了`Arc< T > `内部的原子性之外他们是等价的。其 API 也是一样的,所以可以修改`use`行和`new`调用。列表 16-15 中的代码最终可以编译和运行:
@ -303,12 +303,12 @@ to be able to share ownership across multiple threads</span>
Result: 10
```
成功了!我们从 0 数到了 10, 这可能并不是很显眼, 不过一路上我们学习了很多关于`Mutex< T > `和线程安全的内容!这个例子中构建的结构可以用于比增加计数更为复杂的操作。可以 被分解为独立部分的计算可以像这样被分散到多个线程中,并可以使用`Mutex< T > `来允许每个线程在他们自己的部分更新最终的结果。
成功了!我们从 0 数到了 10, 这可能并不是很显眼, 不过一路上我们学习了很多关于`Mutex< T > `和线程安全的内容!这个例子中构建的结构可以用于比增加计数更为复杂的操作。能够 被分解为独立部分的计算可以像这样被分散到多个线程中,并可以使用`Mutex< T > `来允许每个线程在他们自己的部分更新最终的结果。
你可能注意到了,因为`counter`是不可变的,不过可以获取其内部值的可变引用,这意味着`Mutex< T > `提供了内部可变性,就像`Cell`系列类型那样。正如第十五章中使用`RefCell< T > `可以改变`Rc< T > `中的内容那样,同样的可以使用`Mutex< T > `来改变`Arc< T > `中的内容。
回忆一下`Rc< T > `并没有避免所有可能的问题:我们也讨论了当两个`Rc< T > `相互引用时的引用循环的可能性,这可能造成内存泄露。`Mutex< T > `有一个类似的 Rust 同样也不能避免的问题:死锁。**死锁**( *deadlock*)是一个场景中操作需要锁定两个资源,而两个线程分别拥有一个锁并永远相互等待的问题。如果你对这个主题感兴趣,尝试编写一个带有死锁的 Rust 程序,接着研究任何其他语言中使用互斥器的死锁规避策略并尝试在 Rust 中实现他们。标准库中`Mutex< T > `和`MutexGuard`的 API 文档会提供拥 有的信息。
回忆一下`Rc< T > `并没有避免所有可能的问题:我们也讨论了当两个`Rc< T > `相互引用时的引用循环的可能性,这可能造成内存泄露。`Mutex< T > `有一个类似的 Rust 同样也不能避免的问题:死锁。**死锁**( *deadlock*)是一个场景中操作需要锁定两个资源,而两个线程分别拥有一个锁并永远相互等待的问题。如果你对这个主题感兴趣,尝试编写一个带有死锁的 Rust 程序,接着研究任何其他语言中使用互斥器的死锁规避策略并尝试在 Rust 中实现他们。标准库中`Mutex< T > `和`MutexGuard`的 API 文档会提供有用 的信息。
Rust 的类型系统和所有权确保了线程在更新共享值时拥有独占的访问权限,所以线程不会以一种不可预测的方式覆盖彼此的操作。为了和编译器一起使一切正确运行花了一些时间,不过 我们节省了未来可能需要重现只在线程以特定顺序执行才会出现的诡异错误场景的时间。
Rust 的类型系统和所有权(规则) 确保了线程在更新共享值时拥有独占的访问权限,所以线程不会以一种不可预测的方式覆盖彼此的操作。虽然为了使一切正确运行而在编译器上花了一些时间,但是 我们节省了未来可能需要重现只在线程以特定顺序执行时 才会出现的诡异错误场景的时间。
接下来,为了丰富本章的内容,让我们讨论一下`Send`和`Sync` trait 以及如何对自定义类型使用他们。