【问题标题】:destroying a class instance doesn't kill instances it owns in vala销毁类实例不会杀死它在 vala 中拥有的实例
【发布时间】:2017-01-10 04:20:05
【问题描述】:

我有一个 Gtk.Box 的子类,其中包含一个 GLib.Timer,它会在给定的时间间隔后触发通知。我在这个类中有调用 Gtk.Box 上的 this.destroy() 的方法。即使在其父实例已被销毁后,计时器仍会继续运行并触发通知。已销毁的此类的所有实例都表现出此行为并继续使用 CPU 和内存,直到进程被杀死。

我该如何解决这个问题?如何有效地杀死实例以及如何手动释放内存而不是依赖 vala 的垃圾收集。

编辑:这是一个(令人尴尬的)mvce

// mvce_deletable
// nine
// 2017.01.11
// valac --pkg gtk+-3.0 --pkg glib-2.0 deletablebox.vala

using Gtk;
using GLib;

class RemovableBox : Gtk.Box {
    private Gtk.Button delete_button;
    private GLib.Timer timer;
    private Gtk.Label label;

    public RemovableBox () {
        delete_button = new Gtk.Button.with_label ("DESTROY");
        delete_button.clicked.connect (()=>{this.destroy();});
        this.add (delete_button);
        label = new Gtk.Label ("0000000");
        this.add (label);
        timer = new GLib.Timer ();
        timer.start ();
        Timeout.add (50, update);
        this.show_all ();
    }

    private bool update () {
        if (timer.elapsed () > 10.0f) {
            stdout.printf("and yet it breathes\n");
        }
        label.set_text ("%f".printf(timer.elapsed()));
        return true;
    }
}

int main ( string [] args ) {
    Gtk.init(ref args);
    var window = new Gtk.Window ();
    window.destroy.connect (Gtk.main_quit);
    var delete_me = new RemovableBox ();
    window.add ( delete_me );
    window.show_all();
    Gtk.main();
    return 0;
}

我在 RemovableBox 类中添加了一个 timer_id,但它仍然无法正常工作。

class RemovableBox : Gtk.Box {
    private Gtk.Button delete_button;
    private uint timeout_id;
    private GLib.Timer timer;
    private Gtk.Label label;

    public RemovableBox () {
        delete_button = new Gtk.Button.with_label ("DESTROY");
        delete_button.clicked.connect (()=>{this.destroy();});
        this.add (delete_button);
        label = new Gtk.Label ("0000000");
        this.add (label);
        timer = new GLib.Timer ();
        timer.start ();
        timeout_id = Timeout.add (40, update);
        this.show_all ();
    }

    ~ RemovableBox () {
        Source.remove (timeout_id);
    }

    private bool update () {
        if (timer.elapsed () > 10.0f) {
            stdout.printf("and yet it breathes\n");
        }
        label.set_text ("%f".printf(timer.elapsed()));
        return true;
    }
}

【问题讨论】:

    标签: linux gtk glib gnome vala


    【解决方案1】:

    GLib.Timer 是一个返回经过时间的秒表。它不会生成事件,但 GLib.Timeout 会。

    GLib 使用事件循环。这与 GTK+ 相同,它使用相同的底层 GLib 事件循环。 GLib.Timeout 用于创建一种事件源 - 在给定时间间隔后触发的计时器。当您的程序创建事件源时,您将获得一个源标识符。例如:

    timer_id = Timeout.add_seconds (1, my_callback_function);

    您的程序需要做的是将该计时器标识符存储在对象中,然后当调用按钮单击处理程序时,您可以将计时器作为事件源移除:

    Source.remove (timer_id);

    严格来说 Vala 没有垃圾回收周期。其他语言将收集不再使用的引用,然后在清理周期中删除分配给它们的资源。 Vala 使用引用计数,但它是确定性的。所以当一个对象不再被使用时,通常是当它超出范围时,分配给该对象的资源会立即被移除。对于 Vala 中的普通对象,而不是紧凑类,当对象被释放时也会调用析构函数。这允许在 Vala 中有效使用资源分配是初始化 (RAII) 模式。

    一般来说你不应该手动释放对象,Vala 的引用计数非常好。我认为了解 GLib 的事件循环和事件源以了解正在发生的事情很重要。详细说明见GLib's documentation on its main event loop

    现在你已经提供了MCVE,我们可以详细看看 Vala 是如何管理内存的。如果您想深入了解幕后发生的事情,您可以使用--ccode 开关和valac

    您的程序中感兴趣的第一行是:

    Timeout.add (50, update);

    valac看C代码这一行使用了g-timeout-add-full ()函数:

    g_timeout_add_full (G_PRIORITY_DEFAULT, (guint) 50, _removable_box_update_gsource_func, g_object_ref (self), g_object_unref);

    这里的关键部分是g_object_ref (self)。这会将对象的引用计数增加一并传递一个指向该对象的指针。这很有意义,因为在 Vala 代码中传递的 update () 回调利用了来自对象的实例数据。 Vala 正在做正确的事情,并确保实例数据在计时器出现时保持活动状态。删除源时调用“g_object_unref”。这是您的程序的修改版本,将这种理解付诸实践:

    // mvce_deletable
    // nine
    // 2017.01.11
    // valac --pkg gtk+-3.0 deletablebox.vala
    
    using Gtk;
    
    class RemovableBox : Gtk.Box {
        private Gtk.Button delete_button;
        private uint timeout_id;
        private GLib.Timer timer;
        private Gtk.Label label;
    
        public RemovableBox () {
            delete_button = new Gtk.Button.with_label ("DESTROY");
            delete_button.clicked.connect (()=>{this.tidy_up_and_destroy ();});
            this.add (delete_button);
            label = new Gtk.Label ("0000000");
            this.add (label);
            timer = new GLib.Timer ();
            timer.start ();
            timeout_id = Timeout.add (40, update);
            this.show_all ();
        }
    
        ~ RemovableBox () {
            print ("RemovableBox destructor called\n");
        }
    
        private bool update () {
            if (timer.elapsed () > 10.0f) {
                stdout.printf("and yet it breathes\n");
            }
            label.set_text ("%f".printf(timer.elapsed()));
            return true;
        }
    
        private void tidy_up_and_destroy () {
            print ("RemovableBox.tidy_up_and_destroy called\n");
            Source.remove (timeout_id);
            this.destroy ();
        }
    }
    
    void main ( string [] args ) {
        Gtk.init(ref args);
        var window = new Gtk.Window ();
        window.window_position = WindowPosition.CENTER;
        window.resize (250,50);
        window.destroy.connect (Gtk.main_quit);
        window.add (new RemovableBox ());
        window.show_all();
        Gtk.main();
    }
    

    以前程序仍然保留对RemovableBox 对象的引用,因此从未完全删除。通过首先删除事件源然后调用this.destroy (); 这意味着不再有引用并且对象被删除。

    这里还有一点很重要。行:

    var delete_me = new RemovableBox ();
    window.add ( delete_me );
    

    main () 中已更改为:

    window.add (new RemovableBox ());
    

    Vala 对象存在于创建它们的块的范围内。通过将对象分配给 delete_me,您可以为 main () 块的其余部分保留对该对象的引用。通过将其更改为方法调用的参数,它仅用于调用,因此在单击按钮时被释放。

    顺便说一句,GLib 在使用valac 时会自动包含在内,因此不需要using GLib; 或使用--pkg glib-2.0 编译。

    【讨论】:

    • 直到现在我才意识到 Vala 会做 RAII。堆栈上是否也分配了非 GObject 类(如在 C++ 中)?
    • 维基百科文章提到了 C++ 和堆栈。我认为这对一般 RAII 概念有点误导。在 Vala 中,释放资源的调用由 Vala 编译器按照与它们创建的相反顺序创建。因此,您可以在 try 块中引发“异常”,并且将反向调用析构函数,从而正确释放资源。这是另一个问题:)
    • 所以我将 timer_id 声明为 uint 并像你说的那样修改了类,但它仍然无法按预期工作。
    • 绝对传奇的答案托马斯。是否需要再定义析构函数?
    • 不需要,示例中显示 RemovableBox 对象何时最终被释放。因此,如果您将其分配给 main () 中的变量,您将在程序结束之前看到它被释放。
    【解决方案2】:

    您将自动引用计算与完全垃圾收集混淆了。

    GLib 中没有垃圾收集器,但是类有一个引用计数,当 GObject 在多个地方使用时会增加,当在这些地方不再使用时会减少,直到达到零。然后释放该对象。

    事实上,在 C 代码中,引用计数是手动的:

    // Reference count is set to 1 on gobject_new  
    gpointer obj = gobject_new (G_SOME_OBJECT, NULL);
    // It can be manually increased when the object is stored in multiple places
    // Let's increase the ref count to 2 here
    gobject_ref (obj);
    // Let's decrease it until it reaches 0
    gobject_unref (obj);
    gobject_unref (obj);
    // Here the object is destroyed (but the pointer is still pointing to the previous memory location, e.g. it is a dangling pointer)
    // gobject_clear (obj) can be used in situation where the variable is reused
    // It still only unrefs the object by 1 though! In addition it will set obj to NULL
    

    Vala 将auto 添加到引用计数中,这使其成为“自动引用计数”(ARC)。那就是你不必担心 Vala 中的引用计数,它会为你添加适当的 refunref 操作。

    在完全垃圾回收中(如 C#、Java 等)内存释放不是确定性的,即使不再使用对象也可以保持活动状态。这是使用称为“托管堆”的东西完成的,垃圾收集器在后台运行(即作为 GC 线程)。

    现在我们已经为您的实际问题提供了背景资料:

    您必须从其父容器中删除 Gtk.Box 并将您在 Vala 代码中可能仍然存在的任何引用设置为 null 以使引用计数为 0。然后它将是 unrefed .

    当然还有其他选项,例如禁用计时器等。您确实应该在您的问题中添加MVCE,以便我们能够就您的代码为您提供一些设计建议。

    PS:引用计数通常被认为是一种简单的垃圾收集方法。这就是我写 full 垃圾回收(也称为tracing garbage collection)的原因,以免混淆这两个术语。 See the Wikipedia article on garbage collection.

    【讨论】:

      猜你喜欢
      • 2013-04-27
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2019-03-30
      • 2019-02-22
      • 2019-02-19
      相关资源
      最近更新 更多