如何实现 “defer”:Go vs Java vs C/CPP

1 简述

资源泄漏问题,是不少新手容易忽略的问题。程序中申请的某些资源需要显示地释放,特别是系统资源,如打开的文件描述符、tcp & udp套接字、动态申请的堆内存等等。即便是说很多遍避免资源泄露的各种方式,开发人员仍然不能很好地避免资源泄露问题。避免资源泄露并不是一个技巧性很强的问题,但是当大家写起代码来被各种业务逻辑冲昏了头的时候,bug就容易趁机而入。
go defer、cpp智能指针、java try-catch-finally,某种程度上都是解决这类资源释放问题的利器。

2 避免资源泄露

语言层面如果能够提供某种能力,在资源申请成功、使用之后及时地进行资源释放,那就再好不过了。在深入对比C、C++、Java、Go中如何实现自动释放资源之前,我们先考虑下这个过程中存在那些难点:

  • 资源申请成功、正常使用之后,如何判断资源已经使用完毕?

    以打开的文件描述符为例,fd是int类型变量,文件打开之后在进程的整个生命周期内都是有效的,其实文件访问结束之后就可以认为这个fd可以关闭、释放了;再以动态申请的堆内存为例,堆内存空间也是在进程生命周期内有效的,堆内存是通过指针进行访问的,如果没有任何指针指向这段堆内存区域,可以认为分配的堆内存可以释放掉了;再以申请的lock为例,lock成功之后可以访问临界区了,从临界区退出的那一刻开始,可以认为lock可以释放掉了。

    不同类型的资源,判断是否使用完毕的方式不一样,但有一点可以确认,开发人员清楚资源应该何时释放。

  • 开发人员清楚应该何时释放资源,但是语言级别如何提供某种机制避免开发人员疏漏?

    C并没有提供语言层面的机制来避免资源泄露问题,但是利用gcc提供的C扩展属性也可以做到类似的效果;

    C++提供了智能指针,以malloc动态分配堆内存为例,分配成功返回堆内存指针,用该指针来初始化一个智能指针,并绑定对应的回调方法,当智能指针作用域结束被销毁时,其上绑定的回调方法也会被调用。假如我们将释放堆内存的方法free(ptr)注册为智能指针上的回调方法,就可以在分配内存所在的作用域销毁时自动释放堆内存了。

    Java提供了try-catch-finally,在try中申请、使用资源,catch中捕获可能的异常并处理,在finally中进行资源释放。以打开文件为例,在try中打开文件、文件处理结束之后,finally中调用file.close()方法。考虑一种极端情况,我们的文件处理逻辑比较复杂,中间涉及的代码比较多,在编写了各种逻辑处理、异常处理之后,开发人员是否容易遗忘在finally中关闭文件呢?这种可能性还是比较大的。开发人员的习惯一般是遵循就近原则,定义变量的时候都是在使用之前,如果try block结尾处没有明显的文件相关的操作,开发人员可能不会联想到要关闭文件。

  • 考虑到开发人员遵循就近原则的习惯,能否在资源申请成功后立即注册一个资源释放的回调方法,在资源使用结束的时候回调这个回调方法?这个方式是比较容易实现的,听起来也比较优雅。

    Go defer提供了这样的能力,在资源申请成功之后立即注册一个资源释放的方法,选择函数退出阶段作为申请使用结束的时间点,然后回调注册的释放资源的方法最终完成资源的释放。既能够满足程序员“就近使用”的良好作风,也减少了因为遗忘泄露资源的可能,而且代码的可维护性也更好。

    Go defer在某些情况下也可能会带来一定的性能损耗。比如通过lock在保护临界区,在临界区退出之后就可以释放掉lock了,但是呢,defer只有在函数退出阶段才会触发资源的释放操作。这可能会导致锁粒度过大,降低并发处理能力。这一点,开发人员要做好权衡,确定自己选择defer是没有问题的。

3 不同语言模拟defer

3.1 模拟defer in C

C本身没有提供defer或者类似defer机制,但是借助gcc扩展也可以实现类似能力。利用gcc提供的扩展属性__cleanup__来修饰变量,当变量离开作用域时可以自动调用注册的回调函数。

下面是 gcc扩展属性``的描述,感兴趣的可以了解下。其实gcc提供的这种扩展属性比go defer控制力度更细,因为它可以控制的粒度可以细到“作用域级别”,而go defer只能将有效范围细到“函数级别”。

示例一

cleanup_attribute_demo.c

# include <stdio.h>

/* Demo code showing the usage of the cleanup variable
   attribute. See:http://gcc.gnu.org/onlinedocs/gcc/Variable-Attributes.html
*/

/* cleanup function
   the argument is a int * to accept the address
   to the final value
*/

void clean_up(int *final_value)
{
  printf("Cleaning up\n");
  printf("Final value: %d\n",*final_value);
}

int main(int argc, char **argv)
{
  /* declare cleanup attribute along with initiliazation
     Without the cleanup attribute, this is equivalent 
     to:
     int avar = 1;
  */

  int avar __attribute__ ((__cleanup__(clean_up))) = 1;
  avar = 5;

  return 0;
}

编译运行:

$ gcc -Wall cleanup_attribute_demo.c
$ ./a.out
Cleaning up
Final value: 5
示例二
/* Demo code showing the usage of the cleanup variable
   attribute. See:http://gcc.gnu.org/onlinedocs/gcc/Variable-Attributes.html
*/

/* Defines two cleanup functions to close and delete a temporary file
   and free a buffer
*/

# include <stdlib.h>
# include <stdio.h>

# define TMP_FILE "/tmp/tmp.file"

void free_buffer(char **buffer)
{
  printf("Freeing buffer\n");
  free(*buffer);
}

void cleanup_file(FILE **fp)
{
  printf("Closing file\n");
  fclose(*fp);

  printf("Deleting the file\n");
  remove(TMP_FILE);
}

int main(int argc, char **argv)
{
  char *buffer __attribute__ ((__cleanup__(free_buffer))) = malloc(20);
  FILE *fp __attribute__ ((__cleanup__(cleanup_file)));

  fp = fopen(TMP_FILE, "w+");

  if (fp != NULL)
    fprintf(fp, "%s", "Alinewithnospaces");

  fflush(fp);
  fseek(fp, 0L, SEEK_SET);
  fscanf(fp, "%s", buffer);
  printf("%s\n", buffer);

  return 0;
}

编译运行:

Alinewithnospaces
Closing file
Deleting the file
Freeing buffer

3.2 模拟defer in C++

C++中通过智能指针可以用来对defer进行简单模拟,并且其粒度可以控制到作用域级别,而非go defer函数级别。C++模拟实现的defer也是比较优雅地。

#include <iostream>
#include <memory>
#include <funtional>

using namespace std;
using defer = shared_ptr<void>;

int main() {
    defer _(nullptr, bind([]{ cout << ", world"; }));
    cout << "hello"
}

也可以做去掉bind,直接写lambda表达式。

#include <iostream>
#include <memory>

using namespace std;
using defer = shared_ptr<void>;

int main() {
    defer _(nullptr, [](...){ cout << ", world"; });
    cout << "hello"
}

上述代码执行时输出:hello, world。智能指针变量为在main函数退出时作用域结束,智能指针销毁时会自动调用b绑定的lambda表达式,程序先输出hello,然后再输出world。这里的示例代码近似模拟了go defer。

3.3 模拟defer in Java

Java中try-catch-finally示例代码,这里就简单以伪代码的形式提供吧:

InputStream fin = null;

try {
    // open file and read
fin = new FileInputStream(...);
    String line = fin.readLine();
    System.out.println(line);
} catch (Exception e) {
    // handle exception
} finally {
    fin.close();
}

Java中的这种try-catch-finally的方式,只能算是一种资源释放的方式,不能算作是模拟defer。Java中好像没有提供什么感知到作用域结束或者函数结束并触发回调函数的能力。我没有想出Java中如何优雅地模拟defer。

3.4 defer in Go

我认为defer in Go是目前各种编程语言里面实现的最为优雅的,简单易用,符合大家使用习惯,代码可读性好。defer in Go使用地如此之广,以致于连举个例子都是多余,这里就省掉示例代码了 :).

总结

资源释放是需要慎重考虑的,资源泄漏是新手程序员常犯的错误,本文从go defer的思想触发,对比了下不同语言c、cpp、java实现defer语义的方式。

您的支持,是我继续创作、分享知识的动力。如果您认为本文不错,请点赞、转发、赞赏 :)

本作品采用《CC 协议》,转载必须注明作者和本文链接
hitzhangjie
讨论数量: 0
(= ̄ω ̄=)··· 暂无内容!

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!