假设我正在用C编写一个小库——比如说一些数据结构。如果我无法分配内存,该怎么办?
这可能非常重要,例如,我首先需要一些内存来初始化数据结构,或者我正在插入一个键值对,并希望将其封装在一个小结构中。它也可能不那么关键,例如pretty_print
函数,它可以构建内容的良好字符串表示。然而,它通常比你的平均错误更严重——继续下去可能根本没有意义。大量在线使用malloc
的示例,如果返回NULL
,则直接退出程序。我猜很多真实的客户端代码也会这样做——只是弹出一些错误,或者将其写入stderr
,然后中止。(很多实际代码可能根本不检查malloc
的返回值。)
有时返回NULL
是有意义的,但并非总是如此。错误代码(或者只是一些布尔success
值),无论是作为返回值还是输出参数,都可以正常工作,但它们似乎会扰乱或损害API的可读性(再说一遍,这在像C这样的语言中可能是意料之中的事?)。另一种选择是具有某种内部错误状态,调用方随后可以查询,例如使用get_error
函数,但您必须小心线程安全,这可能很容易错过;不管怎样,人们在检查错误方面往往很松懈,如果这是一个单独的函数,他们可能不知道,或者他们可能不会打扰(但我想这是他们的问题)。
(我有时看到malloc
被封装在一个函数中,该函数会再次尝试,直到内存可用…
void *my_malloc(size_t size)
{
void *result = NULL;
while (result == NULL)
result = malloc(size);
return result;
}
但这似乎有点愚蠢,也许很危险。)
处理这个问题的正确方法是什么?
如果分配失败,阻止了向前的进展,那么库代码唯一可接受的解决方案是退出在部分完成的操作中已经进行的任何分配和其他更改,并向调用方返回失败代码。只有调用应用程序才能知道正确的处理方式
- 音乐播放器可能只是中止或返回到初始/停止状态,然后等待用户再次输入
- 字处理器可能需要将当前文档状态的紧急转储存储到恢复文件中,然后中止
- 高端数据库服务器可能需要拒绝并退出整个事务,并向客户端报告
如果你遵循一个经常被建议但倒退的想法,即你的库应该在分配失败时中止调用程序,那么你会有很多程序因此决定不能使用你的库,当分配失败导致他们的宝贵数据被丢弃时,你的程序用户会非常愤怒。
编辑:一些"中止"阵营会对我的回答提出一个反对意见,即在过度调试的系统上,当内核尝试为分配的虚拟内存实例化物理存储时,即使对malloc
的调用看起来成功了,也可能失败。这忽略了这样一个事实,即任何需要高可靠性的人都会禁用过度承诺,以及(至少在32位系统上)分配失败更可能是由于虚拟地址空间耗尽而非物理存储耗尽。
只需以您通常所做的任何方式返回错误。由于我们讨论的是API,您不知道从什么环境调用您,所以只需返回NULL或遵循您已经使用的任何其他错误处理过程。你不想永远循环,因为调用者可能真的不需要内存,他们宁愿知道你无法处理它,或者调用者有一个用户界面,他们可以将错误发送到。
大多数API都会有某种返回值,指示所有函数中的错误,其他API则要求调用方调用一个特殊的"check_error"函数来确定是否存在错误。您可能还想要一个"get_error"函数来返回一个错误字符串,调用者可以选择向用户显示该字符串或将其包含在日志中。它应该是描述性的:"所以API在函数中遇到了一个错误:无法分配内存"。或者其他什么。足够了,当有人收到错误时,他们知道是哪个组件抛出了错误,当他给你发电子邮件时,你就知道到底出了什么问题。
当然,你也可以崩溃,但这会阻止调用方关闭他们可能一直在做的任何其他事情,而且看起来很难看,如果你的代码有一个习惯,即在调用时死亡,而不是返回错误,那么人们会寻找一个不会主动尝试终止程序的库。
对于库,您有两种选择。在没有应用程序协作的情况下,您所能做的几乎就是将错误传递回应用程序。
通过应用程序合作,您可以做得更多。例如,当malloc
返回NULL
时,您可以提供应用程序来注册库调用的回调。您可以向回调传递所需的字节数以及所需的紧急程度。(例如,"将完全无法操作"、"将不得不中止操作"等等。)然后,应用程序作者可以决定是否给您内存。
在应用程序级别,您可以做更多的工作。例如,您可以malloc
一堆内存块,用作"应急池"。如果malloc
失败,您可以从池中释放块,并开始减载、减少缓存或其他任何减少内存消耗的选择。
但是,在库中,除了通知协作应用程序之外,通常不能做太多事情。
用于线性代数函数调用的BLAS标准API使用了与这里给出的"返回错误代码"建议略有不同的方法:它调用特定的文档化函数,然后返回。
现在,该库还提供了这个文档化函数的实现,它打印一条有用的错误消息和(在可能的情况下)堆栈跟踪,然后中止。这是处理事情的一种方式,这意味着普通用户不会因为忘记检查错误代码而遇到奇怪的问题。
然而,作为一个特定的文档功能,这一点的关键在于,这意味着用户也可以选择提供自己的功能实现,这将覆盖默认功能。这个实现可以做很多事情——它可以设置一个全局错误代码,然后用户检查,或者它可以做一些事情,试图清除一些内存并继续。
对于实现和用户来说,这是一种重量级的解决方案,但在明显的错误代码不合适的情况下,它提供了很大的灵活性。
编辑以添加更多细节:BLAS函数是xerbla(或C接口中的cblas_xerbla),期望在链接时覆盖它——假设是静态链接。在Apple的这个头中,还有一些关于如何为动态库调整的相关说明(请参阅文件底部附近的SetBLASParamErrorProc上的注释)——在动态链接的情况下,需要在运行时注册回调。
另请参阅下面注释中"R"的有用注释,了解这种覆盖是如何不幸地全局的,如果用户通过第二个库直接或间接使用您的库,并且用户和第二个库都希望覆盖处理程序,则可能会导致问题。
很难设计软件来干净地处理内存不足的问题并继续进行。很少有真正的应用程序认真尝试做到这一点。作为库作者,唯一合理的做法就是向调用者报告错误。(返回失败;抛出异常;取决于语言等)
您绝对不想为通用库循环和阻塞@R有一个很好的点,如果发生故障,请尝试将状态恢复到其原始状态。
处理内存不足和磁盘空间不足的问题可能需要应用程序各个部分的协调。您可能需要预先分配应急内存。您可能会像预期的那样循环/重试mallocs,但其间会有一些延迟和超时。它确实超出了典型图书馆的范围。
在Java或C#这样的语言中,无法回答的答案通常是"抛出异常!"。
在C中,一种常见的方法是同步处理错误(例如,使用结果代码和/或类似"null"的标志值)。
还可以生成异步信号(很像Java的"异常"…或严厉的"中止()")。使用这种方法,您还可以允许用户安装自定义的"错误处理程序"。
下面是一个使用setjmp/longjmp:的例子
- http://www.di.unipi.it/~nids/docs/longjump_try_tro_catch.html
这里有一些有趣的想法:
- http://blog.staila.com/?p=114
这里有一个很好的讨论使用C回调进行错误处理的利弊:
- 在C中使用回调函数进行错误处理
您可以编写一个首先不需要malloc
的软件——安全关键的东西。
你只需要在执行开始时确保它分配、定义它需要什么,并确保算法不会超过这些障碍。
很难,但并非不可能。