我个人不同意永远不要在代码中使用 goto 的传统智慧。在某些情况下,我发现它不仅方便,而且是最佳实践。最常见的情况是 goto cleanup。考虑以下情况
没有 goto
void f(void) {
void *a = NULL;
void *b = NULL;
void *c = NULL;
a = malloc(32);
//...
if(cond1) {
free(a)
return;
}
b = malloc(64);
//...
if(cond2) {
free(a);
free(b);
return;
}
c = malloc(128);
//...
free(a);
free(b);
free(c);
}
使用 goto
void f(void) {
void *a = NULL;
void *b = NULL;
void *c = NULL;
a = malloc(32);
//...
if(cond1) goto cleanup;
b = malloc(64);
//...
if(cond2) goto cleanup;
c = malloc(128);
//...
cleanup:
if(a) free(a);
if(b) free(b);
if(c) free(c);
}
我们无需在满足条件时跟踪哪些指针需要释放,只需跳转、释放分配的任何内容并返回即可。在我看来,这种设计更简洁,不易出错,但我理解为什么其他人反对它。
最近,我们想引入错误报告来处理在评估表达式时发生的失败。例如,评估静态表达式 toUpper(5) 将失败,因为 toUpper 函数期望其参数为字符串。如果未满足此假设,toUpper 应该抛出异常。
SIValue toUpper(SIValue v) {
SIType actual_type = SI_TYPE(v);
if(actual_type != SI_STRING) {
const char *actual_type_str = SIType_ToString(actual_type);
raise("Type mismatch: expected string but was %s", actual_type_str);
}
}
不幸的是,C 没有像许多其他高级语言那样提供内置的异常机制。
if(cond) {
raise Exception("something went wrong")
}
我们想要的是 try catch 逻辑
try {
// Perform work which might throw an exception
work();
} catch (error *e) {
reportError(e);
}
这种设计的优点是,无论异常是在调用 work 的执行路径中的哪个位置抛出的,堆栈都会自动恢复,我们将在 catch 块中恢复执行。
在我们的例子中,函数 work 被调用 ExecutionPlan_Execute 所取代,该函数实际上评估查询执行计划。从这一点开始,我们必须准备好遇到异常,但是 ExecutionPlan_Execute 在解开和深入时所采取的路径,请考虑以下调用堆栈
redisgraph.so!QueryCtx_SetError (./src/query_ctx.c:78)
redisgraph.so!_AR_EXP_ValidateInvocation (./src/arithmetic/arithmetic_expression.c:220)
redisgraph.so!_AR_EXP_Evaluate (Unknown Source:0)
redisgraph.so!AR_EXP_Evaluate (./src/arithmetic/arithmetic_expression.c:327)
redisgraph.so!_cache_records (./src/execution_plan/ops/op_value_hash_join.c:136)
redisgraph.so!ValueHashJoinConsume (./src/execution_plan/ops/op_value_hash_join.c:201)
redisgraph.so!ProjectConsume (./src/execution_plan/ops/op_project.c:67)
redisgraph.so!SortConsume (./src/execution_plan/ops/op_sort.c:169)
redisgraph.so!ResultsConsume (./src/execution_plan/ops/op_results.c:34)
redisgraph.so!ExecutionPlan_Execute (./src/execution_plan/execution_plan.c:959
执行调用堆栈。
异常是在堆栈的较高位置抛出的,在这种情况下,我们想要
我们可以在执行路径上的每个函数中引入错误检查,但这会降低性能(分支预测)并使我们的代码过于复杂,因为到处都充斥着 if(error) return error; 逻辑结构。
因此,跳跃是想到的第一个选择,但请注意,跳跃只能跳到其被调用的函数中的某个位置。
function A() {
jump there; // Can't jump outside of current scope.
}
function B() {
there:
...
}
我们另一个想法是在一个新线程中调用 ExecutionPlan_Execute,这样当抛出异常时,我们只需终止该线程并在“父”线程中恢复执行。这种方法将使我们无需引入额外的逻辑或代码分支。
function Query_Execute() {
/* Call ExecutionPlan_Execute on a different thread
* and wait for it to exit */
char *error = NULL;
pthread_t thread;
pthread_create(&thread, NULL, ExecutionPlan_Execute, NULL);
pthread_join(thread, &error);
if(error != NULL) {
// Exception been thrown.
reportError(error);
}
...
}
但是这种设计会引入额外的线程执行开销(即使我们使用线程池),我们不想放弃对操作系统调度器的太多控制。
最终,我们发现了 longjmp,它类似于 jump,但不受调用函数范围的限制。我们可以简单地从任何地方跳到调用堆栈中的某个预设点,最棒的是,我们的堆栈会像从每个嵌套函数中返回一样解开到该点。如果你愿意,这就像时光倒流。
// ExecutionPlan.c
function Query_Execute() {
/* Set an exception-handling breakpoint to capture run-time errors.
* encountered_error will be set to 0 when setjmp is invoked, and will be nonzero if
* a downstream exception returns us to this breakpoint. */
QueryCtx *ctx = pthread_getspecific(_tlsQueryCtxKey);
if(!ctx->breakpoint) ctx->breakpoint = rm_malloc(sizeof(jmp_buf));
int encountered_error = setjmp(*ctx->breakpoint);
if(encountered_error) {
// Encountered a run-time error; return immediately.
reportError();
return;
}
/* Start executing, if an exception is thrown somewhere down the road
* we will resume execution at: if(encountered_error) above. */
ExecutionPlan_Execute();
}
/* ArithmeticExpression.c
* AR_EXP_Evaluate is called from various points in our code base
* all originating from Query_Execute. */
SIValue AR_EXP_Evaluate(AR_ExpNode *root, const Record r) {
SIValue result;
AR_EXP_Result res = _AR_EXP_Evaluate(root, r, &result);
if(res != EVAL_OK) {
/* An error was encountered during evaluation!
* Exit this routine and return to the point on the stack where the handler was
* instantiated. */
jmp_buf *env = _QueryCtx_GetExceptionHandler();
longjmp(*env, 1);
}
return result;
}
这是我们最近引入的设计。如果您曾经运行了一个违反了已调用函数假设的查询,此机制将用于报告错误。
127.0.0.1:6379> GRAPH.query G "match (a:person) where toUpper(a.name) = 'Alexander' RETURN a"
(error) Type mismatch: expected String but was Integer
通过 redis-cli 进行错误报告。
出于好奇,我在 cpython github 仓库(Python 实现)中搜索了 longjmp。我想知道他们是否像我们一样将相同的方案应用于异常处理,但是我的搜索没有结果,我需要进一步调查。