现在,有很多的语言都支持try-catch,比如常见的就是c++, java等。这样让我们能够在代码运行的时候更好的定位出现问题的原因,提高了开发的效率。我们今天就简单的聊一聊try-catch的实现原理,并且用c语言实现一个自己的try catch。
try-catch在使用上是很简单的。把你预计会出现错误的代码写在Try内,catch去捕获可能出现的异常,finally是无论如何都会执行的语句。
比如:
try {
int ret = write(buf, 1, sizeof(buf));
// ......
} catch (Exception e) {
// 捕获异常之后的处理
} finally {
// ......
}
上述代码中write函数调用可能出现的问题就会被catch捕获,捕获之后自己可以做一些处理来debug代码。
在我们手动实现这个try-catch之前,我们需要了解几个函数。这几个是系统调用,以及和线程有关的函数。
int setjmp(jmp_buf env); // env 表示的是当前的上下文
这个函数的作用是将当前运行的上下文环境保存起来,等到longjmp调用的时候就可以直接跳转到setjmp保存的位置。
void longjmp(jmp_buf env, int val); // env 就是存储上下文环境的变量,val就是setjmp的返回值。
这个函数的作用就是跳转到setjmp保存的上下文的地方
需要注意的是,setjmp和longjmp一次只能用一个环境变量,这样才能保证能跳转到想要的位置。
了解这两个函数之后,我们就可以简单先写一个初版。代码如下:
#include
#include
typedef struct Excep
{
jmp_buf _stackInfo;
int _excpType;
} Excep;
#define Try(excep) if ((excep._excpType = setjmp(excep._stackInfo)) == 0)
#define Catch(excep, tag) else if (excep._excpType == tag)
#define Throw(e, tag) longjmp(e, tag)
#define finally
int main()
{
Excep excep;
Try(excep) {
Throw(excep, 2);
printf("0>>>\n");
} Catch(excep, 1) {
printf("1>>>\n");
} Catch(excep, 2) {
printf("2>>>\n");
} finally {
printf("3>>>\n");
}
}
以上运行结果:
0>>>
2>>>
3>>>
但是这个代码还是有一个明显的问题,当我在Try的内部再进行Try的话,就会出现嵌套的情况。就好比函数入栈,此时第一个try入栈,第二个try入栈,出栈的话也要按照顺序,不然的话环境变量就会混乱。同时当多个线程共享环境变量的时候,由于是共享的,如果同时访问的话肯定会出问题。
因此我们想到一个办法就是创建一个私有的环境变量来存储当前的环境变量,同时我们需要一个结点来指向当前环境变量结点的前一个,每次跳转的时候就释放要出栈的环境变量。
这时我们就需要引入另一组api:
int pthread_key_create(pthread_key_t *key, void (destructor)(void));
// key为指向一个键值的指针,destructor指明了一个destructor函数,如果这个参数不为空,那么当每个线程结束时,系统将调用这个函数来释放绑定在这个键上的内存块。
int pthread_setspecific (pthread_key_t key, const void *pointer));
void *pthread_getspecific(pthread_key_t key);
// set是把一个变量的地址告诉key,一般放在变量定义之后,get会把这个地址读出来,然后你自己转义成相应的类型再去操作,注意变量的有效期。
现在我们就大致看下如何实现的。
代码如下:
#define ThreadKeyType pthread_key_t
#define ThreadKeyCreate(key) pthread_key_create(&key, NULL)
#define ThreadDataSet(key, value) pthread_setspecific(key, value)
#define ThreadDataGet(key) pthread_getspecific(key)
ThreadKeyType excepStack;
#define Try(exp) Excep *exp = (Excep *)malloc(sizeof(Excep)); \ // create a Node
exp->prev = ThreadDataGet(ExcepStack); \ // get private space
ThreadDataSet(ExcepStack, exp); \ // set current space
if ((exp._excpType = setjmp(exp._stackInfo)) == 0) { // handle exception
#define Catch(e)
Excep* cur = ThreadDataGet(ExcepStack);
ThreadDataSet(ExcepStack, cur->prev); // point to prev node
} else if (e == cur->prev) {
#define Throw(excep, Exception) Excep* cur = ThreadDataGet(ExcepStack);
longjmp(cur->ExcepStack, Exception);
#define Finally
int main()
{
Excep excep;
Try(excep) {
Throw(excep, 2);
printf("0>>>\n");
} Catch(excep) {
printf("1>>>\n");
} Catch(excep) {
printf("2>>>\n");
} finally {
printf("3>>>\n");
}
}
Try的部分首先是创建一个结点,存储上下文以及前一个的指针;然后取到当前存这的私有数据,将prev指向取出的私有数据,将当前的上下文set进私有数据
catch的部分,先取出当前key存储的私有数据,将前一个存到私有数据,然后比对当前的和我们设置的异常是否相同。
Throw就是获取到当前的环境变量,然后跳转。
同上面的代码我们也可以看出,try-catch的原理实际是依赖setjmp,longjmp这类系统调用,可以保存上下文环境,出现问题时,可以跳转到设置环境的地方继续执行,同时try-catch的嵌套问题可以用栈的思维解决,保存一个指向上一个环境的地址。多线程的问题,可以用pthread库中的函数创建私有数据,不能共享环境变量,这样各自线程捕获异常之后跳转就互不相干了。