蘭陵N梓記

一指流沙,程序年华


  • 首页

  • 归档

  • 关于

  • 搜索
close

飞哥讲代码1:确保资源被释放

时间: 2020-05-16   |   分类: 技术     |   阅读: 2345 字 ~5分钟

案例

下面的代码来自我们某一工具源码(Python语言)中:

file_gz = gzip.GzipFile(file_name)
src_path, src_file = os.path.split(file_name)
tmp_file_name = os.path.join(path_name, src_file).strip('gz').strip('.')
tmp_file = open(tmp_file_name, 'wb')
tmp_file.writeline(file_gz.realines())
file_gz.close()
tmp_file.close()
os.remove(file_name)

从代码健壮角度来看,存在如下两个问题:

  • 缺少捕获异常,在GzipFile打开文件,open打开文件之后的操作都可能抛出异常
  • 当抛出异常时,file_gz与tmp_file就会出现未正常close,存在文件句柄的泄露问题

能正确释放资源的建议写法是:

src_path, src_file = os.path.split(file_name)
dst_file_name = os.path.join(path_name, src_file).rstrip('.gz')

with gzip.GzipFile(file_name) as src_gz_file, open(dst_file_name, 'wb') as out_file:
    out_file.writeline(src_gz_file.realines())
os.remove(file_name)

还有一种写法,采用try-except-finally,在finally中对打开的文件关闭,但这种写法的代码显得臃肿。所以Python又提供上述示例中with语句写法。

背后的知识

with语句启用了上下文管理器,标准库中contextlib模块包含用于处理上下文管理器一些工具。

上下文管理器涉及两个方法:

  • 当进入内部代码块时,执行 __enter__() 方法, 返回要在上下文中使用的对象
  • 当离开 with 块时,执行 __exit__() 方法,清理正在使用的任何资源

对于任何一个对象能够使用 with 语句来清理资源,只要像下面来提供 __enter__() 方法与 __exit__() 方法:

class Context:
    def __enter__(self):
        print('__enter__()')
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        print('__exit__()')

with Context():
    print('do somethon in the context')

file 类内嵌支持上下文管理器API,但有些历史遗留下的其他对象并不支持,标准库文档中给出的 contextlib 示例是 urllib.urlopen() 返回的对象。还有其他遗留类使用 close() 方法,但不支持上下文管理器API。要确保资源已关闭,要使用 closing() 为其创建上下文管理器。

class Resource:
    def __init__(self):
        print('__init__()')
        self.status = 'open'

    def close(self):
        print('close()')
        self.status = 'closed'

with contextlib.closing(Resource()) as r:
    print('inside with statement: {}'.format(r.status))

另外 contextlib 模块还提供了装饰器来简化上下文管理器相关场景的代码开发,这里不展开讲了,有兴趣的同学找资料研究吧。

其它语言玩法

对于资源的简洁释放是所有编程语言都要解决的问题,举一反三,我们再来看看其它语言的一些玩法。

Java

在Java1.7之前,是采用try-catch-finally的方式解决:

BufferedInputStream bin = null;
BufferedOutputStream bout = null;
try {
    bin = new BufferedInputStream(new FileInputStream(new File("input.txt")));
    bout = new BufferedOutputStream(new FileOutputStream(new File("out.txt")));
    int br = -1;
    while ((br = bin.read()) != -1) {
        bout.write(br);
    }
} catch (IOException e) {
   log.error("....");
} finally {
    if (bin != null) {
        try {
            bin.close();
        } catch (IOException e) {
            log.error("....")
        }
    }
    if (bout != null) {
        try {
            bout.close();
        } catch (IOException e) {
            log.error("....");
        }
    }
}

上面的代码是不是不够简洁?关闭资源也要 try-catch ,否则会导致后续的close未被执行。Java 1.7中新增的try-with-resource语法糖,简化的代码就成了如下:

try (BufferedInputStream bin = new BufferedInputStream(new FileInputStream(new File("input.txt")));
    BufferedOutputStream bout = new BufferedOutputStream(new FileOutputStream(new File("out.txt")))
) {
    int br = -1;
    while ((br = bin.read()) != -1) {
        bout.write(br);
    }
} catch (IOException e) {
    log.error("....");
}

与Python的with语句用法与效果真是异曲同工。为了能够配合try-with-resource,资源必须实现AutoClosable接口。

如果熟悉lombok库的同学,也会知道有个 @Cleanup 注解,它会帮助你安全的调用close方法来释放资源,相比Java内建的try-with-resource语法糖,它还可以调用非close方法。@Cleanup(“dispose”),通过指定方法名来调用相应的方法来清理资源。不过约束是被调用的方法要求是无参数方法。

无论是try-with-resource,还是lombok的@Cleanup注解,他们都是语法糖,通过编译帮你生成的字节码在finally中调用close方法来释放资源。

Go

作为后起之秀的Go,对于资源释放的解决方法,相比Python与Java来得更灵活些。它提供了defer关键字:

src, err := os.Open(srcFile)
if err != nil {
    return
}
defer src.Close()

defer的底层实现是:defer后面的表达式会被放入一个列表中,在当前方法返回的时候,列表中的表达式就会被执行。采用栈数据结构,一个方法中,当存在多个defer语句时,先加入列表则后执行。

当然,由于defer后面可以跟匿名函数块,如:

func test() int {
    i := 0
    defer func () {
        i++
        fmt.Println("defer2:", i) // 打印结果为 defer2: 2
    }()
    defer func () {
        i++
        fmt.Println("defer1:", i) // 打印结果为 defer1: 1
    }()
    return i // 假如返回值是a,此时a=i,defer中修改i的值不会影响返回值a,defer也根本访问不到a
}

若是像上面代码在defer的函数中有使用前面的变量并对它进行修改,则引入了复杂性。有兴趣的同学的不烦再对defer深挖一下。是不是像Java一样要求,不要在finally中修改基本类型或对象中的值的既视感?

再来一个例子,对命名返回值修改:

func test() (i int) {
    i = 1
    defer func() {
        i++
        fmt.Println("defer2:", i) // 打印结果为 defer2: 3
    }()
    defer func() {
        i++
        fmt.Println("defer1:", i) // 打印结果为 defer1: 2
    }()
    return i  // 返回的结果是几?
}

它的返回值又是什么,还有更多的defer坑等你去发现哦。

C++

C++其实在资源管理上是最为成熟,RAII技术被认为是C++中管理资源的最佳方法。 RAII是C++的发明者Bjarne Stroustrup老爷子提出的概念,RAII全称是Resource Acquisition is Initialization,直译过来是资源获取即初始化,也就是说在构造函数中申请分配资源,在析构函数中释放资源。

智能指针(std::unique_ptr)即RAII最具代表的实现,使用智能指针,可以实现自动的内存管理,再也不需要担心忘记delete造成的内存泄漏。内存只是资源的一种,如对于文件的打开与关闭,也可以使用RAII来解决,不过有点麻烦,按照常规的RAII技术需要写一堆管理它们的类。

不过C++11有lambda表达式,结合std::function,我们可以利用RAII机制完美地模拟Go的defer(效果与Go还是有些区别的,Go 是函数级,它是代码块级):

#define SCOPEGUARD_LINENAME_CAT(name, line) name##line
#define SCOPEGUARD_LINENAME(name, line) SCOPEGUARD_LINENAME_CAT(name, line)
#define DEFER(callback) ScopeGuard SCOPEGUARD_LINENAME(EXIT, __LINE__)(callback)

class ScopeGuard {
public:
    explicit ScopeGuard(std::function<void()> f) : 
        handleExitScope(f){};

    ~ScopeGuard() { handleExitScope(); }
private:
    std::function<void()> handleExitScope;
};

{
    std::ofstream file("test.txt");
    DEFER([&] { file.close(); });
}

上面的代码看起来是不是很Clean,妈妈再不用担心我的代码出现资源泄露了^_^。

结语

程序使用的资源,不仅仅是CPU与内存。在内存管理方面,有垃圾回归器的语言帮程序员省了很多事。但广义上资源还有文件、流、管道、连接与锁等等,这些都需要开发者手动关闭他们,否则随着程序的不断运行,资源泄露将会累积成重大的生产事故。我们也许会记得在正常流程中关闭这些资源,却可能经常忽视了异常分支场景,我们应该利用语言中最新的特性,既使代码Clean,能又能确保资源被正常释放。

#软件开发# #python# #java# #go# #c++#
飞哥讲代码2:把大象装进冰箱要几步
阅读
微信扫一扫交流

标题:飞哥讲代码1:确保资源被释放
作者:兰陵子
关注:lanlingthink(览聆时刻)
声明:自由转载-非商用-非衍生-保持署名(创作共享3.0许可证)

  • 文章目录
  • 站点概览
兰陵子

兰陵子

Programmer & Architect

164 日志
4 分类
57 标签
GitHub 知乎
  • 案例
    • 背后的知识
  • 其它语言玩法
    • Java
    • Go
    • C++
  • 结语
© 2009 - 2022 蘭陵N梓記
Powered by - Hugo v0.101.0
Theme by - NexT
0%