23.并发

目录

  • 一、一些概念
  • 二、进程和线程
    • 2.1 概念
    • 2.2 多线程导致的问题
    • 2.3 使用spawn创建新线程
    • 2.4 线程与move闭包
  • 三、消息传递
    • 3.1 概念
    • 3.2 创建通道
    • 3.3 示例
    • 3.4 其它测试
  • 四、共享状态并发
    • 4.1 互斥器
    • 4.2 Mutex的API
    • 4.3 多线程共享Mutex
      • 1)在多线程之间共享值;
      • 2)多线程和多所有权
      • 3) 原子引用计数
      • 4)RefCell/Rc与 Mutex/Arc的相似性
  • 五、使用Sync和Send Trait的可扩展并发
    • 5.1 send
    • 5.2 Sync

一、一些概念

  • 并发编程(Concurrent programming):程序的不同部分相互独立运行;
  • 并行编译(parallel programming):程序不同部分同时运行;
  • 这里将以上两个概念统称为并发,而不做更加细致的区分;

二、进程和线程

2.1 概念

  • 进程(process):在大部分现代OS中,已执行的代码运行在一个进程中,OS管理多个进程;
  • 线程(Thread):在程序内部同时运行的多个独立部分;

2.2 多线程导致的问题

  • 线程是同时运行的,所以无法预先线程的执行顺序,因此导致以下问题:
    • 竞争状态(Race conditions):多个线程以不一致的顺序访问数据或资源;
    • 死锁(Deadlocks):两个线程相互等待对方所持有的资源;
    • 只在某些情况下发生BUG,很难复现和修复;

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

程序运行结果如下
在这里插入图片描述

  • 主线程和子线程一起运行,当主线程结束之后无论子线程是否结束,子线程都会被强制结束;
  • 使用thread::spawn的返回值的join()方法可以保证线程执行完成;
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所代表的线程运行结束;
    在这里插入图片描述
  • 把join()方法移动到最后一个for之前,像下面这样
fn main(){……handle.join().unwrap();for i in 1..5 {println!("hi number {} from the main thread!", i);thread::sleep(Duration::from_millis(1));}
}

则打印如下
在这里插入图片描述

  • 可见,这是等待子线程运行结束后才会开始运行主线程;

2.4 线程与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);});//	drop(v);  //所有权被转移handle.join().unwrap();
}
  • 通过move关键字,将变量v的所有权进行转移;
  • 转移之后主线程里的v就不能再使用了;

三、消息传递

3.1 概念

  • 消息传递(message passing) 是一个日益流行且安全的并发的方式;
  • 通道(channel) 是Rust中实现消息传递并发的主要工具;
  • 通道由两部分组成:一个发送者(transmitter)和一个接收者(receiver);

3.2 创建通道

  • 使用mpsc::channel函数创建新通道,函数返回一个元组,分别表示发送端和接收端;
  • mpsc是多个生产者,单个消费者(multiple producer,single consumer)的缩写;

3.3 示例

use std::thread;
use std::sync::mpsc;fn main() {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);
}
  • 使用mpsc::channel()创建通道;
  • 使用move将发送者tx的所有权转移到子线程中;
  • 发送端:
    • 发送端的send方法用来获取需要放入通道的值;
    • send方法返回一个Result<T, E>类型,这里发送失败时调用unwrap产生panic;
  • 接收端:
    • 接收端有两个接收方法:recvtry_recv
    • recv会阻塞主线程执行直到从通道中接收一个值,返回Result<T, E>,发送端关闭时收到一个错误;
    • try_recv不会阻塞,立即Result<T, E>

3.4 其它测试

通道与所有权转移

use std::thread;
use std::sync::mpsc;fn main() {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);
}
  • 代码尝试在发送端的send之后将值打印出来,提示所有权已经发生了移动;

在这里插入图片描述

发送多个值并观察接收者在等待

use std::thread;
use std::sync::mpsc;
use std::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(1));}});for received in rx {println!("Got: {}", received);}
}
  • 子线程循环发送一些字符串;
  • 主线程不再显式调用recv函数,而是将rx当作迭代器使用,循环输出接收到的值;
  • 主线程的for循环里没有任何暂停的代码,因此它一直在等待从子线程发送的值;

通过克隆创建多个发送者

use std::thread;
use std::sync::mpsc;
use std::time::Duration;fn main() {let (tx, rx) = mpsc::channel();let tx1 = tx.clone();thread::spawn(move || {let vals = vec![String::from("tx1: hi"),String::from("tx1: from"),String::from("tx1: the"),String::from("tx1: thread"),];for val in vals {tx1.send(val).unwrap();thread::sleep(Duration::from_secs(1));}});thread::spawn(move || {let vals = vec![String::from("tx: more"),String::from("tx: messages"),String::from("tx: for"),String::from("tx: you"),];for val in vals {tx.send(val).unwrap();thread::sleep(Duration::from_secs(1));}});for received in rx {println!("Got: {}", received);}
}
  • 发送之前调用发送端的clone方法再创建一个发送端;
  • 两个发送端者往通道里写入,接收端接收,从下面的结果中可以看到两个发送端的数据被交替接收到了;
    在这里插入图片描述

四、共享状态并发

  • Rust支持通过共享状态实现并发;
  • 通道类似于单所有权:一旦将值传送到通道中,就无法再次使用这个值;
  • 共享内存并发类似多所有权:多个线程可以同时访问同一块内存;

4.1 互斥器

  • 互斥器(mutex)只允许一个线程访问某些数据;
  • 使用互斥器:
    • 在使用数据之前尝试获取互斥锁;
    • 处理完被互斥器所保护的数据之后,必须解锁;

4.2 Mutex的API

  • 通过Mutex::new创建Mutex<T>,这是一个智能指针;
  • 使用lock方法获取锁,此方法会阻塞当前线程,直接获取锁为止;
  • 当拥有锁的线程panic了,lock会失败;
  • lock的返回值是MutexGuard的智能指针,离开作用域时,自动解锁;
use std::sync::Mutex;fn main() {let m = Mutex::new(5);{let mut num = m.lock().unwrap();*num = 6;}println!("m = {:?}", m);
}
  • 调用Mutex::new创建一个Mutex,其中的5是要保护的数据;
  • 调用lock获取锁,使用 unwrap()处理panic的情况;
  • 离开内部作用域后自动解锁;
    在这里插入图片描述

4.3 多线程共享Mutex

1)在多线程之间共享值;

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,初始值是0和空的Vector;
  • 创建10个子线程,将每个线程句柄放到Vector中,线程内部获取要保护的值并加1;
  • 离开子线程作用域后自动释放锁资源;
  • 最后在主线程中打印counter的值;
    在这里插入图片描述
  • value moved into closure here, in previous iteration of loop表示所有权已经在上一次循环中移动过了,因此这里不能再移动;

2)多线程和多所有权

  • Rc<T>智能指针可以拥有多所有者;
  • Mutex<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<T>不能被安全的发送;
  • 之前提过Rc<T>只能用在单线程中;
  • 多线程中使用原子引用计数

3) 原子引用计数

  • 原子引用计数Arc<T>是一个类似Rc<T>并可以安全的用于并发环境的类型;
  • 封装在std::sync::atomic中;
  • Arc<T>Rc<T>的API相同;
use std::sync::{Mutex, Arc};
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

4)RefCell/Rc与 Mutex/Arc的相似性

  • 和Cell家族一样,Mutex<T>提供了内部可变性;
  • 可以使用RefCell<T>改变Rc<T>里的内容;
  • 可以使用Mutex<T>改变Arc<T>里的内容;
  • Mutex<T>有死锁风险

五、使用Sync和Send Trait的可扩展并发

  • Rust语言本身的并发性较少,目前的并发特性都来自于标准库;
  • 无需局限于标准库的并发,可以自己实现;
  • Rust语言中有两个并发概念:std::marker::Syncstd::marker::Send两个trait;

5.1 send

  • Send标记的trait表明所有权可以在线程间传递;
  • 几乎所有的Rust类型都是Send的;
  • Rc<T>没有实现Send,它只适合单线程;
  • 任何完全由Send类型组成的类型也被标记为Send;
  • 除了原始指针之外,几乎所有的基础类型都是Send;

5.2 Sync

  • Sync标记的trait可以安全的被多个线程引用;
  • 如果T是Sync,那么&T就是Send,这意味着引用可以安全的发送到另一个线程;
  • 基础类型都是Sync
  • 完全由Sync类型组成的类型也是Sync,但Rc<T>不是Sync的,RefCell<T>Cell<T>家族也不是Sync的;

手动实现Send和Sync是不安全的

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

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

相关文章

Pikachu靶场--CRSF

借鉴参考 CSRF跨站请求伪造&#xff08;CTF教程&#xff0c;Web安全渗透入门&#xff09;_bilibili pikachu靶场CSRF之TOKEN绕过_csrf token绕过的原理-CSDN博客 CSRF(get) 发现需要登录 查看提示&#xff0c;获取username和password 选择一个用户进行登录 选择修改个人信息 …

基于SpringBoot+Vue汽车配件销售管理系统设计和实现(源码+LW+调试文档+讲解等)

&#x1f497;博主介绍&#xff1a;✌全网粉丝10W,CSDN作者、博客专家、全栈领域优质创作者&#xff0c;博客之星、平台优质作者、专注于Java、小程序技术领域和毕业项目实战✌&#x1f497; &#x1f31f;文末获取源码数据库&#x1f31f; 感兴趣的可以先收藏起来&#xff0c;…

【设计模式-04】原型模式

【设计模式-04】原型模式 1. 概述2. 结构3. 实现4. 案例5. 使用场景6. 优缺点6.1 原型模式的优点6.2 原型模式的缺点 7. 实现深克隆(深拷贝) 1. 概述 原型模式: 用一个已经创建的实例作为原型&#xff0c;通过复制该原型对象来创建一个和原型对象相同的新对象。 2. 结构 原型…

5.树莓派4b+ubuntu18.04(ros版本melodic)+arduino mega自制两轮差速小车,实现建图导航功能

这一节介绍雷达的使用&#xff0c;我们使用的雷达型号是ydlidar x3 1.进入工作空间 cd catkin_ws/src2.下载官方提供的SDK文件 git clone https://github.com/YDLIDAR/YDLidar-SDK.git3.安装cmake sudo apt install cmake pkg-config4.编译和安装 进入YDLidar-SDK文件夹后如…

在线随机密码生成工具

对于运维工作&#xff0c;经常需要生产随机密码&#xff0c;这里介绍一款在线生成密码工具&#xff0c;支持配置密码组合类型&#xff0c;如数字、字母大小写、特殊字符等&#xff0c;还能排除某些特殊字符。 在线随机密码生成工具 https://tool.hiofd.com/random-string-gen…

SLAM Paper Reading和代码解析

最近对VINS、LIO-SAM等重新进行了Paper Reading和代码解析。这两篇paper和代码大约在三年前就读过&#xff0c;如今重新读起来&#xff0c;仍觉得十分经典&#xff0c;对SLAM算法研发具有十分重要的借鉴和指导意义。重新来读&#xff0c;对其中的一些关键计算过程也获得了更新清…

Django 模版过滤器

Django模版过滤器是一个非常有用的功能&#xff0c;它允许我们在模版中处理数据。过滤器看起来像这样&#xff1a;{{ name|lower }}&#xff0c;这将把变量name的值转换为小写。 1&#xff0c;创建应用 python manage.py startapp app5 2&#xff0c;注册应用 Test/Test/sett…

FPGA PCIe加载提速方案

目录 1.bit流压缩 2.flash加载速度 3.Tandem模式 1.bit流压缩 set_property BITSTREAM.GENERAL.COMPRESS TRUE [current_design] 2.flash加载速度 打开bitstream setting&#xff0c;设置SPI的线宽和速率&#xff08;线宽按原理图设置&#xff0c;速率尽可能高&#xff09…

day3-xss漏洞(米斯特web渗透测试)

day3-xss漏洞&#xff08;米斯特web渗透测试&#xff09; XSSXss种类三种反射型1.反射型xss2.存储型xss3.DOM型xss XSS Xss有一部分是前端的有一部分不是前端的&#xff0c;我们来看一下&#xff0c;昨天的HTML注入修复方法应灵活使用。 HTML注入是注入一段HTML&#xff0c;那…

AI大模型之争:通用性与垂直性,哪个更具优势?

文章目录 每日一句正能量前言背景介绍能力分析通用大模型的能力&#xff1a;垂直大模型的能力&#xff1a;差异与互补性分析&#xff1a; 难点探究1. 算力挑战2. 数据挑战3. 算法挑战4. 泛化能力5. 可解释性和透明度6. 伦理和偏见问题7. 成本效益 后记 每日一句正能量 昨天已逝…

汇编快速入门

一.基础知识 1.数据类型 DB&#xff08;Define Byte&#xff0c;字节类型 占位8位bit 1字节&#xff09; 范围&#xff1a;DB可以用来定义&#xff08;无符号、有符号&#xff09;整数&#xff08;包含二、十、十六进制&#xff09;和字符 语法&#xff1a;a DB 数据个数…

增加attention的seq2seq和transformer有什么区别

1.seq2seq是什么 seq2seq 是一个Encoder–Decoder 结构的网络&#xff0c;它的输入是一个序列&#xff0c;输出也是一个序列。Encoder 中将一个可变长度的信号序列变为固定长度的向量表达&#xff0c;Decoder 将这个固定长度的向量变成可变长度的目标的信号序列。   很多自然…

package.json简介

1、package.json简介 通过 npm init 初始化一个项目&#xff0c;会生成3个目录/文件&#xff0c; node_modules, package.json和 package.lock.json。其中package-lock.json文件是为了锁版本。 2、package.json常用属性 1&#xff09;name name是项目的名称&#xff0c;命名…

2024最新1小时零基础编写uniapp和小程序管理后台,基于uniadmin和vue3实现uniapp小程序的网页管理后台

一&#xff0c;创建uniAdmin项目 打开开发者工具Hbuilder,然后点击左上角的文件&#xff0c;点新建&#xff0c;点项目。如下图。 选择uniadmin&#xff0c;编写项目名&#xff0c;然后使用vue3 记得选用阿里云服务器&#xff0c;因为最便宜 点击创建&#xff0c;等待项目创…

示例:WPF中应用DependencyPropertyDescriptor监视依赖属性值的改变

一、目的&#xff1a;开发过程中&#xff0c;经常碰到使用别人的控件时有些属性改变没有对应的事件抛出&#xff0c;从而无法做处理。比如TextBlock当修改了IsEnabled属性我们可以用IsEnabledChanged事件去做对应的逻辑处理&#xff0c;那么如果有类似Background属性改变我想找…

Atcoder Beginner Contest 359

传送门 A - Count Takahashi 时间限制&#xff1a;2秒 内存限制&#xff1a;1024MB 分数&#xff1a;100分 问题描述 给定 N 个字符串。 第 i 个字符串 () 要么是 Takahashi 要么是 Aoki。 有多少个 i 使得 等于 Takahashi &#xff1f; 限制 N 是整数。每个…

探索ChatTTS项目:高效的文字转语音解决方案

文章目录 &#x1f4d6; 介绍 &#x1f4d6;&#x1f4d2; ChatTTS &#x1f4d2;&#x1f4dd; 项目介绍&#x1f4dd; 项目亮点&#x1f4dd; UI &#x1f388; 项目地址 &#x1f388; &#x1f4d6; 介绍 &#x1f4d6; 在AI技术迅速发展的今天&#xff0c;文本到语音&…

指令调度基本概念

概述 为了提高处理器执行指令的并行度&#xff0c;处理器将计算机指令处理过程拆分为多个阶段&#xff0c;并通过多个硬件处理单元&#xff0c;将不同指令处理的前后阶段重叠并行执行&#xff0c;形成流水线(pipeline) 处理器的流水线结构是处理器微架构最基本的要素&#xf…

数据类型 运算符

基本数据类型与引用数据类型的区分 存储内容&#xff1a; 基本数据类型&#xff1a;直接存储实际的数据值&#xff0c;如整数、浮点数、字符等。引用数据类型&#xff1a;存储对象的引用&#xff08;内存地址&#xff09;&#xff0c;而不是对象本身。 内存分配&#xff1a; 基…

本地离线模型搭建指南-本地运行显卡选择

搭建一个本地中文大语言模型&#xff08;LLM&#xff09;涉及多个关键步骤&#xff0c;从选择模型底座&#xff0c;到运行机器和框架&#xff0c;再到具体的架构实现和训练方式。以下是一个详细的指南&#xff0c;帮助你从零开始构建和运行一个中文大语言模型。 本地离线模型搭…