ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Rust 高级特性秀场:spawn 闭包的类型约束

2026/9/25 18:41:38 拓冰建站 浏览量
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 的