光阴者,百代之过客,唯有奋力奔跑,方能生风其,是时势造英雄,英雄存在时代
大家好,我是柒八九。
今天,我们继续Rust学习笔记的探索。我们来谈谈关于Rust学习笔记之并发的相关知识点。
如果,想了解该系列的文章,可以参考我们已经发布的文章。如下是往期文章。
文章list
- Rust学习笔记之Rust环境配置和入门指南
- Rust学习笔记之基础概念
- Rust学习笔记之所有权
- Rust学习笔记之结构体
- Rust学习笔记之枚举和匹配模式
- Rust学习笔记之包、Crate和模块
- Rust学习笔记之集合
- Rust学习笔记之错误处理
- Rust学习笔记之泛型、trait 与生命周期
- Rust学习笔记之闭包和迭代器
- Rust学习笔记之智能指针
你能所学到的知识点
- {并发编程|Concurrent Programming} VS {并行编程|Parallel programming}推荐阅读指数 ⭐️⭐️⭐️
- 使用线程同时运行代码 推荐阅读指数 ⭐️⭐️⭐️⭐️⭐️
- 使用消息传递在线程间传送数据 推荐阅读指数 ⭐️⭐️⭐️⭐️
- 共享状态并发 推荐阅读指数 ⭐️⭐️⭐️
- 使用 Sync 和 Send trait 的可扩展并发 推荐阅读指数 ⭐️⭐️⭐️
好了,天不早了,干点正事哇。
{并发编程|Concurrent programming} VS {并行编程|Parallel Programming}
{并发编程|Concurrent programming}和{并行编程|Parallel Programming}都是指在计算机程序中同时执行多个任务或操作的编程方式,但它们在实现方式和目标上存在一些异同点。
{并发编程|Concurrent programming}指的是在一个程序中同时进行多个任务,这些任务可以是独立的,相互之间没有直接的依赖关系。
在并发编程中,这些任务通常是通过交替执行、时间片轮转或事件驱动的方式来实现并行执行的假象。
并发编程的目标是提高程序的效率、响应性和资源利用率。
{并行编程|Parallel Programming}是指在硬件级别上同时执行多个任务,利用计算机系统中的多个处理单元(例如多核处理器)或多台计算机来同时处理多个任务。
在并行编程中,任务之间可以有依赖关系,需要进行任务的分割和协调。
并行编程的目标是实现更高的计算性能和吞吐量。
总结一下,并发编程和并行编程的异同点如下:
- 目标:并发编程旨在提高程序的效率、响应性和资源利用率,而并行编程旨在实现更高的计算性能和吞吐量。
- 执行方式:并发编程通过交替执行、时间片轮转或事件驱动的方式,在一个程序中同时进行多个任务的执行;并行编程通过同时使用多个处理单元或计算机来同时执行多个任务。
- 任务关系:并发编程中的任务通常是独立的,相互之间没有直接的依赖关系;而并行编程中的任务可能存在依赖关系,需要进行任务的分割和协调。
- 实现方式:并发编程可以通过线程、进程、协程等机制来实现;并行编程可以通过并行算法、分布式计算等技术来实现。
{并发编程|Concurrent programming},代表程序的不同部分相互独立的执行,而 {并行编程|parallel programming}代表程序不同部分于同时执行,这两个概念随着计算机越来越多的利用多处理器的优势时显得愈发重要。
使用线程同时运行代码
在大部分现代操作系统中,已执行程序的代码在一个 {进程|Process}中运行,操作系统则负责管理多个进程。在程序内部,也可以拥有多个同时运行的独立部分。运行这些独立部分的功能被称为 {线程|Threads}。
进程和线程
{进程|Process}和{线程|Threads}是操作系统中用于执行程序的基本执行单位,它们之间有着密切的关系,但又有一些本质的区别。
{进程|Process}是操作系统中的一个运行实例,它包含了程序执行所需的代码、数据和资源。
每个进程都有自己的地址空间,包括独立的堆、栈和全局数据区域。进程之间是相互独立的,它们不能直接访问其他进程的内部数据,通信和数据共享需要通过操作系统提供的机制(如管道、共享内存等)进行。
{线程|Threads}是进程中的一个执行流,它是进程中的一个独立单元,负责执行程序中的指令。
一个进程可以拥有多个线程,这些线程共享进程的地址空间和资源,包括堆、栈和全局数据区域。不同线程之间可以直接访问进程的内部数据,它们共享相同的上下文环境,因此线程之间的通信和数据共享比进程之间更加高效。
进程和线程之间的关系
- 进程是程序的执行实例,它可以包含多个线程。
- 进程之间是独立的,相互之间不能直接访问对方的内部数据,通信需要通过操作系统提供的机制。
- 同一进程内的多个线程共享进程的地址空间和资源,它们可以直接访问进程的内部数据。
- 进程之间的切换开销比线程之间的切换开销更大,因为进程切换需要保存和恢复整个进程的上下文环境,而线程切换只需要保存和恢复线程的上下文环境。
- 进程之间的并行执行是由操作系统的调度器决定的,而线程之间的并行执行是由线程调度器(也称为内核级线程调度器或用户级线程调度器)决定的。
线程的问题和类型
将程序中的计算拆分进多个线程可以改善性能,因为程序可以同时进行多个任务,不过这也会增加复杂性。因为线程是同时运行的,所以无法预先保证不同线程中的代码的执行顺序。这会导致诸如此类的问题:
- {竞争状态|Race conditions},多个线程以不一致的顺序访问数据或资源
- {死锁|Deadlocks},两个线程相互等待对方停止使用其所拥有的资源,这会阻止它们继续运行
- 只会发生在特定情况且难以稳定重现和修复的 bug
1:1线程
很多操作系统提供了创建新线程的 API。这种由编程语言调用操作系统 API
创建线程的模型有时被称为 1:1
,一个 OS 线程对应一个语言线程。
用java
创建一个1:1
线程
// 创建线程类 class MyThread extends Thread { public void run() { // 线程执行的代码 } } // 创建并启动线程 public class Main { public static void main(String[] args) { MyThread thread = new MyThread(); thread.start(); } }
绿色线程
很多编程语言提供了自己特殊的线程实现。编程语言提供的线程被称为 {绿色|green}线程,使用绿色线程的语言会在不同数量的 OS
线程的上下文中执行它们。为此,绿色线程模式被称为 M:N
模型:M
个绿色线程对应 N
个 OS
线程,这里 M
和 N
不必相同。
用javascript
创建一个绿色线程
。
// 在 worker.js 文件中的代码 self.onmessage = function(event) { // 处理消息 // 发送消息回主线程 self.postMessage('处理完成'); }; // 在主线程中的代码 var worker = new Worker('worker.js'); worker.onmessage = function(event) { // 处理 worker 返回的消息 }; worker.postMessage('开始处理');
Web Worker
引入了一种不同的线程模型,它在 JavaScript
运行时环境中实现了类似线程的机制,但是并不依赖于操作系统的线程。Web Worker
在底层使用了浏览器提供的异步事件模型,利用了浏览器的多线程特性。
Web Worker
并非真正的操作系统级线程,它是在 JavaScript
运行时环境中模拟的线程。每个 Web Worker
都有自己的上下文和事件循环,它们之间通过消息传递进行通信。因此,Web Worker
通常被称为绿色线程,因为它们在用户级别实现了线程的效果,而不需要依赖操作系统的线程管理。
在当前上下文中,{运行时|Runtime} 代表二进制文件中包含的由语言自身提供的代码。这些代码根据语言的不同可大可小,不过任何非汇编语言都会有一定数量的运行时代码。为此,通常人们说一个语言 没有运行时,一般意味着 小运行时。更小的运行时拥有更少的功能不过其优势在于更小的二进制输出,这使其易于在更多上下文中与其他语言相结合。
绿色线程的
M:N 模型
需要更大的语言运行时来管理这些线程。因此,Rust
标准库只提供了 1:1 线程模型实现。
使用 spawn 创建新线程
为了创建一个新线程,需要调用 thread::spawn
函数并传递一个闭包,并在其中包含希望在新线程运行的代码。
use std::thread; use std::time::Duration; fn main() { thread::spawn(|| { for i in 1..10 { println!("hi number {} from the spawned thread!", i); thread::sleep(Duration::from_millis(1)); } }); for i in 1..5 { println!("hi number {} from the main thread!", i); thread::sleep(Duration::from_millis(1)); } }
当主线程结束时,新线程也会结束,而不管其是否执行完毕。这个程序的输出可能每次都略有不同。
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
调用强制线程停止执行一小段时间,这会允许其他不同的线程运行。这些线程可能会轮流运行,不过并不保证如此:这依赖操作系统如何调度线程。在这里,主线程首先打印,即便新创建线程的打印语句位于程序的开头,甚至即便我们告诉新建的线程打印直到 i 等于 9 ,它在主线程结束之前也只打印到了 5。
使用 join 等待所有线程结束
由于主线程结束,上面的代码大部分时候不光会提早结束新建线程,甚至不能实际保证新建线程会被执行。其原因在于无法保证线程运行的顺序!
可以通过将 thread::spawn
的返回值储存在变量中来修复新建线程部分没有执行或者完全没有执行的问题。thread::spawn
的返回值类型是 JoinHandle
。JoinHandle
是一个拥有所有权的值,当对其调用 join
方法时,它会等待其线程结束。
use std::thread; use std::time::Duration; fn main() { let handle = thread::spawn(|| { for i in 1..10 { println!("hi number {} from the spawned thread!", i); thread::sleep(Duration::from_millis(1)); } }); for i in 1..5 { println!("hi number {} from the main thread!", i); thread::sleep(Duration::from_millis(1)); } handle.join().unwrap(); }
通过调用 handle
的 join
会阻塞当前线程直到 handle
所代表的线程结束。{阻塞|Blocking}线程意味着阻止该线程执行工作或退出。
运行上面的代码应该会产生类似这样的输出:
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()
调用会等待直到新建线程执行完毕。
线程与 move 闭包
move
闭包,其经常与thread::spawn
一起使用,因为它允许我们在一个线程中使用另一个线程的数据。
可以在参数列表前使用 move
关键字强制闭包获取其使用的环境值的所有权。
为了在新建线程中使用来自于主线程的数据,需要新建线程的闭包获取它需要的值。
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(); }
通过告诉 Rust
将 v
的所有权移动到新建线程,我们向 Rust
保证主线程不会再使用 v
。 move
关键字覆盖了 Rust
默认保守的借用,但它不允许我们违反所有权规则。