在本系列关于将 Perl 5 代码迁移到 Perl 6 的第一篇文章中,我们研究了移植代码时可能遇到的一些问题。在第二篇文章中,我们将深入探讨 Perl 6 中垃圾回收的不同之处。
Perl 6 中没有对象的及时销毁。对于习惯了 Perl 5 中对象销毁语义的人来说,这个启示通常会令人震惊。但请不要担心,Perl 6 中还有其他方法可以获得相同的行为,尽管这需要开发人员多加思考。让我们首先了解一下 Perl 5 中的情况背景。
引用计数
在 Perl 5 中,“超出作用域”的对象的及时销毁是通过 引用计数 实现的。当在 Perl 5 中创建对象时,它的引用计数为 1 或更多,这使其保持活动状态。在最简单的情况下,它看起来像这样
{
my $a = 42; # reference count of $a = 1, because lives in lexical pad
}
# lexical pad is gone, reference count to 0
在 Perl 5 中,如果值是一个对象(也称为 blessed),则会调用其 DESTROY
方法。
{
my $a = Foo->new;
}
# $a->DESTROY called
如果不涉及外部资源,及时销毁只是另一种管理程序使用的内存的方式。作为程序员,您无需关心事物如何以及何时被回收。话虽如此,如果您需要处理外部资源(例如数据库句柄,数据库服务器通常只提供有限数量的句柄),那么及时销毁是一个非常好的功能。引用计数可以提供这种功能。
然而,引用计数有几个缺点。Perl 5 核心开发人员花费了多年时间才使引用计数正确工作。如果您正在使用 XS,则始终需要注意引用计数,以防止内存泄漏或过早销毁。
在多线程环境中,保持事物同步变得更加困难,因为您不希望丢失来自多个线程同时进行的引用更新(因为这会导致内存泄漏和/或外部资源无法释放)。为了规避这种情况,需要某种形式的锁定或原子更新,但这两种方法都不便宜。
请注意,Perl 5 ithreads 更像是解释器之间具有非共享内存的内存中 fork,而不是 C 等编程语言中的线程。因此,它的引用计数仍然不需要任何锁定。
引用计数还存在一个基本缺点,即如果两个对象包含对彼此的引用,它们将永远不会被销毁,因为它们使彼此的引用计数保持在 0 以上(循环引用)。在实践中,这种情况通常会更深入,更像是 A -> B -> C -> A
,其中 A、B 和 C 都使彼此保持活动状态。
弱引用的概念是为了规避 Perl 5 中的这些情况而开发的。虽然这可以解决循环引用问题,但它会带来性能影响,并且无法从根本上解决拥有(和查找)循环引用的问题。您需要能够找出在何处以最佳方式使用弱引用;否则,您可能会得到不必要的过早对象销毁。
可达性分析
由于 Perl 6 在其核心中是多线程的,因此在早期就决定引用计数在性能和维护方面都存在问题。相反,当需要更多内存并且可以安全删除对象时,对象将从内存中逐出。
在 Perl 6 中,您可以创建 DESTROY
方法,就像在 Perl 5 中一样。但是您不能确定它何时(如果曾经)会被调用。
在不深入 太多细节 的情况下,Perl 6 中的对象仅在启动垃圾回收运行时才会被销毁,例如,当达到某个内存限制时。只有到那时,如果内存中的其他对象不再能访问某个对象并且该对象具有 DESTROY
方法,才会在删除该对象之前调用它。
当程序退出时,Perl 6 不会进行垃圾回收。适用的 phaser(例如 LEAVE
和 END
)将被调用,但除了 phaser 中运行的代码(间接)启动的垃圾回收之外,不会进行其他垃圾回收。
如果您始终需要有序地关闭程序使用的外部资源(例如数据库句柄),则可以使用 phaser 来确保以正确且及时的方式释放外部资源。
例如,您可以使用 END
phaser(在 Perl 5 中称为 END
块)在程序退出时(无论出于何种原因)正确断开与数据库的连接
my $dbh = DBIish.connect( ... ) or die "Couldn't connect";
END $dbh.disconnect;
请注意,在 Perl 6 中,END
phaser 不需要具有块(如 { ... }
)。如果它没有,则 phaser 中的代码与周围的代码共享词法 pad (lexpad)。
上面的代码中有一个缺陷:如果程序在建立数据库连接之前退出,或者由于任何原因数据库连接失败,它仍然会尝试在 $dbh
中的任何内容上调用 .disconnect
方法,这将导致执行错误。但是,Perl 6 中有一个简单的习惯用法可以使用 with 来规避这种情况。
END .disconnect with $dbh;
后缀 with
仅在给定值已定义(通常是实例化的对象)时才匹配,然后将其主题化为 $_
。 .disconnect
是 $_.disconnect
的简写形式。
如果您希望在退出特定作用域时清理外部资源,则可以在该作用域内使用 LEAVE
phaser。
if DBIish.connect( ... ) -> $dbh {
LEAVE $dbh.disconnect; # no need for `with` here
# do your stuff with the database
}
else {
say "Could not do the stuff that needed to be done";
}
每当离开 if
的作用域时,都会执行任何 LEAVE
phaser。因此,只要代码在该作用域中运行过,数据库资源就会被释放。
总结
尽管 Perl 6 没有 Perl 5 用户习惯的及时对象销毁,但它确实有易于使用的替代方法来确保外部资源的管理,类似于 Perl 5 中的方法。
3 条评论