使用线程同时运行代码
在大多数当前的作系统中,已执行程序的代码在一个进程中运行,作系统将一次管理多个进程。 在程序中,您还可以拥有同时运行的独立部分。 运行这些独立部分的功能称为线程。为 例如,Web 服务器可以有多个线程,以便它可以响应 多个请求。
将程序中的计算拆分为多个线程以运行多个 任务可以提高性能,但也会增加复杂性。 由于线程可以同时运行,因此没有 不同线程上的代码部分将按顺序运行。这可能会导致 到问题,例如:
- 争用条件,其中线程访问 顺序不一致
- 死锁,其中两个线程相互等待,从而阻止两个线程 线程继续
- 仅在某些情况下发生且难以重现和修复的错误 可靠地
Rust 试图减轻使用线程的负面影响,但是 在多线程上下文中编程仍然需要仔细考虑,并且需要 代码结构不同于在单个 线。
编程语言以几种不同的方式实现线程,并且许多 作系统提供了一个 API,语言可以调用该 API 来创建新的 线程。Rust 标准库使用 1:1 的线程实现模型, 其中,程序对一个语言线程使用一个作系统线程。 有些 crate 实现了其他线程模型,这些模型使 权衡 1:1 模型。
使用 创建新线程spawn
要创建新线程,我们调用thread::spawn
函数并向其传递一个
closure(我们在第 13 章中讨论了 closure)包含我们想要的代码
在新线程中运行。示例 16-1 中的示例从 main 打印了一些文本
线程和来自新线程的其他文本:
文件名: src/main.rs
use std::thread; use std::time::Duration; fn main() { thread::spawn(|| { for i in 1..10 { println!("hi number {i} from the spawned thread!"); thread::sleep(Duration::from_millis(1)); } }); for i in 1..5 { println!("hi number {i} from the main thread!"); thread::sleep(Duration::from_millis(1)); } }
示例 16-1:创建一个新线程来打印一个 而主线程打印其他内容
请注意,当 Rust 程序的主线程完成时,所有生成的线程 将关闭,无论它们是否已完成运行。此 程序可能每次都略有不同,但它看起来类似于 以后:
hi number 1 from the main thread!
hi number 1 from the spawned thread!
hi number 2 from the main thread!
hi number 2 from the spawned thread!
hi number 3 from the main thread!
hi number 3 from the spawned thread!
hi number 4 from the main thread!
hi number 4 from the spawned thread!
hi number 5 from the spawned thread!
对thread::sleep
强制线程暂时停止执行
duration,允许其他线程运行。线程可能需要
转动,但这并不能保证:这取决于您的作系统如何
调度线程。在此运行中,主线程首先打印,即使
spawned thread 的 print 语句首先出现在代码中。甚至
尽管我们告诉生成的线程打印直到i
是 9,则只到了 5
在主线程关闭之前。
如果运行此代码,并且只能看到主线程的输出,或者看不到任何 overlap 中,请尝试增加范围中的数字以创造更多商机 供作系统在线程之间切换。
等待所有线程完成使用join
处理
示例 16-1 中的代码不仅大部分时间过早地停止了生成的线程 时间由于主线程结束,但因为不能保证 线程运行的顺序,我们也不能保证生成的线程 根本就要跑了!
我们可以修复 spawned thread 无法运行或提前结束的问题
通过保存thread::spawn
在变量中。返回类型thread::spawn
是JoinHandle
.一个JoinHandle
是一个拥有的值,当我们
调用join
方法,将等待其线程完成。示例 16-2
演示如何使用JoinHandle
我们在示例 16-1 中创建的线程中,并且
叫join
确保生成的线程在main
出口:
文件名: src/main.rs
use std::thread; use std::time::Duration; fn main() { let handle = thread::spawn(|| { for i in 1..10 { println!("hi number {i} from the spawned thread!"); thread::sleep(Duration::from_millis(1)); } }); for i in 1..5 { println!("hi number {i} from the main thread!"); thread::sleep(Duration::from_millis(1)); } handle.join().unwrap(); }
示例 16-2:保存JoinHandle
从thread::spawn
保证线程运行完成
叫join
在手柄上阻塞当前正在运行的线程,直到
handle 表示的 thread 终止。阻塞线程意味着
thread 无法执行工作或退出。因为我们已经发出了决定
自join
在主线程的for
循环,运行示例 16-2 应该
生成类似于以下内容的输出:
hi number 1 from the main thread!
hi number 2 from the main thread!
hi number 1 from the spawned thread!
hi number 3 from the main thread!
hi number 2 from the spawned thread!
hi number 4 from the main thread!
hi number 3 from the spawned thread!
hi number 4 from the spawned thread!
hi number 5 from the spawned thread!
hi number 6 from the spawned thread!
hi number 7 from the spawned thread!
hi number 8 from the spawned thread!
hi number 9 from the spawned thread!
两个线程继续交替,但主线程等待,因为
调用handle.join()
并且在生成的线程完成之前不会结束。
但是,让我们看看当我们转而移动时会发生什么handle.join()
在for
循环输入main
喜欢这个:
文件名: src/main.rs
use std::thread; use std::time::Duration; fn main() { let handle = thread::spawn(|| { for i in 1..10 { println!("hi number {i} from the spawned thread!"); thread::sleep(Duration::from_millis(1)); } }); handle.join().unwrap(); for i in 1..5 { println!("hi number {i} from the main thread!"); thread::sleep(Duration::from_millis(1)); } }
主线程将等待生成的线程完成,然后运行其for
循环,因此输出将不再交错,如下所示:
hi number 1 from the spawned thread!
hi number 2 from the spawned thread!
hi number 3 from the spawned thread!
hi number 4 from the spawned thread!
hi number 5 from the spawned thread!
hi number 6 from the spawned thread!
hi number 7 from the spawned thread!
hi number 8 from the spawned thread!
hi number 9 from the spawned thread!
hi number 1 from the main thread!
hi number 2 from the main thread!
hi number 3 from the main thread!
hi number 4 from the main thread!
小细节,例如位置join
被调用,则会影响您的
线程同时运行。
用move
带螺纹的闭包
我们经常使用move
关键字,并将闭包传递给thread::spawn
因为 Closure 将从
environment 的 intent 中,从而将这些值的所有权从一个线程转移到
另一个。在第 13 章的 “捕获引用或移动所有权” 一节中,我们讨论了move
在 Closure 的上下文中。现在
我们将更多地关注move
和thread::spawn
.
请注意,在示例 16-1 中,我们传递给thread::spawn
不需要
arguments: 我们没有使用来自生成的
thread 的代码。要在生成的线程中使用来自主线程的数据,
spawned thread 的 closure 必须捕获它需要的值。示例 16-3 显示
尝试在主线程中创建向量并在生成的
线。但是,这还不起作用,您稍后会看到。
文件名: src/main.rs
use std::thread;
fn main() {
let v = vec![1, 2, 3];
let handle = thread::spawn(|| {
println!("Here's a vector: {v:?}");
});
handle.join().unwrap();
}
示例 16-3:尝试使用由 主线程在另一个线程中
闭包使用v
,因此它将捕获v
并将其作为 Closure 的
环境。因为thread::spawn
在新线程中运行此 Closure 时,我们
应该能够访问v
在那个新线程中。但是当我们编译这个
example,我们得到以下错误:
$ cargo run
Compiling threads v0.1.0 (file:///projects/threads)
error[E0373]: closure may outlive the current function, but it borrows `v`, which is owned by the current function
--> src/main.rs:6:32
|
6 | let handle = thread::spawn(|| {
| ^^ may outlive borrowed value `v`
7 | println!("Here's a vector: {v:?}");
| - `v` is borrowed here
|
note: function requires argument type to outlive `'static`
--> src/main.rs:6:18
|
6 | let handle = thread::spawn(|| {
| __________________^
7 | | println!("Here's a vector: {v:?}");
8 | | });
| |______^
help: to force the closure to take ownership of `v` (and any other referenced variables), use the `move` keyword
|
6 | let handle = thread::spawn(move || {
| ++++
For more information about this error, try `rustc --explain E0373`.
error: could not compile `threads` (bin "threads") due to 1 previous error
Rust 推断如何捕获v
,并且因为println!
只需要一个参考
自v
,则 Closure 会尝试借用v
.但是,有一个问题: Rust 不能
告诉生成的线程将运行多长时间,因此它不知道引用
自v
将始终有效。
示例 16-4 提供了一个更有可能引用v
那将是无效的:
文件名: src/main.rs
use std::thread;
fn main() {
let v = vec![1, 2, 3];
let handle = thread::spawn(|| {
println!("Here's a vector: {v:?}");
});
drop(v); // oh no!
handle.join().unwrap();
}
示例 16-4:一个带有闭包的线程,它试图
捕获对v
从丢弃v
如果 Rust 允许我们运行此代码,则有可能生成线程
将立即置于后台,根本不运行。生成的
thread 引用了v
内部,但主线程立即下降v
,使用drop
函数。然后,当
spawned thread 开始执行,v
不再有效,因此对它的引用
也无效。哦不!
为了修复示例 16-3 中的编译器错误,我们可以使用错误消息的 建议:
help: to force the closure to take ownership of `v` (and any other referenced variables), use the `move` keyword
|
6 | let handle = thread::spawn(move || {
| ++++
通过添加move
关键字,我们强制 Closure 采用
它正在使用的值的所有权,而不是允许 Rust 推断它
应该借用这些值。清单 中所示的对示例 16-3 的修改
16-5 将按照我们的预期编译和运行:
文件名: src/main.rs
use std::thread; fn main() { let v = vec![1, 2, 3]; let handle = thread::spawn(move || { println!("Here's a vector: {v:?}"); }); handle.join().unwrap(); }
示例 16-5:使用move
keyword 强制闭包
获得它使用的值的所有权
我们可能很想尝试同样的事情来修复示例 16-4 中的代码,其中
名为drop
通过使用move
关闭。但是,此修复程序将
不起作用,因为示例 16-4 试图做的事情对于
不同的原因。如果我们添加了move
到 Closure 时,我们会移动v
到
closure 的环境,我们不能再调用drop
在它上面
线。相反,我们会收到这个编译器错误:
$ cargo run
Compiling threads v0.1.0 (file:///projects/threads)
error[E0382]: use of moved value: `v`
--> src/main.rs:10:10
|
4 | let v = vec![1, 2, 3];
| - move occurs because `v` has type `Vec<i32>`, which does not implement the `Copy` trait
5 |
6 | let handle = thread::spawn(move || {
| ------- value moved into closure here
7 | println!("Here's a vector: {v:?}");
| - variable moved due to use in closure
...
10 | drop(v); // oh no!
| ^ value used here after move
For more information about this error, try `rustc --explain E0382`.
error: could not compile `threads` (bin "threads") due to 1 previous error
Rust 的所有权规则再次拯救了我们!我们从
示例 16-3 因为 Rust 很保守,只借用v
对于
thread 的
thread 的引用。通过告诉 Rust 移动v
到生成的
thread 的 Rust 中,我们保证主线程不会使用v
了。如果
我们以同样的方式更改示例 16-4,然后我们违反了所有权
规则v
在主线程中。这move
关键字覆盖
Rust 保守的借款违约;它不允许我们违反
所有权规则。
对线程和线程 API 有了基本的了解后,让我们看看我们 可以用线程来做。
本文档由官方文档翻译而来,如有差异请以官方英文文档(https://doc.rust-lang.org/)为准