当前位置: 首页 > article >正文

Rust入门之并发编程基础(一)

Rust入门之并发编程基础(一)

无畏并发

本文源码

安全且高效地处理并发编程是 Rust 的另一个主要目标。并发编程Concurrent programming),代表程序的不同部分相互独立地执行,而 并行编程parallel programming)代表程序不同部分同时执行,这两个概念随着计算机越来越多的利用多处理器的优势而显得愈发重要。由于历史原因,在此类上下文中编程一直是困难且容易出错的:Rust 希望能改变这一点。

起初,Rust 团队认为确保内存安全和防止并发问题是两个分别需要不同方法应对的挑战。随着时间的推移,团队发现所有权和类型系统是一系列解决内存安全 并发问题的强有力的工具!通过利用所有权和类型检查,在 Rust 中很多并发错误都是 编译时 错误,而非运行时错误。因此,相比花费大量时间尝试重现运行时并发 bug 出现的特定情况,Rust 会拒绝编译不正确的代码并提供解释问题的错误信息。因此,你可以在开发时修复代码,而不是在部署到生产环境后修复代码。我们给 Rust 的这一部分起了一个绰号 无畏并发fearless concurrency)。无畏并发令你的代码免于出现诡异的 bug 并可以轻松重构且无需担心会引入新的 bug。本文重点介绍了以下内容:

  1. 多线程基础
    • 使用thread::spawn创建线程,通过JoinHandlejoin方法确保线程同步,避免主线程提前退出。
    • move闭包强制转移变量所有权到子线程,避免悬垂引用,保障跨线程数据安全。
  2. 消息传递机制
    • 通过mpsc通道(多生产者单消费者模型)实现线程间通信,发送端(Sender)与接收端(Receiver)解耦。
    • 发送数据时自动转移所有权,防止发送后篡改,天然规避数据竞争问题。
  3. 灵活性与扩展性
    • 支持clone生成多个发送端,实现多生产者场景;接收端通过迭代器循环读取消息,简化代码逻辑。

Rust 无畏并发的特点

  • 目标:安全高效的并发编程
  • 独特方法:利用所有权和类型系统在编译时防止并发错误
  • 优势:在开发阶段而非生产环境中发现错误
  • 灵活性:为不同并发模型提供多种工具

使用多线程同时执行代码

在操作系统中,正在执行的一个独立程序是一个进程,而程序中可以存在多个同时运行的独立单元,这些独立单元被称之为线程。例如:web 服务器可以有多个线程以便可以同时响应多个请求。

多线程运行可能导致的问题:

  • 竞态条件(Race conditions),多个线程以不一致的顺序访问数据或资源
  • 死锁(Deadlocks)两个线程相互等待对方,这会阻止两个继续运行
  • 只会发生在特定情况且难以稳定重现和修复的bug

Rust 尝试减轻使用线程的负面影响。不过在多线程上下文中编程仍需格外小心,同时其所要求的代码结构也不同于运行于单线程的程序。

编程语言有一些不同的方法来实现线程,而且很多**操作系统提供了创建新线程的 API。Rust 标准库使用 1:1 线程实现,这代表程序的每一个语言级线程使用一个系统线程。**有一些 crate 实现了其他有着不同于 1:1 模型取舍的线程模型。

使用spawn 创建新线程

创建新线程可以使用thread::spawn 函数并传递一个闭包。

代码示例:

#[cfg(test)]
mod tests {use std::{thread, time::Duration};#[test]fn test_thread() {thread::spawn(|| {for i in 1..10 {println!("hi number {} from the spawn 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));}}}

执行单元测试:

successes:---- thread_code::tests::test_thread stdout ----
hi number 1 from the main thread!
hi number 1 from the spawn thread!
hi number 2 from the main thread!
hi number 2 from the spawn thread!
hi number 3 from the main thread!
hi number 3 from the spawn thread!
hi number 4 from the main thread!
hi number 4 from the spawn thread!successes:thread_code::tests::test_thread

主线程打印到4,子线程打印到了4,如果再多试几次,我们会发现子线程始终无法打印完for循环的从1到9。原因是当Rust程序主线程结束的时候,所有已经创建的其他线程都会关闭掉。

如果希望所有子线程执行结束后,程序再关闭,该如何实现呢?就要使用下面介绍的方法了。

使用join Handles 等待所有线程完成

  • 我们可以通过将thread::spawn 的返回值保存到一个变量中,来解决生成的线程无法运行或过早结束的问题
  • thread::spawn 的返回值是 JoinHandle
  • JoinHandle 是一个拥有的值,当我们调用它的join方法时,它会等待其线程完成
  • Handle 上调用join 会阻塞当前正在运行的线程,直到该Hande代表的线程终止
    • 阻塞一个线程意味着该线程被阻止执行工作或退出

代码示例:

#[cfg(test)]
mod tests {use std::{thread, time::Duration};#[test]fn test_thread() {let join_handle = thread::spawn(|| {for i in 1..10 {println!("hi number {} from the spawn 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));}join_handle.join().unwrap();println!("Thread finished execution");}}

单元测试执行结果如下:

---- thread_code::tests::test_thread stdout ----
hi number 1 from the main thread!
hi number 1 from the spawn thread!
hi number 2 from the main thread!
hi number 2 from the spawn thread!
hi number 3 from the main thread!
hi number 3 from the spawn thread!
hi number 4 from the main thread!
hi number 4 from the spawn thread!
hi number 5 from the spawn thread!
hi number 6 from the spawn thread!
hi number 7 from the spawn thread!
hi number 8 from the spawn thread!
hi number 9 from the spawn thread!
Thread finished execution

通过调用join 方法,使得子线程打印完了从1到9,整个程序才结束。

在线程中使用 move 闭包

我们经常会在传递给thread::spawn 的闭包中使用move 关键字,因为这样闭包会接管它从环境中使用的值的所有权,从而将这些值的所有权从一个线程转移到另一个线程。

再看这样一个例子,定义一个数组v,我们在子线程中打印这个数组v,最后调用join 方法,

    #[test]fn test_thread_move() {let v = vec![1, 2, 3];let handle = thread::spawn(|| {println!("Here's a vector:{v:?}");});handle.join().unwrap();}

编译报错:

  --> crates/thread_demo/src/thread_code.rs:27:36|
27 |         let handle = thread::spawn(|| {|                                    ^^ may outlive borrowed value `v`
28 |             println!("Here's a vector:{v:?}");|                                        - `v` is borrowed here|
note: function requires argument type to outlive `'static`--> crates/thread_demo/src/thread_code.rs:27:22|
27 |           let handle = thread::spawn(|| {|  ______________________^
28 | |             println!("Here's a vector:{v:?}");
29 | |         });| |__________^
help: to force the closure to take ownership of `v` (and any other referenced variables), use the `move` keyword|
27 |         let handle = thread::spawn(move || {|                                    ++++

闭包使用了 v,所以闭包会捕获 v 并使其成为闭包环境的一部分。因为 thread::spawn 在一个新线程中运行这个闭包,所以可以在新线程中访问 v

Rust 会 推断 如何捕获 v,因为 println! 只需要 v 的引用,闭包尝试借用 v。然而这有一个问题:Rust 不知道这个新建线程会执行多久,所以无法知晓对 v 的引用是否一直有效。

通过在闭包之前增加 move 关键字,我们强制闭包获取其使用的值的所有权,而不是任由 Rust 推断它应该借用值。

这样代码就可以正确执行了。

    #[test]fn test_thread_move() {let v = vec![1, 2, 3];let handle = thread::spawn(move || {println!("Here's a vector:{v:?}");});handle.join().unwrap();}

使用消息传递在线程间传送数据

线程间消息传递或者线程间共享内存是多线程开发中的两种数据通讯方式。Rust 标准库提供了一个 信道channel)实现。信道是一个通用编程概念,也可以称之为通道,表示数据从一个线程发送到另一个线程。

  • 两个核心部分(transmitter)和接收端(receiver)
  • 当通道的任一端(发送端或接收端)被丢弃时,我们说通道被关闭了

代码示例:

fn channel_value() {let (tx, rx) = mpsc::channel();thread::spawn(move || {let val = String::from("Hi");tx.send(val).unwrap();});let received = rx.recv().unwrap();println!("Got: {}", received);
}

执行结果:

successes:---- tests::test_channel_value stdout ----
Got: Hisuccesses:tests::test_channel_value

使用 mpsc::channel 函数创建一个新的信道;mpsc多个生产者,单个消费者multiple producer, single consumer)的缩写。简而言之,Rust 标准库实现信道的方式意味着一个信道可以有多个产生值的 发送sending)端,但只能有一个消费这些值的 接收receiving)端。

信道发送数据的会发生所有权转移

fn channel_value() {let (tx, rx) = mpsc::channel();thread::spawn(move || {let val = String::from("Hi");tx.send(val).unwrap();println!("val is: {}", val); // ========这行会报错==========});let received = rx.recv().unwrap();println!("Got: {}", received);
}

报错提示:

borrow of moved value: `val`
value borrowed here after move

send 线程已经将数据发送,在send线程中,发送之后再使用所发送的数据。这在Rust 中是不允许的,因为发生了所有权的转移,数据所有权现在归接收线程所有。想象一下如果A线程发送数据给B线程,A线程依然更改了数据,B线程拿到的数据可能会导致发生数据不一致的情况从而导致意外的结果。

发送多个值

use std::{sync::mpsc, thread, time::Duration};fn main() {let (tx, rx) = mpsc::channel();thread::spawn(move || {let vals = vec![String::from("hi"),String::from("from"),String::from("the"),String::from("thread")];for val in vals {tx.send(val).unwrap();thread::sleep(Duration::from_secs(2));}});println!("main thread ...");for recv in rx {println!("Got :{}", recv);}}

子线程发送端每隔1s 发送一个 String,主线程接收端同样每隔1s打印接收到的String。

通过clone 实现多生产者

代码示例:

use std::{sync::mpsc, thread, time::Duration};fn main() {let (tx, rx) = mpsc::channel();let tx1 = mpsc::Sender::clone(&tx);thread::spawn(move || {let vals = vec![String::from("hi"),String::from("Tome"),String::from("from"),String::from("the"),String::from("thread")];for val in vals {tx.send(val).unwrap();thread::sleep(Duration::from_secs(1));}});thread::spawn(move || {let vals = vec![String::from("hi1"),String::from("Tome1"),String::from("from1"),String::from("the1"),String::from("thread1")];for val in vals {tx1.send(val).unwrap();thread::sleep(Duration::from_secs(1));}});println!("main thread...");for received in rx {println!("Got: {}", received);}}

执行结果如下:

main thread...
Got: hi
Got: hi1
Got: Tome
Got: Tome1
Got: from
Got: from1
Got: the
Got: the1
Got: thread
Got: thread1

在创建新线程之前,我们对发送端调用了 clone 方法。这会给我们一个可以传递给第一个新建线程的发送端句柄。我们会将原始的信道发送端传递给第二个新建线程。这样就会有两个线程,每个线程将向信道的接收端发送不同的消息。

总结

Rust通过无畏并发的设计理念,为开发者提供了安全且高效的并发编程工具。其核心优势在于利用所有权系统类型检查在编译阶段拦截并发错误(如数据竞争、死锁),而非依赖运行时调试。后续文章将深入探讨共享状态并发、Sync/Send trait等高级主题,进一步揭示Rust如何通过类型系统简化复杂并发场景的开发。

相关文章:

Rust入门之并发编程基础(一)

Rust入门之并发编程基础(一) 无畏并发 本文源码 安全且高效地处理并发编程是 Rust 的另一个主要目标。并发编程(Concurrent programming),代表程序的不同部分相互独立地执行,而 并行编程(par…...

高级特性实战:死信队列、延迟队列与优先级队列(二)

三、延迟队列:实现任务定时执行 3.1 延迟队列概念解析 延迟队列(Delay Queue),是一种特殊的队列,它的独特之处在于队列中的元素(消息)并不会立即被处理,而是会在指定的延迟时间过后…...

VR 电缆故障测试系统:技术革新​

VR 电缆故障测试系统,作为电力领域的创新科技成果,融合了虚拟现实技术、三维建模、实时交互等前沿技术,为电缆故障测试带来了全新的解决方案。它的工作原理犹如一位经验丰富的侦探,通过层层线索,精准地锁定电缆故障的位…...

Rocky Linux上安装Go

使用官方二进制包安装 1. 下载 Go 官方二进制包 cd /tmp wget https://go.dev/dl/go1.22.3.linux-amd64.tar.gz2. 解压并安装到 /usr/local sudo rm -rf /usr/local/go # 如果之前有旧版本先删除 sudo tar -C /usr/local -xzf go1.22.3.linux-amd64.tar.gz3. 设置环境变量…...

深度学习论文: FastVLM: Efficient Vision Encoding for Vision Language Models

深度学习论文: FastVLM: Efficient Vision Encoding for Vision Language Models FastVLM: Efficient Vision Encoding for Vision Language Models PDF: https://www.arxiv.org/abs/2412.13303 PyTorch代码: https://github.com/shanglianlm0525/CvPytorch PyTorch代码: https…...

白杨SEO:做AI搜索优化的DeepSeek、豆包、Kimi、百度文心一言、腾讯元宝、通义、智谱、天工等AI生成内容信息采集主要来自哪?占比是多少?

大家好,我是白杨SEO,专注SEO十年以上,全网SEO流量实战派,AI搜索优化研究者。 在开始写之前,先说个抱歉。 上周在上海客户以及线下聚会AI搜索优化分享说各大AI模型的联网搜索是关闭的,最开始上来确实是的。…...

显示docker桌面,vnc远程连接docker

目录 相关概念: 实现步骤: 1.启动docker容器 2.安装x11 3.Docker 容器中安装一个完整的图形桌面(XFCE)和 VNC 远程桌面服务器(TightVNC) 4.配置vncservice 5.本地安装VNC Viewer连接VNC Viewer下载地…...

Web 端顶级视效实现:山海鲸端渲染底层原理与发布模式详解

大家好,欢迎大家回到山海鲸的渲染模式系列教程。昨天,我们看了一下山海鲸支持的3种渲染模式的整体概览。今天,我们就来看一下山海鲸支持的最基础的渲染模式,也就是端渲染的渲染设置。 1. 山海鲸的端渲染 我们说到端渲染&#xf…...

腾讯云国际站性能调优

全球化业务扩张中,云端性能直接决定用户体验与商业成败。腾讯云国际站通过资源适配、网络优化与存储革新,为企业提供全链路调优方案。 ​​资源精准适配​​ 实例选型需与业务场景深度耦合,计算优化型实例加速AI训练效率3倍,内存…...

深入解析操作系统内核与用户空间以及内核态与用户态转换

用户空间和内核空间的划分是现代操作系统的基础,对应用程序网络模型的设计和优化有着深远的影响。 内核空间与用户空间的分工 现代操作系统为了保证系统的稳定性和安全性,将虚拟内存空间划分为用户空间和内核空间。 一、用户空间 用户空间是用户程序…...

每日一题洛谷P8662 [蓝桥杯 2018 省 AB] 全球变暖c++

P8662 [蓝桥杯 2018 省 AB] 全球变暖 - 洛谷 (luogu.com.cn) DFS #include<iostream> using namespace std; int n, res; char a[1005][1005]; bool vis[1005][1005]; bool flag; int dx[4] { 0,0,1,-1 }; int dy[4] { 1,-1,0,0 }; void dfs(int x, int y) {vis[x][y]…...

【JVM】初识JVM 从字节码文件到类的生命周期

初识JVM JVM&#xff08;Java Virtual Machine&#xff09;即 Java 虚拟机&#xff0c;是 Java 技术的核心组件之一。JVM的本质就是运行在计算机上的一个程序&#xff0c;通过软件模拟实现了一台抽象的计算机的功能。JVM是Java程序的运行环境&#xff0c;负责加载字节码文件&a…...

多级体验体系构建:基于开源AI智能客服与AI智能名片的S2B2C商城小程序体验升级路径研究

摘要&#xff1a;在体验经济时代&#xff0c;传统企业单一的总部体验模式难以覆盖全链路用户需求。本文针对B端与C端体验深度差异&#xff0c;提出“一级总部体验—二级区域体验—三级终端体验”的分层架构&#xff0c;并引入“开源AI智能客服”与“AI智能名片”技术&#xff0…...

每日算法 -【Swift 算法】字符串转整数算法题详解:myAtoi 实现与正则表达式对比

Swift 字符串转整数算法题详解&#xff1a;myAtoi 实现与正则表达式对比 &#x1f9e9; 题目背景 LeetCode 上的经典算法题 8. String to Integer (atoi) 是一道考察字符串解析与边界处理的题目。这道题虽看似简单&#xff0c;但处理细节相当复杂。我们将使用 Swift 语言实现…...

记录一个难崩的bug

1.后端配置了 Filter 过滤器&#xff0c;如果再配置了Configuration ,那么会出现冲突吗&#xff1f; 过滤器与Configuration类本身无直接冲突&#xff0c;但需注意注册机制、执行顺序和依赖管理。通过显式控制过滤器的注册方式和优先级&#xff0c;结合Spring Security的链式配…...

Git切换历史版本及Gitee云绑定

1、git介绍 Git是目前世界上最先进的分布式版本控制系统 Linux <- BitKeeper&#xff08;不是开源的&#xff0c;但免费的&#xff0c;后来要收费&#xff09; Linus Torvalds(林纳斯托瓦兹) 两周时间吧&#xff0c;弄了个 Git&#xff1b;大约一个月就把Linux代码从BitK…...

智能外呼系统中 NLP 意图理解的工作原理与技术实现

智能外呼系统通过整合语音识别&#xff08;ASR&#xff09;、自然语言处理&#xff08;NLP&#xff09;和语音合成&#xff08;TTS&#xff09;等技术&#xff0c;实现了自动化的电话交互。其中&#xff0c;NLP 意图理解是核心模块&#xff0c;负责解析用户话语中的语义和意图&…...

服务器的IP是什么东西?

一、什么是服务器的IP地址&#xff1f; 服务器的IP地址是互联网协议&#xff08;Internet Protocol&#xff09;的缩写&#xff0c;是服务器在网络中的唯一数字标识符。它类似于现实生活中的门牌号&#xff0c;用于标识服务器在网络中的位置&#xff0c;使其他设备能够通过它与…...

[问题解决]:Unable to find image ‘containrrr/watchtower:latest‘ locally

一&#xff0c;问题 在使用docker安装部署新应用的时候&#xff0c;报错&#xff1a;Unable to find image containrrr/watchtower:latest locally 分析认为是当前docker的资源库里找不到这个软件的镜像&#xff0c;需要配置一个包含这个软件镜像的新的资源库。 二&#xff0…...

【文件上传】阿里云对象存储服务实现文件上传

一、基础 上传到本地&#xff1a; package org.example.controller;import lombok.extern.slf4j.Slf4j; import org.example.pojo.Result; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; imp…...

IPv6代理如何引领下一代网络未来

随着互联网技术的不断发展&#xff0c;IPv6逐渐成为下一代网络协议的核心&#xff0c;替代IPv4已是大势所趋。IPv6代理作为IPv6网络环境下的重要工具&#xff0c;为用户提供了更高效、更安全的网络解决方案。 IPv6代理的定义 IPv6代理是在IPv6网络环境中为处理IPv4转换和其他网…...

Linux——数据链路层

1. 认识以太网 认知&#xff1a;以太网是用于局域网数据通信的协议标准&#xff0c;定义了同一局域网内通过电缆/无线怎么在设备之间传输数据帧。 注&#xff1a;整个网络世界可以具象看出由许许多多的局域网组成&#xff0c; • 家庭中的设备A and 家庭中的设备B and 家庭路由…...

ubuntu 22.04 安装下载

ubuntu 22.04下载安装及相关配置_ubuntu22.04下载-CSDN博客...

深度学习面试八股简略速览

在准备深度学习面试时&#xff0c;你可能会感到有些不知所措。毕竟&#xff0c;深度学习是一个庞大且不断发展的领域&#xff0c;涉及众多复杂的技术和概念。但别担心&#xff0c;本文将为你提供一份全面的指南&#xff0c;从基础理论到实际应用&#xff0c;帮助你在面试中脱颖…...

【深度学习-pytorch篇】1. Pytorch矩阵操作与DataSet创建

Pytorch矩阵操作与DataSet创建 1. Python 环境配置 1.1 安装 Anaconda 推荐使用 Anaconda 来管理 Python 环境&#xff0c;访问官网下载安装&#xff1a; https://www.anaconda.com/download/success 1.2 安装 PyTorch 请根据自己的系统平台&#xff08;Windows/Linux/ma…...

游戏引擎学习第310天:利用网格划分完成排序加速优化

回顾并为今天的内容做个铺垫 昨天我们完成了一个用于排序的空间划分系统&#xff0c;但还没有机会真正利用它。昨天的工作刚好在结束时才完成&#xff0c;所以今天我们打算正式使用这个空间划分来加速排序。 现在我们在渲染代码中&#xff0c;可以看到在代码底部隐藏着一个“…...

数据结构 - 树的遍历

一、二叉树的遍历 对于二叉树&#xff0c;常用的遍历方式包括&#xff1a;先序遍历、中序遍历、后序遍历和层次遍历 。 1、先序遍历&#xff08;PreOrder&#xff09; 先序遍历的操作过程如下&#xff1a; 若二叉树为空&#xff0c;则什么也不做&#xff1b;否则&#xff0…...

时序模型介绍

一.整体介绍 1.单变量 vs 多变量时序数据 单变量就是只根据时间预测&#xff0c;多变量还要考虑用户 2.为什么不能用机器学习预测&#xff1a; a.时间不是影响标签的关键因素 b.时间与标签之间的联系过于弱/过于复杂&#xff0c;因此时序模型依赖于时间与时间的相关性来进行预…...

Java面试实战:从Spring到大数据的全栈挑战

Java面试实战&#xff1a;从Spring到大数据的全栈挑战 在某家知名互联网大厂&#xff0c;严肃的面试官正在面试一位名叫谢飞机的程序员。谢飞机以其搞笑的回答和对Java技术栈的独特见解而闻名。 第一轮&#xff1a;Spring与微服务的探索 面试官&#xff1a;“请你谈谈Spring…...

解决idea与springboot版本问题

遇到以下问题&#xff1a; 1、springboot3.2.0与jdk1.8 提示这个包org.springframework.web.bind.annotation不存在&#xff0c;但是pom已经引入了spring-boot-starter-web 2、Error:Cannot determine path to tools.jar library for 17 (D:/jdk17) 3、Error:(3, 28) java: …...