如何防止 Elisp 出现资源泄露

发布于 2023-03-26 11:21:17 字数 3325 浏览 103 评论 0

对于一些基础类型的对象,例如 list、vector、string 等,Elisp 都能够帮你自动进行垃圾回收,然而对于 process 和 buffer 这两类对象,只能由你手工 kill。

一般来说,为了防止使用者忘了主动 kill 这些对象,Elisp 预先提供了一些 with-XXXX 的宏以供使用,这些宏的内部一般使用 unwind-protect 来保证 body 执行完后会回收资源。

例如

(with-temp-buffer
  (insert-file-contents "foo.txt" nil 0 10)
  (buffer-string))

实际会扩展成

(let ((temp-buffer (generate-new-buffer "*temp*")))
  (with-current-buffer temp-buffer
    (unwind-protect
        (progn
          (insert-file-contents "foo.txt" nil 0 10)
          (buffer-string))
      (and (buffer-live-p temp-buffer)
           (kill-buffer temp-buffer)))))

但是使用 with-XXXX 宏,或者说使用 unwind-protect 有一个受到约束的地方,那就是无法用在需要异步调用的情况中,例如,当使用 url-retrieve 时,这个API会创建一个 buffer 用于存放 url 的内容,但是销毁这个 buffer 的工作需要交由它的回调函数来负责。

Emacs Lisp Object Finalizers 这篇文中提出了一种解决方案. 它允许你将一个析构函数与一个普通对象相关联,当这个普通对象被Elisp的垃圾回收器回收时,同时会调用相对应的析构函数,从而回收process/buffer对象.

Emacs本身提供了一个 post-gc-hook,当垃圾回收完成后就会调用该hook中的函数. 我们要做的就是在这个函数中检查我们注册的普通对象是否被回收了,如果回收了的话就调用对应的机构函数.

那么现在问题来了,我怎么知道我们注册的普通对象被回收了呢,还是根本就没有注册该对象呢? 这就的用到hashtable的弱引用属性了.

使用 make-hash-table 创建hashtable时,有一个 :weakness 属性,关于它的说明是这样的:

:weakness WEAK -- WEAK must be one of nil, t, ‘key’, ‘value’, ‘key-or-value’, or ‘key-and-value’. If WEAK is not nil, the table returned is a weak table. Key/value pairs are removed from a weak hash table when there are no non-weak references pointing to their key, value, one of key or value, or both key and value, depending on WEAK. WEAK t is equivalent to ‘key-and-value’. Default value of WEAK is nil.

这样一来,我们可以把注册的对象作为 value,并用一个一定存在的值(比如t/nil)作为 key 值来创建一个只包含唯一键值对的弱引用hashtable. hashtable存在则说明一定注册了某个对象,若该对象没有被回收则使用 key 值应该能从该 hashtable 中取出值来,若对象被销毁,则使用该key值就不能从该hashtable中取出值来了,也就是返回nil.

(defun weak-ref (thing)
  "创建一个弱引用关系"
  (let ((ref (make-hash-table :size 1 :weakness 'value :test 'eq)))
    (setf (gethash t ref) thing)
    ref))
(defun deref (ref)
  "ref中的value被回收,则deref函数会返回nil,否则返回该value"
  (gethash t ref))

现在我们可以把这个普通对象与析构函数组合在一起了。

(defvar finalizable-objects ())
(defun register (object destructor)
  (push (cons (weak-ref object) destructor) finalizable-objects))

这里我们用变量 finalizable-objects 来将所有注册的对象都给收集起来,这样在 post-gc-hook 函数中,只需要遍历该变量,找出其中被回收的普通变量然后调用对应的析构函数就 OK 了。

(defun try-finalize ()
  (let ((alive (cl-remove-if-not #'deref finalizable-objects :key #'car))
        (dead (cl-remove-if #'deref finalizable-objects :key #'car)))
    (setf finalizable-objects alive)
    (mapc #'funcall (mapcar #'cdr dead))))

(add-hook 'post-gc-hook #'try-finalize)

当然上面的代码只是为了说明方案的原理的,还远远不够完美,比如在调用 post-gc-hook 函数时不会触发垃圾回收机制,因此要小心在函数中不要使用太多的内存,而 cl-remove-if 就会产生大量的新 cons cell,又比如,当依次调用各个析构函数时,要保证即使其中一个析构函数执行时发生 error,也不能因此而中断对其他析构函数的调用.

最后, Emacs Lisp Object Finalizers 的作者已经开发出了一个 package 用于实现该回收机制了,我们只要直接用就好了: 地址是: https://github.com/skeeto/elisp-finalize

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据

关于作者

↙厌世

暂无简介

文章
评论
27 人气
更多

推荐作者

櫻之舞

文章 0 评论 0

弥枳

文章 0 评论 0

m2429

文章 0 评论 0

野却迷人

文章 0 评论 0

我怀念的。

文章 0 评论 0

    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文