Rust编程中的共享状态并发执行

news2024/11/25 22:33:53

1.共享状态并发

虽然消息传递是一个很好的处理并发的方式,但并不是唯一一个。另一种方式是让多个线程拥有相同的共享数据。在学习Go语言编程过程中大家应该听到过一句口号:"不要通过共享内存来通讯"。

在某种程度上,任何编程语言中的信道都类似于单所有权,因为一旦将一个值传送到信道中,将无法再使用这个值。共享内存类似于多所有权:多个线程可以同时访问相同的内存位置。第十五章介绍了智能指针如何使得多所有权成为可能,然而这会增加额外的复杂性,因为需要以某种方式管理这些不同的所有者。Rust 的类型系统和所有权规则极大的协助了正确地管理这些所有权。作为一个例子,让我们看看互斥器,一个更为常见的共享内存并发原语。

互斥器mutex)是 mutual exclusion 的缩写,也就是说,任意时刻,其只允许一个线程访问某些数据。为了访问互斥器中的数据,线程首先需要通过获取互斥器的 lock)来表明其希望访问数据。锁是一个作为互斥器一部分的数据结构,它记录谁有数据的排他访问权。因此,我们描述互斥器为通过锁系统 保护guarding)其数据。

互斥器以难以使用著称,因为你不得不记住:

  1. 在使用数据之前尝试获取锁。

  2. 处理完被互斥器所保护的数据之后,必须解锁数据,这样其他线程才能够获取锁。

作为一个现实中互斥器的例子,想象一下在某个会议的一次小组座谈会中,只有一个麦克风。如果一位成员要发言,他必须请求或表示希望使用麦克风。一旦得到了麦克风,他可以畅所欲言,然后将麦克风交给下一位希望讲话的成员。如果一位成员结束发言后忘记将麦克风交还,其他人将无法发言。如果对共享麦克风的管理出现了问题,座谈会将无法如期进行!

正确的管理互斥器异常复杂,这也是许多人之所以热衷于信道的原因。然而,在 Rust 中,得益于类型系统和所有权,我们不会在锁和解锁上出错。

2.Mutex<T>的API

作为展示如何使用互斥器的例子,让我们从在单线程上下文使用互斥器开始, 看下面的代码:

use std::sync::Mutex;

fn main() {
    let m = Mutex::new(5);

    {
        let mut num = m.lock().unwrap();
        *num = 6;
    }

    println!("m = {:?}", m);
}

像很多类型一样,我们使用关联函数 new 来创建一个 Mutex<T>。使用 lock 方法获取锁,以访问互斥器中的数据。这个调用会阻塞当前线程,直到我们拥有锁为止。

如果另一个线程拥有锁,并且那个线程 panic 了,则 lock 调用会失败。在这种情况下,没人能够再获取锁,所以这里选择 unwrap 并在遇到这种情况时使线程 panic。

一旦获取了锁,就可以将返回值(在这里是num)视为一个其内部数据的可变引用了。类型系统确保了我们在使用 m 中的值之前获取锁。m 的类型是 Mutex<i32> 而不是 i32,所以 必须 获取锁才能使用这个 i32 值。我们是不会忘记这么做的,因为反之类型系统不允许访问内部的 i32 值。

Mutex<T> 是一个智能指针。更准确的说,lock 调用 返回 一个叫做 MutexGuard 的智能指针。这个智能指针实现了 Deref 来指向其内部数据;其也提供了一个 Drop 实现当 MutexGuard 离开作用域时自动释放锁,为此,我们不会忘记释放锁并阻塞互斥器为其它线程所用的风险,因为锁的释放是自动发生的。

丢弃了锁之后,可以打印出互斥器的值,并发现能够将其内部的 i32 改为 6。

3.在线程间共享Mutex<T>

现在让我们尝试使用 Mutex<T> 在多个线程间共享值。我们将启动十个线程,并在各个线程中对同一个计数器值加一,这样计数器将从 0 变为 10。看下面的代码:

use std::sync::Mutex;
use std::thread;

fn main() {
    let counter = Mutex::new(0);
    let mut handles = vec![];

    for _ in 0..10 {
        let handle = thread::spawn(move || {
            let mut num = counter.lock().unwrap();

            *num += 1;
        });
        handles.push(handle);
    }

    for handle in handles {
        handle.join().unwrap();
    }

    println!("Result: {}", *counter.lock().unwrap());
}

这里创建了一个 counter 变量来存放内含 i32Mutex<T>, 接下来遍历 range 创建了 10 个线程。使用了 thread::spawn 并对所有线程使用了相同的闭包:它们每一个都将调用 lock 方法来获取 Mutex<T> 上的锁,接着将互斥器中的值加一。当一个线程结束执行,num 会离开闭包作用域并释放锁,这样另一个线程就可以获取它了。

在主线程中,我们收集了所有的 join 句柄, 调用它们的 join 方法来确保所有线程都会结束。这时,主线程会获取锁并打印出程序的结果。

编译上面的代码, Rust编译器报了一个错误:

错误信息表明 counter 值在上一次循环中被移动了。所以 Rust 告诉我们不能将 counter 锁的所有权移动到多个线程中。下面来看看如何修复这个错误。

4.多线程和多所有权

我们先尝试将Mutex<T>封装进Rc<T>中并在将所有权移入线程之前克隆Rc<T>,看下面代码:

use std::rc::Rc;
use std::sync::Mutex;
use std::thread;

fn main() {
    let counter = Rc::new(Mutex::new(0));
    let mut handles = vec![];

    for _ in 0..10 {
        let counter = Rc::clone(&counter);
        let handle = thread::spawn(move || {
            let mut num = counter.lock().unwrap();

            *num += 1;
        });
        handles.push(handle);
    }

    for handle in handles {
        handle.join().unwrap();
    }

    println!("Result: {}", *counter.lock().unwrap());
}

再一次编译代码,纳尼, 居然又报了另一个错误, 成年人的崩溃谁能懂:

Rc<Mutex<i32>>` cannot be sent between threads safely`。这个错误编译器告诉我们原因是:`the trait `Send` is not implemented for `Rc<Mutex<i32>>

Rc<T> 并不能安全的在线程间共享。当 Rc<T> 管理引用计数时,它必须在每一个 clone 调用时增加计数,并在每一个克隆被丢弃时减少计数。Rc<T> 并没有使用任何并发原语,来确保改变计数的操作不会被其他线程打断。在计数出错时可能会导致诡异的 bug,比如可能会造成内存泄漏,或在使用结束之前就丢弃一个值。我们所需要的是一个完全类似 Rc<T>,又以一种线程安全的方式改变引用计数的类型。

5.原子引用计数Arc<T>

在Rust标准库中, 提供了一个名为Arc<T>的类型, 这是一个可以安全的用于并发环境的类型, 字母 “a” 代表 原子性atomic),所以这是一个 原子引用计数atomically reference counted)类型, 将代码修改为:

use std::sync::{Arc, Mutex};
use std::thread;

fn main() {
    let counter = Arc::new(Mutex::new(0));
    let mut handles = vec![];

    for _ in 0..10 {
        let counter = Arc::clone(&counter);
        let handle = thread::spawn(move || {
            let mut num = counter.lock().unwrap();

            *num += 1;
        });
        handles.push(handle);
    }

    for handle in handles {
        handle.join().unwrap();
    }

    println!("Result: {}", *counter.lock().unwrap());
}

再次编译代码, 执行结果如下:

这次终于得到结果10, 程序从0数到10, 虽然过程看上去并不明显, 但我们却学到了很多关于Mutex<T>和线程安全的内容。

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Actipro Software WPF Controls 23.1.3

Actipro Software WPF Controls v23.1.3 Actipro Software 为 Microsoft 提供软件组件和 .NET 平台。它位于克利夫兰&#xff0c;重点主要是提供高质量的用户界面软件组件以及客户的过程&#xff0c;以便他们有能力信任&#xff0c;以便为用户应用程序添加强大的功能。自 .NET…

Vscode舒适的主题推荐

1. One Dark Pro与One Dark Pro Darker 感觉配色特别好看&#xff0c;强烈推荐

如何编写一个Perl爬虫程序

要编写一个Perl爬虫程序&#xff0c;首先需要安装LWP::UserAgent模块。你可以使用cpan命令来安装该模块&#xff1a; cpan LWP::UserAgent 安装完成后&#xff0c;可以使用以下代码来编写爬虫程序&#xff1a; use LWP::UserAgent; use HTML::TreeBuilder; my $proxy_host …

【算法与数据结构】46、47、LeetCode全排列I, II

文章目录 一、46.全排列I二、47.全排列II三、完整代码 所有的LeetCode题解索引&#xff0c;可以看这篇文章——【算法和数据结构】LeetCode题解。 一、46.全排列I 思路分析&#xff1a;本题要求是全排列&#xff0c;意味着每次递归的时候startIndex都要从0开始&#xff0c;否则…

基于 Letterize.js + Anime.js 实现炫酷文本特效

如上面gif动图所示&#xff0c;这是一个很炫酷的文字动画效果&#xff0c;文字的每个字符呈波浪式的扩散式展开。本次文章将解读如何实现这个炫酷的文字效果。 基于以上的截图效果可以分析出以下是本次要实现的主要几点&#xff1a; 文案呈圆环状扩散开&#xff0c;扩散的同时…

代驾预约小程序系统源码 :提起预约,避免排队 带完整搭建教程

大家好啊&#xff0c;又到罗峰来给大家分享好用的源码系统的时间了。今天要给大家分享的第一款代驾预约小程序源码系统。传统的代驾服务中&#xff0c;用户往往需要在酒后代驾、长途驾驶等场景下&#xff0c;面对排队等待代驾司机空闲时间的繁琐过程。这不仅浪费了用户的时间和…

excel中的OFFSET函数

介绍 OFFSET函数是确定从基点出发移动后的引用区域。它有5个参数&#xff1a; 第1个参数是引用的参考基点区域第2个参数是移动的行数&#xff0c;正数代表向下移动的行数&#xff0c;负数代表向上移动的行数第3个参数是移动的列数&#xff0c;正数代表向右移动的列数&#xf…

性价比高的台灯推荐,呼声最高的五款护眼台灯推荐

台灯可以说家家必备&#xff01;家中有上学的小孩更是需要一款好台灯&#xff0c;因为看书、写字、做作业都离不开台灯&#xff0c;一款好的台灯不仅会提供明亮的学习环境&#xff0c;而且还能保护视力&#xff0c;预防近视&#xff0c;因此&#xff0c;挑选台灯绝对不可以马虎…

企业计算机服务器中了halo勒索病毒怎么办,halo勒索病毒解密数据恢复

随着科技技术的不断提升&#xff0c;越来越多的企业开始走向数字化办公&#xff0c;让企业的生产运营得到了快速发展&#xff0c;但随之而来的网络安全威胁引起了人们的重视&#xff0c;近期&#xff0c;云天数据恢复中心陆续接到很多企业的求助&#xff0c;企业的计算机服务器…

产品经理天天跑火车,我直接和他闹翻

前言 说起产品经理与程序员&#xff0c;简直就是一对冤家。 程序员觉得产品经理不尊重技术规则&#xff0c;产品经理埋怨程序员不尊重创作用心。 一边互怼&#xff0c;一边还要合作&#xff0c;终于&#xff0c;有人忍不下去&#xff0c;动手了…… ![](https://img-blog.cs…

RFID技术在仓储物流管理中的应用方案

一、方案背景 当前市场竞争日益激烈&#xff0c;提高生产效率、降低运营成本对来说企业至关重要&#xff0c;仓储物流管理在各个行业广泛应用&#xff0c;设计和建立完善的仓储管理流程&#xff0c;提高仓储周转效率&#xff0c;减少运营资金的占用&#xff0c;将冻结的资产转…

第3关:集合操作100

任务描述相关知识编程要求测试说明 任务描述 本关任务&#xff1a;使用 集合操作解决实际问题 相关知识 1.集合并操作符 可转换为SQL 若R,S的属性名不同&#xff0c;可使用重命名使相应列名一致后进行并操作 例如&#xff1a;R(A,B,C) S(D,E,F) select A,B from R union sel…

深入了解鼠标光标的设置过程

有一位读者问了这样一个问题&#xff1a; “为什么鼠标光标的设定绑定在窗口类&#xff0c;而不是窗口上&#xff1f;” 这个问题隐含地假设了光标与窗口类相关联。虽然每个窗口类都有一个关联的光标&#xff0c;但决定使用哪个光标的是窗口。 光标设置过程在 WM_SETCURSOR 消…

用CHAT如何写视频剪辑思路?

问CHAT&#xff1a;我的行业是国学教育&#xff0c;我的工作是视频剪辑师&#xff0c;给我推荐几个剪辑思路 CHAT回复&#xff1a; 作为一个国学教育视频剪辑师&#xff0c;你的主要任务是通过剪辑创作引人入胜、富有教育性的视频。 以下是一些可能对你有帮助的剪辑思路&…

数据结构-散列表

列表&#xff08;Hash Table&#xff09;&#xff0c;又称哈希表&#xff0c;是一种数据结构&#xff0c;特点是&#xff1a;数据元素的关键字与其存储地址直接相关 例&#xff1a;有一堆数据元素&#xff0c;关键字分别为&#xff5b;19&#xff0c;14&#xff0c;23&#xff…

Session、Token、Jwt三种登录方案介绍

新开发一个应用首先要考虑的就是登录怎么去做&#xff0c;登录本身就是判断一下输入的用户名和密码与系统存储的是否一致&#xff0c;但因为Http是无状态协议&#xff0c;用户请求其它接口时是怎么判断该用户已经登录了呢&#xff1f;下面聊一个三种实现方案。 一、传统sessio…

mysql 中with的用法(2)

with递归练习主要用于表里面包含父节点id之类的 查询出对应的省份和市。 建表 CREATE TABLE tb(id VARCHAR(3), pid VARCHAR(3), name VARCHAR(64));INSERT INTO tb VALUES(002, 0, 浙江省); INSERT INTO tb VALUES(001, 0, 广东省); INSERT INTO tb VALUES(003, 002, 衢州市…

模拟实现C语言--strstr函数

模拟实现C语言–strstr函数 文章目录 模拟实现C语言--strstr函数一、strstr函数是什么&#xff1f;二、使用示例三、模拟实现3.1 模拟实现1 一、strstr函数是什么&#xff1f; 在目标字符串中寻找字符串 char * strstr ( const char *str1, const char * str2);根据语法结构&…

navicat导入已有sql表过程

已知我有一个外部的sql表&#xff0c;如图&#xff1a; 想要将其导入navicat并运行。 看一下我的navicat&#xff0c; navicat里已有的数据库并没有library&#xff0c;所以需要建立一个同名的library数据库来存储library.sql文件。 具体步骤如下&#xff1a; 在“本地连接”…

Java 反射设置List属性

使用 Java 反射可以动态地设置对象的属性值&#xff0c;包括 List 类型的属性。以下是一个示例代码&#xff0c;演示如何通过反射设置 List 类型的属性&#xff1a; 假设有一个类 Person&#xff0c;包含一个 List 类型的属性 names&#xff1a; java public class Person { …