为什么`Box::new`不返回`Option`或`Result`?

28

我不明白为什么Box::new不返回OptionResult

由于内存不是无限的,或者其他情况可能发生,因此分配可能会失败; 在这种情况下会发生什么行为? 我找不到任何相关信息。


2
如果你的堆空间用尽了,那么你肯定有更大的问题。我想不出任何情况下你不想在这个时候恐慌。 - Peter Hall
6
有时您希望处理服务器错误,以便为其他客户端继续保持运行状态,或者在退出之前需要进行清理。 - Stargateur
8
我不确定我想要在分配时处理那个错误。这属于意外和灾难性异常的范畴,很难从中恢复。以你提到的服务器请求为例,更好的做法是在处理有问题的请求时使用catch_unwind,以便之后可以继续接受新的请求。 - Peter Hall
3
根据默认OOM处理程序的源代码,@Stargateur,它只会终止进程。您需要使用Nightly Rust来更改OOM行为。 - red75prime
1
从失败的大型分配(例如多兆字节向量)中恢复通常是可能的。从小型分配(例如盒子)中恢复很少发生。 - CodesInChaos
显示剩余5条评论
4个回答

37

更一般的形式是内存不足时应该怎么办?

处理OOM有许多困难:

  • 检测它,
  • 从中恢复(优雅地),
  • 将其优雅地整合到语言中。

第一个问题是检测它。今天许多操作系统默认使用交换空间。在这种情况下,您的进程实际上在OOM出现之前就已经遇到了麻烦,因为开始使用交换空间会显着减慢进程速度。其他操作系统将在更高的进程需要更多内存时(OOM killer)杀死低优先级进程,或者承诺更多内存,希望它不会被使用或将在必要时可用(过度承诺),等等...

第二个问题是恢复。在进程级别上,唯一恢复的方法是释放内存……同时不分配任何内存。这并不像听起来那么容易,例如,并没有保证惊慌和展开不需要分配内存(例如,如果不小心地存储恐慌消息,则可能会进行分配)。这就是为什么当前的rustc运行时默认中止OOM上。

第三个问题是语言集成:内存分配无处不在。任何使用BoxVecString等的操作……因此,如果你避免了恐慌路线,而是使用Result路线,你需要调整几乎所有的变异方法签名来解决这种失败,这将在所有接口中传播。

最后,需要处理内存分配失败的领域通常不允许进行内存分配。例如,在关键嵌入式软件中,所有内存都是预先分配的,并且有一个证明,说明不会超过所分配的内存需求。这很重要,因为这意味着只有极少数情况下允许动态内存分配并且必须优雅地处理其失败。
此时,人们只能想知道应该花费多少“复杂度预算”以及这将给那些不关心的99%程序带来多少复杂性。

3
图像处理可以从不可靠的内存分配中获益。一个未压缩的16K HDR 图像大约需要2GB 的内存,而且没有办法检查是否分配成功。 - red75prime
10
@red75prime说:“哦,我不是说它从来没有用处,我是说它很少有用。因此,不必担心它是正确的默认值,但确实有另一种处理易错分配(Box::try_new()?)的方式会更好。”请为他翻译成通俗易懂的中文。 - Matthieu M.
1
一堆容易出错的函数与可能会引发恐慌的函数并行存在似乎不是一个优雅的解决方案,考虑到你提到的稀有性。也许可以使用类似于 fn try_alloc<T, F: FnOnce() ->T>(f: F) -> Option<T> 的方式来解决? - red75prime
你能否补充一下这个错误在 Rust 中的行为,来完善你的回答?有哪些文档提到了这个问题?或者 Rust 是故意让这种未定义的行为存在的吗? - Stargateur
1
“过度承诺”行为有点鸡生蛋的问题。过度承诺在某种程度上存在是因为程序员通常没有实现优雅的方式来响应内核告诉你它无法提供更多内存的情况,因此,默认行为是始终成功。您可以设置一个内核标志 vm.overcommit_memory=2,这会导致内核更愿意在您用尽内存时告诉您。https://serverfault.com/questions/606185/how-does-vm-overcommit-memory-work#606193 - Kent Fredric
显示剩余7条评论

12
我发现Rust开发人员在liballoc中的一些低级函数没有返回Option的通讯如下:PR#14230。尤其是以下部分解释了其中的一些原因:
huonw: Hm... 应该不会触发任务失败,最底层库吧?我们计划让任何较低级别的库返回Option或其他什么东西吗?
alexcrichton: 我发现要触发任务失败很常见,比我最初意识到的要多得多。我还发现所有上下文都有某种形式或概念上的失败,尽管它并不总是任务失败。
huonw: 我想从任务失败不可恢复的角度考虑,即更高级别的库可以自由地失败,但最基本的构建模块不应该这样做,以便人们可以根据自己的意愿处理问题(即使只是手动触发任务失败)。如果liballoc没有设计成最低级别的分配库,则失败是可以接受的。(顺便说一下,我认为您可能误解了我的评论,因为我没有谈论libcore,只是liballoc。)
alexcrichton: 哎呀,抱歉! 我相信核心分配器接口(位于liballoc中)将被规定为不会失败!(),只有它们之上的原语(例如,box运算符)。
或许我们有一天可以扩展盒子语法以允许返回Option来适应这种用例,因为我肯定想能够重新使用这些代码!

3
也许我们可以将盒子语法扩展,以便有一天可以返回 Option ,以适应这种用例,因为我确实希望能够重用这段代码!也许我应该问问他们 :p. - Stargateur
1
@Stargateur 你可能得不到你想要的,但几乎肯定会得到友好的反馈! - Kyle Strand

6
这是一项语言设计决策。你不仅要考虑单个操作的逻辑(例如Box::new),还要考虑它对语言人性化的影响。如果我们使用Return机制处理内存分配错误,那么这些错误将几乎无处不在地开始浮出水面。即使该方法当前未在堆上分配任何内存,将来也可能会这样做。突然间一个简单的实现更改就会被卡住,因为你必须改变API,而语义版本控制意味着一个重大发布。所有这些都只为了一个小好处,因为在交换和内存杀手存在的情况下,内存耗尽处理并不是非常可靠或有用的(通常你应该在出现内存耗尽错误之前停止分配内存)。
这个问题在reddit上讨论得很多,可以参见这里这里
我看到的其中一个提出的解决方案是将内存耗尽视为panic,展开并终止相应的任务。

1
即使该方法当前不在堆上分配任何内存,它将来可能会采用这种方式。我不同意。您可以选择让函数不返回错误或选项,因此决定它永远不会失败。如果您一开始设计得不好,那么这是您的问题。在这种情况下,您可以自己触发 panic,以便不会出现破坏性更改(如果您想要)。感谢 Reddit 链接。 - Stargateur
4
您说得有道理,但这只是一个细微的问题。在大多数软件中,堆分配非常普遍,考虑它是否需要在接口设计中考虑会给设计带来压力。 - ArtemGr
正如其他人所评论的那样,OOM不会引发panic,因此您无法解开它,不幸的是 :/ - Peter Hall
请参考以下链接:https://www.reddit.com/r/rust/comments/ms2nl7/linus_torvalds_concerns_about_panics_in_rust_code/ - ArtemGr

2

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接