尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
Rust 高级特性秀场:spawn 闭包的类型约束
我们已经在本博客的数篇文章中接连讨论了 Rust 的闭包、Fn / FnMut / FnOnce 特质、多线程、Send 特质和 static 约束等高级特性在具备了这些知识储备后我们终于可以来拆解 spawn 闭包的类型约束了这是一个集以上众多高级特性于一身生动诠释 Rust 语言表现力的经典场景。友情提示本文牵涉知识点众多但每一个关键知识点我们都已专门介绍过在它们出现的地方会给出对应链接。本文原文链接https://laurence.blog.csdn.net/article/details/166011287 禁止转载。好文推荐《Rust GPUI 桌面应用开发入门Assets 资源文件的加载与使用》1. 方法签名我们知道thread::spawn()用于孵化一个单独的线程用户将打算在线程中执行的代码写成一个闭包并传给这个函数就完成了一个多线程任务得益于 Rust 诸多高级特性的支持这种线程的定义和执行方式简洁而优雅更重要的是它还提供了诸多的线程安全保护机制除了始终在发挥作用的所有权和借用检查之外这里不是单单指它们在内存安全中的常规作用更多的是指它们在线程安全上发挥的作用具体可参考《无为而治Rust 多线程的处世哲学》的第 2 节还有一部保护机制就来自于我们今天要介绍的闭包类型约束上。让我们正式切入主题看一下thread::spawn()方法的签名pubfnspawnF,T(f:F)-JoinHandleTwhereF:FnOnce()-TSendstatic,T:Sendstatic我们重点关注的是对闭包类型 F 的约束F: FnOnce() - T Send static这一行容易被误读Send static不是返回值而是对 F 的类型约束的一部分正确的分组是F:FnOnce()-TSendstatic// ^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^// F 的第一个 bound F 的另外两个 bound这一行同时声明了三件事•F 是一个无参数、返回类型为 T 的一次性闭包即FnOnce() - T部分•F 必须是一个 Send即F 跨线程 move 是线程安全的•F 必须是一个static类型即F 不得含有任何外部引用static引用除外下图是对以上三部分声明的详细解释我们将在接下来的章节逐一解释这三个约束。2. 闭包的类型约束FnOnce() - T我们先来看 F 的第一个类型约束FnOnce() - T它明确地告诉了我们 spawn 函数需要一个什么样的函数。这里先消除一个误解spawn 并没说只能接受闭包FnOnce() - T是对函数类型的描述实际上我们完全可以将一个函数传给 spawn就像这样usestd::thread;fnmain(){fnf(){println!(Hello, world!);}lethandlethread::spawn(f);handle.join().unwrap();}只是因为FnOnce() - T限制了函数不可以有参数这就直接回绝了绝大多数函数以函数指针或函数项的形式传入 spawn 的可能而闭包则不受此限制因为闭包可以通过自由变量引入外部值不必非得使用参数这就是为什么人们几乎总是给 spawn 传入一个闭包的原因。很多人在初次看到F: FnOnce() - T时会误认为 spawn 只接受一次性闭包并认定这是最严格的闭包约束。殊不知是完全搞反了实际上这是最宽松的约束只要参数和返回值的类型匹配你可以将任何函数和闭包传给 spawn不管这个闭包是否捕获自由变量以及捕获的是不可变引用、可变引用还是值都可以传给 spawn当然还有其他的约束会起作用比如static但这是另一个话题。如果你对此感到诧异或无法理解那说明你对 Fn、FnMut、FnOnce 三个 trait 的理解还不够透彻推荐你阅读《你理解的 Fn、FnMut、FnOnce 可能从一开始就错了…》。3. 闭包的类型约束SendSend 约束与线程息息相关最表层的解释是如果一个类型要 move 进线程中它必须是 Send 的。因为只有被标记为 Send 的类型才可以安全地在线程间 move。这里有两个更深入的问题需要解释一个是到底什么样的类型可以被标记为 Send另一个是为什么要求闭包得是 Send 的我们先回答第二个问题要求闭包必须是 Send 的原因是闭包可能会捕获外部值编译器会把这个值封装进闭包对应的匿名结构体中对闭包施加 Send 约束的目的其实是通过自动传导机制把 Send 约束传播到闭包捕获的外部值上因为此时闭包是结构体它捕获的值是结构体的一个字段对结构体的 Send 约束会自动作用到字段上也就是说如果一个闭包捕获了一个不是 Send 的值比如一个 Rc 实例那 spawn 会拒绝这个闭包报类型不匹配错误至于到底什么样的类型可以被标记为 Send这是一个非常复杂的话题我们已经在《万物皆可多线程深入解读 Rust 的 Send 和 Sync》一文中把这个问题解释得非常透彻了。解释这个问题通常会用 Rc 类型作为示例来解释因为 Rc 是一个非常典型的非 Send 类型但是要完全理解 Rc 为什么是非 Send 类型又要对共享所有权有清晰的认识于是我们又专门撰写了《Rust 所有权进阶必读为什么 Rc 共享所有权是个伪概念》来进行解释。4. 闭包的类型约束static准确地说static是生命周期约束但是生命周期约束也是泛型类型约束的一种。我们曾在《困惑是正常的,static 生命周期约束本来就不简单》一文中系统地介绍过它而 spawn 闭包的这个static约束是一个非常“妙”的案例我们曾在《无为而治Rust 多线程的处世哲学》一文中专门点评过这个static约束由于闭包中的代码会在一个新线程中执行如果闭包捕获了一个外部值的引用那闭包中的引用和外部值谁先失效就变成了一件不确定的事情。如果是单线程环境靠所有权和借用检查分析静态代码它们最后一次使用的地方是可以确定的但是在多线程环境中如果值和引用分属于两个线程中那谁先失效确实是无法预判的一旦另一个线程中的值先于当前线程中的引用失效那当前线程中的引用就是“悬垂引用”了。怎么才能避免这种情况呢spawn 的做法就是给闭包添加上static约束禁止闭包依赖外部引用static引用除外。这里顺带解释一个偶尔会有人使用的概念短期借用、短生命周期引用其实它们的意思就是除static引用之外的所有引用因为每次都说禁止依赖引用static引用除外会比较麻烦所以有人把排除掉static引用后的引用类型叫短期借用或短生命周期引用这个“短”也不是真的有多短只是相对于static而言的。我们前面提到过闭包会被编译成匿名结构体它捕获的外部值和引用都在这个结构体中如果捕获了引用结构体必须要添加生命周期约束保证结构体和它捕获的引用的生命周期一致下面的示例代码分别展示了捕获不可变和可变引用闭包时生成的匿名结构体的样子• 捕获不可变引用的闭包/* 闭包 */letxString::from(hello);letc||println!({},x);/* 编译器生成的匿名结构体伪代码*/structClosurea{x:aString,// 字段是引用带生命周期a}• 捕获可变引用的闭包/* 闭包 */letmutx0;letmutc||x1;/* 编译器生成的匿名结构体伪代码*/structClosurea{x:amuti32,// 字段是可变引用}不管是捕获不可变引用的闭包还是可变引用的闭包因为它们捕获了引用所以结构体必须添加生命周期标注以确保它和被捕获引用的生命周期一致但是现在由于多线程导致的不确定性spawn 给闭包添加上了static约束实际上也就是给这个结构体添加上了static约束这个约束会自动传导给闭包中的每一个字段那就意味着结构体不得含有引用类型(static引用除外考虑到并不是什么值都能借出static引用所以实际上这一“招”直接把闭包捕获外部引用的路给“堵死”了但是闭包总是需要从环境中获得类似参数性质的数据如果不允许捕获引用那唯一可行的做法就是把所有依赖的引用的值全部 move 进闭包为什么把依赖到的引用的值 move 进闭包后就不违反static约束了呢因为移动进闭包里之后闭包捕获的就是值了不再依赖任何引用没有了引用static约束就等于被“架空”了。下面的代码演示了这种关系/* 闭包 */letxString::from(hello);letcmove||println!({},x);/* 编译器生成的匿名结构体伪代码*/structClosure{x:String,// 字段是完整的值不是引用。所有权 move 进结构体}至此spawn 闭包的类型约束F: FnOnce() - T Send static就全部介绍完了最后我们再来总结一下 spawn 到底需要什么样的闭包① FnOnce 型闭包注意Fn 继承自 FnOnce无任何外部依赖的 Fn 闭包或函数也满足条件② 不得依赖任何非static引用③ 若确实需要外部依赖可使用 move 关键字将依赖的值直接移动到闭包内且值必须是 Send 的
RELATED

相关推荐

智能体工作流工程化:从8000个AI Bot看低代码规模化生产

智能体工作流工程化:从8000个AI Bot看低代码规模化生产

1. 项目本质还原:这不是“造AI”,而是构建可复用的智能体工作流“央视点赞!南开大学10天造了8000个AI智能体”——这个标题在社交平台刷屏时,我第一反应不是兴奋,而是皱眉。作为带过三届AI工程实践课的从业者&#xff…

📅 2026/9/25 18:41:46
档案库房温湿度物联网方案:边缘自治型智能管控实践

档案库房温湿度物联网方案:边缘自治型智能管控实践

1. 这不是又一个“智能改造”口号,而是档案管理员每天擦汗时的真实困境你见过凌晨三点的档案库房吗?不是影视剧里那种泛着冷光的科幻场景,而是闷热、凝滞、带着纸张微酸气味的密闭空间。我陪某省级档案馆做系统升级时,在恒温恒湿机…

📅 2026/9/25 18:41:46
IndexedDB 本地数据库

IndexedDB 本地数据库

5.4 IndexedDB 本地数据库IndexedDB 是浏览器原生提供的事务型结构化本地数据库,专为解决 Web Storage 容量上限低、仅支持字符串存储、查询能力弱的问题。它支持大容量存储、索引查询、事务处理,能够存储海量结构化业务数据与二进制资源,是复…

📅 2026/9/25 18:36:46
MORE NEWS

更多资讯

📰

APM架构拆解:22个模块的install流水线如何实现“一条命令处处复现”

APM架构拆解:22个模块的install流水线如何实现“一条命令处处复现” 【免费下载链接】apm Agent Package Manager 项目地址: https://gitcode.com/gh_mirrors/apm10/apm APM(Agent Package Manager) 的 apm install 是整条产品体验的核…

📰

选型思考:纯计算引擎 vs 一体化数据平台,实时项目该如何抉择

摘要在建设实时数仓、数据交换项目时,很多团队会选择以Flink为核心,搭配Kafka、各类CDC工具组装整套链路。但在生产落地过程中,经常遇到组件繁多、故障点多、乱序丢数、状态膨胀、运维门槛高等现实问题。本文结合项目实践,对比纯流…

📰

机器人不接受迟到的答案!Thor 芯片上,Pi0.5推理被干到了26ms,10.7倍加速。

278ms。 这个时长对于人类对时间的认知来说,几乎是一瞬间。同时,它也是Pi0.5 使用 OpenPI 基线在端侧完成一次推理的等待时间。 然而对于机器人来说,这个时间还是太长了。 从指令进入系统,到机器人开始动作,中间的每…

📰

TCP三次握手与四次挥手的真实世界:从协议原理到故障排查

1. 为什么你看到的“三次握手”从来不是三步,而“四次挥手”也从不按剧本走?TCP连接管理这件事,我带过十几届实习生,每次讲到三次握手和四次挥手,总有人盯着Wireshark里抓到的包发愣:“老师,这明…

📰

Windows 10截图保存路径与管理全指南

1. Windows 10截图后文件“消失”的真相:不是丢了,是藏得比你想象的还深你按下Win PrtSc,屏幕一闪,听到那声熟悉的“咔嚓”快门音——心里一松:“截好了。”可翻遍桌面、文档、图片文件夹,甚至把整个C盘按…

📰

DeepSeek之后,又一国产AI爆火!用TaoToken统一Key接入AI Agent的配置实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬