说实话,我不同意永远不要在代码中使用 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; 逻辑结构。
因此,跳转 是首先想到的选项,但请注意 jump 只能跳转到它所调用的函数中的某个位置。
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 的引用。我想知道他们是否采用了与我们相同的异常处理方法,但是 我的搜索 没有结果——我必须进一步调查。