转载

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

案例

下面的代码来自我们某一工具源码中:

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).strip('gz').strip('.')

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 Resurce:
    def __init__(self):
        print('__init__()')
        self.status = 'open'

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

with contextlib.closing(Resurce()) 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("test.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("test.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 = 5
    defer func() {
        i++
        fmt.Println("defer2:", i) // 打印结果为 defer2: 7
    }()
    defer func() {
        i++
        fmt.Println("defer1:", i) // 打印结果为 defer1: 6
    }()
    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

#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,妈妈再不用担心我的代码出现资源泄露了^_^。

结语

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

原文  http://lanlingzi.cn/post/technical/2020/0516_code/
正文到此结束
Loading...