案例
下面的代码来自我们某一工具源码(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,能又能确保资源被正常释放。