在Rust中使用多线程并发运行代码

1.Rust线程实现理念

在大部分现代操作系统中,已执行程序的代码在一个 进程process)中运行,操作系统则会负责管理多个进程。在程序内部,也可以拥有多个同时运行的独立部分。这些运行这些独立部分的功能被称为 线程threads)。例如,web 服务器可以有多个线程以便可以同时响应多个请求。

将程序中的计算拆分进多个线程可以改善性能,因为程序可以同时进行多个任务,不过这也会增加复杂性。因为线程是同时运行的,所以无法预先保证不同线程中的代码的执行顺序。这会导致诸如此类的问题:

  • 竞态条件(Race conditions),多个线程以不一致的顺序访问数据或资源。

  • 死锁(Deadlocks),两个线程相互等待对方,这会阻止两者继续运行。

  • 只会发生在特定情况且难以稳定重现和修复的 bug。

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

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

2.使用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));
    }
}

注意当 Rust 程序的主线程结束时,新线程也会结束,而不管其是否执行完毕。这个程序的输出可能每次都略有不同,不过它大体上看起来像这样:

thread::sleep 调用强制线程停止执行一小段时间,这会允许其他不同的线程运行。这些线程可能会轮流运行,不过并不保证如此:这依赖操作系统如何调度线程。在这里,主线程首先打印,即便新创建线程的打印语句位于程序的开头,甚至即便我们告诉新建的线程打印直到 i 等于 9,它在主线程结束之前也只打印到了 5。

如果运行代码只看到了主线程的输出,或没有出现重叠打印的现象,尝试增大区间 (变量 i 的范围) 来增加操作系统切换线程的机会。

3.使用join等待所有线程结束

由于主线程结束,上面演示的代码大部分时候不光会提早结束新建线程,因为无法保证线程运行的顺序,甚至不能实际保证新建线程会被执行!

可以通过将 thread::spawn 的返回值储存在变量中来修复新建线程部分没有执行或者完全没有执行的问题。thread::spawn 的返回值类型是 JoinHandleJoinHandle 是一个拥有所有权的值,当对其调用 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)线程意味着阻止该线程执行工作或退出。因为我们将 join 调用放在了主线程的 for 循环之后,编译这段代码后运行结果如下:

这两个线程仍然会交替执行,不过主线程会由于 handle.join() 调用会等待直到新建线程执行完毕。

不过如果将 handle.join() 移动到 mainfor 循环之前会发生什么呢,看下面的代码:

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));
        }
    });
​
    handle.join().unwrap();
​
    for i in 1..5 {
        println!("hi number {} from the main thread!", i);
        thread::sleep(Duration::from_millis(1));
    }
}

代码编译执行后结果如下:

主线程会等待直到新建线程执行完毕之后才开始执行 for 循环,所以输出将不会交替出现。

因此,将join放在代码的不同地方, 将会影响线程是否同时执行。

4.将move闭包与线程一起使用

move 关键字经常用于传递给 thread::spawn 的闭包,因为闭包会获取从环境中取得的值的所有权,因此会将这些值的所有权从一个线程传送到另一个线程。

为了在新建线程中使用来自于主线程的数据,需要新建线程的闭包获取它需要的值, 下面的代码展示了一个尝试在主线程中创建一个 vector 并用于新建线程的例子,不过这么写还不能工作, 代码如下:

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

闭包使用了 v,所以闭包会捕获 v 并使其成为闭包环境的一部分。因为 thread::spawn 在一个新线程中运行这个闭包,所以可以在新线程中访问 v。然而当编译这个例子时,会得到如下错误:

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

看一段比较极端情况的代码:

use std::thread;
​
fn main() {
    let v = vec![1, 2, 3];
​
    let handle = thread::spawn(|| {
        println!("Here's a vector: {:?}", v);
    });
​
    drop(v); // 坏事儿了!
​
    handle.join().unwrap();
}

如果 Rust 允许这段代码运行,则新建线程则可能会立刻被转移到后台并完全没有机会运行。新建线程内部有一个 v 的引用,不过主线程立刻就使用drop丢弃了v。接着当新建线程开始执行,v 已不再有效,所以其引用也是无效的。

为了修复上面的编译错误, 我们可以根据编译器给予我们的help尝试修正一下,如图:

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

修正后的代码如下:

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

编译运行试一下:

看起来没问题,那么以这个成功的经验, 修改那段极端情况的代码如下:

use std::thread;
​
fn main() {
    let v = vec![1, 2, 3];
​
    let handle = thread::spawn(move || {
        println!("Here's a vector: {:?}", v);
    });
​
    drop(v); // 坏事儿了!
​
    handle.join().unwrap();
}

再次编译一下看看结果如何:

Rust编译器依然没有放行, 这个修复行不通。

如果为闭包增加 move,将会把 v 移动进闭包的环境中, 因此将不能在主线程中对其调用 drop 了, Rust 的所有权规则又一次帮助我们杜绝了隐患。因为 Rust 是保守的并只会为线程借用 v,这意味着主线程理论上可能使新建线程的引用无效。通过告诉 Rust 将 v 的所有权移动到新建线程,我们向 Rust 保证主线程不会再使用 v。如果对其作出同样的move修改, 那么当在主线程中使用 v 时就会违反所有权规则。 move 关键字覆盖了 Rust 默认保守的借用,但它不允许我们违反所有权规则。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/125466.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

竞赛选题 深度学习猫狗分类 - python opencv cnn

文章目录 0 前言1 课题背景2 使用CNN进行猫狗分类3 数据集处理4 神经网络的编写5 Tensorflow计算图的构建6 模型的训练和测试7 预测效果8 最后 0 前言 🔥 优质竞赛项目系列,今天要分享的是 🚩 **基于深度学习猫狗分类 ** 该项目较为新颖&a…

GUI:贪吃蛇

以上是准备工作 Data import javax.swing.*; import java.net.URL;public class Data {public static URL headerURLData.class.getResource("static/header.png");public static ImageIcon header new ImageIcon(headerURL);public static URL upURLData.class.getR…

【vector题解】连续子数组的最大和 | 数组中出现次数超过一次的数字

连续子数组的最大和 连续子数组的最大和_牛客题霸_牛客网 描述 输入一个长度为n的整型数组array,数组中的一个或连续多个整数组成一个子数组,子数组最小长度为1。求所有子数组的和的最大值。 要求:时间复杂度为 O(n),空间复杂度为 O(n) 进…

Python爬虫框架Scrapy:实现高效数据抓取

目录 一、引言 二、Scrapy框架概述 1、Scrapy框架特点 2、Scrapy框架结构 三、Scrapy框架的使用 1、安装Scrapy框架 2、创建Scrapy项目 3、创建爬虫 4、运行爬虫 四、Scrapy框架常见问题及解决方案 1、请求被网站封禁 2、处理动态加载的页面 3、避免被网站检测到爬…

C#查看启用或关闭的Windows功能

通过命令查看启用或关闭的Windows功能,以管理员身份打开powershell,输入命令get-windowsoptionalfeature -online 得出结果如下: 如果使用C#查看,需要先安装System.Management 代码如下: private void isInstall() …

pinpoint监控tomcat应用,页面显示No data collected

pinpoint安装部署教程大家都可以搜到。这里就不说了。单说一下 页面没有数据的情况。 部署环境,pinpoint安装部署在A服务器上。现在是在C、D、E、F……linux机器上安装pinpoint-agnet 1. 将文件 pinpoint-agent-1.8.5.tar.gz 上传到 服务器C、D、E、F…… 2. 解压…

MQTT协议消息代理服务公网远程连接

文章目录 前言1. Linux 搭建 Mosquitto2. Linux 安装Cpolar3. 创建MQTT服务公网连接地址4. 客户端远程连接MQTT服务5. 代码调用MQTT服务6. 固定连接TCP公网地址7. 固定地址连接测试 前言 Mosquitto是一个开源的消息代理,它实现了MQTT协议版本3.1和3.1.1。它可以在不…

Spring Cloud 微服务入门篇

文章目录 什么是微服务架构 Microservice微服务的发展历史微服务的定义微小的服务微服务 微服务的发展历史1. 微服务架构的发展历史2. 微服务架构的先驱 微服务架构 Microservice 的优缺点1. 微服务 e Microservice 优点2. 微服务 Microservice 缺点微服务不是银弹:…

C语言---插入排序、希尔排序、冒泡排序、选择排序、快速排序简单介绍

文章目录 插入排序希尔排序冒泡排序选择排序快速排序 本文主要介绍用C语言实现的一些排序方法,有插入排序、希尔排序、冒泡排序、选择排序和快速排序,文章中给出的例子都是按照升序排列的。 插入排序 若数组只有一个元素,自然不用排序&#…

和数链“分布式存储”技术结合隐私计算让数据更安全

存储是IT业的核心技术,全球存储行业历经半个世纪的洗礼,在技术和需求相互促进的演变下沧桑变幻,经历桌面级存储、企业级存储、云存储多次迭代变迁。 目前的存储方式主要是“大数据中心”等集中式存储,随着数据规模和复杂度的迅速…

如何用Java实现一个基于机器学习的情感分析系统,用于分析文本中的情感倾向

背景:练习两年半(其实是两周半),利用工作闲余时间入门一下机器学习,本文没有完整的可实施的案例,由于知识体系不全面,目前代码只能运行,不能准确的预测 卡点: 1 由于过…

Java自学第6课:电商项目(2)

1 创建工具类并连接数据库 在工程src右键单击new,新建util包 再创建DBUtil类 数据库交互需要有数据库支持的包,这是官方给出的类库。 先声明1个代码块 // 静态代码块 只加载1次static{try {Class.forName("com.mysql.jdbc.Driver");} catch (…

Kotlin文件和类为什么不是一对一关系

在Java中,一个类文件的public类名必须和文件名一致,如何不一致就会报异常,但是在kotlin的文件可以和类名一致,也可以不一致。这种特性,就跟c有点像,毕竟c的.h 和 .cpp文件是分开的。只要最终编译的时候对的…

无人机红外相机的畸变矫正

在项目开展过程中,发现大疆M30T的红外相机存在比较明显的畸变问题,因此需要对红外图像进行畸变矫正。在资料检索过程中,发现对红外无人机影像矫正的资料较少,对此,我从相机的成像原理角度出发,探索出一种效…

史上第一款AOSP开发的IDE (支持Java/Kotlin/C++/Jni/Native/Shell/Python)

ASFP Study 史上第一款AOSP开发的IDE (支持Java/Kotlin/C/Jni/Native/Shell/Python) 类似于Android Studio,可用于开发Android系统源码。 Android studio for platform,简称asfp(爱上富婆)。 背景&下载&使用 背景 由…

2022最新版-李宏毅机器学习深度学习课程-P34 自注意力机制类别总结

在课程的transformer视频中,李老师详细介绍了部分self-attention内容,但是self-attention其实还有各种各样的变化形式: 一、Self-attention运算存在的问题 在self-attention中,假设输入序列(query)长度是N…

leetcode:LCP 11. 期望个数统计(python3解法)

难度:简单 某互联网公司一年一度的春招开始了,一共有 n 名面试者入选。每名面试者都会提交一份简历,公司会根据提供的简历资料产生一个预估的能力值,数值越大代表越有可能通过面试。 小 A 和小 B 负责审核面试者,他们均…

Python克隆单个网页

网上所有代码都无法完全克隆单个网页,不是Css,Js下载不下来就是下载下来也不能正常显示,只能自己写了,记得点赞~ 效果如图: 源码与所需的依赖: pip install requests pip install requests beautifulsoup4…

Zigbee—网络层地址分配机制

🎬慕斯主页:修仙—别有洞天 ♈️今日夜电波:孤雏 0:21━━━━━━️💟──────── 4:14 🔄 ◀️ ⏸ ▶️ ☰ 💗关注…

CSS特效004:hover图片,显示文字或附加层

css实战中,时常会碰见鼠标放在某个区块上,显示出一段文字或者其他附加信息。思路是利用position的层叠关系,将文字层放在图片的上面,display:none; hover的时候层 display:block。 效果图 源代码 /* * Author: 大剑师…