更改 gtkmm 小部件的标签时出现零星段错误

sporadic segfaults when changing label of gtkmm widget

嗨, 我有一个 gtkmm 应用程序,它执行一些异步网络请求,向服务器询问 gtk-widgets 的其他属性。 这意味着,例如,应用程序应该能够更改小部件的标签。

在此示例中,我创建了一个基于 Gtk::ToggleButton 的新小部件。

但我发现有时 gtkmm 应用程序会因段错误而崩溃。当使用 gdb 调试时,我总是得到设置标签的行。

为了更好地理解,我创建了一个 MWE,它在循环中进行标签更改,以模拟大量异步调用:

#include <boost/asio.hpp>
#include <boost/asio/steady_timer.hpp>

#include <iostream>
#include <thread>
#include <mutex>
#include <gtkmm/application.h>
#include <gtkmm/window.h>
#include <gtkmm/togglebutton.h>

class led_label_t : public Gtk::ToggleButton {
public:
    using value_list_t = std::vector<Glib::ustring>;
    using lock_t = std::lock_guard<std::mutex>;

    led_label_t(Glib::ustring label = "<no data>", bool mnemonic = false)
        : Gtk::ToggleButton(std::move(label), std::move(mnemonic)),
          _values{"SEL1", "SEL2"} {}

protected:
    virtual void on_toggled(void) override {
        std::cout << "Clicked Button." << std::endl;
        lock_t lock(_mtx);
        value_changed(_values[get_active()]);
    }

    virtual void value_changed(Glib::ustring& value) {
        std::string path;
        if (get_active()) {
            path =
                "/usr/share/icons/Adwaita/16x16/emblems/emblem-important.png";
        } else {
            path = "/usr/share/icons/Adwaita/16x16/emblems/emblem-default.png";
        }
        remove();  // remove previous label
        std::cout << "Changed Label of led_label: "
                  << ", value: " << value << std::endl;
        add_pixlabel(path, value);
    }

private:
    mutable std::mutex _mtx;
    value_list_t _values;
};

int main(void) {
    auto app = Gtk::Application::create();
    Gtk::Window window;
    window.set_default_size(200, 200);

    led_label_t inst{};
    inst.show();
    window.add(inst);

    auto f = [&inst, &window]() {
        using namespace std::chrono_literals;
        boost::asio::io_service io;
        {   //wait for startup
            boost::asio::steady_timer t{io, 100ms};
            t.wait();
        }

        bool toggle = true;
        for (auto i = 0; i < 2000; i++) {
            std::cout << "i=" << i << std::endl;
            //wait until next simulated button click
            boost::asio::steady_timer t{io, 1ms};
            t.wait();
            inst.set_active(toggle);
            toggle = !toggle;
        }
    };

    std::thread c1(f);
    std::thread w([&app, &window]() { app->run(window); });
    c1.join();
    window.hide();
    w.join();
    return EXIT_SUCCESS;
}

要编译此示例,我使用以下命令:

g++ main.cpp -o main `pkg-config --cflags --libs gtkmm-3.0` -Wall -pedantic -Wextra -Werror -Wcast-qual -Wcast-align -Wconversion -fdiagnostics-color=auto -g -O0 -std=c++14 -lboost_system -pthread

我正在使用 GCC 4.9.2 和 libgtkmm-3.14(都是标准的 debian jessie)

我得到的段错误如下:

Program received signal SIGSEGV, Segmentation fault.
[Switching to Thread 0x7fffe7fff700 (LWP 7888)]
0x00007ffff6288743 in ?? () from /usr/lib/x86_64-linux-gnu/libgtk-3.so.0
(gdb) bt
#0  0x00007ffff6288743 in ?? () from /usr/lib/x86_64-linux-gnu/libgtk-3.so.0
#1  0x00007ffff6288838 in ?? () from /usr/lib/x86_64-linux-gnu/libgtk-3.so.0
#2  0x00007ffff6267ce9 in ?? () from /usr/lib/x86_64-linux-gnu/libgtk-3.so.0
#3  0x00007ffff627241b in ?? () from /usr/lib/x86_64-linux-gnu/libgtk-3.so.0
#4  0x00007ffff63a1601 in ?? () from /usr/lib/x86_64-linux-gnu/libgtk-3.so.0
#5  0x00007ffff63a154c in ?? () from /usr/lib/x86_64-linux-gnu/libgtk-3.so.0
#6  0x00007ffff63a26b8 in ?? () from /usr/lib/x86_64-linux-gnu/libgtk-3.so.0
#7  0x00007ffff644d5ff in ?? () from /usr/lib/x86_64-linux-gnu/libgtk-3.so.0
#8  0x00007ffff644d9b7 in gtk_widget_realize ()
   from /usr/lib/x86_64-linux-gnu/libgtk-3.so.0
#9  0x00007ffff644dbe8 in gtk_widget_map ()
   from /usr/lib/x86_64-linux-gnu/libgtk-3.so.0
#10 0x00007ffff621c387 in ?? () from /usr/lib/x86_64-linux-gnu/libgtk-3.so.0
#11 0x00007ffff626270f in ?? () from /usr/lib/x86_64-linux-gnu/libgtk-3.so.0
#12 0x00007ffff46bf474 in ?? ()
   from /usr/lib/x86_64-linux-gnu/libgobject-2.0.so.0
#13 0x00007ffff46d9087 in g_signal_emit_valist ()
   from /usr/lib/x86_64-linux-gnu/libgobject-2.0.so.0
#14 0x00007ffff46d99df in g_signal_emit ()
   from /usr/lib/x86_64-linux-gnu/libgobject-2.0.so.0
#15 0x00007ffff644db99 in gtk_widget_map ()
   from /usr/lib/x86_64-linux-gnu/libgtk-3.so.0
#16 0x00007ffff64506d8 in gtk_widget_set_parent ()
   from /usr/lib/x86_64-linux-gnu/libgtk-3.so.0
#17 0x00007ffff6217a9b in ?? () from /usr/lib/x86_64-linux-gnu/libgtk-3.so.0
#18 0x00007ffff79a44eb in Gtk::Container_Class::add_callback(_GtkContainer*, _GtkWidget*) () from /usr/lib/x86_64-linux-gnu/libgtkmm-3.0.so.1
#19 0x00007ffff46c253b in g_cclosure_marshal_VOID__OBJECTv ()
   from /usr/lib/x86_64-linux-gnu/libgobject-2.0.so.0
#20 0x00007ffff46bf474 in ?? ()
   from /usr/lib/x86_64-linux-gnu/libgobject-2.0.so.0
#21 0x00007ffff46d9087 in g_signal_emit_valist ()
   from /usr/lib/x86_64-linux-gnu/libgobject-2.0.so.0
#22 0x00007ffff46d99df in g_signal_emit ()
   from /usr/lib/x86_64-linux-gnu/libgobject-2.0.so.0
#23 0x00007ffff6261aa5 in gtk_container_add ()
   from /usr/lib/x86_64-linux-gnu/libgtk-3.so.0
#24 0x000000000040b0b5 in led_label_t::value_changed (this=0x7fffffffe2a0, 
    value=...) at main.cpp:38
#25 0x000000000040afb1 in led_label_t::on_toggled (this=0x7fffffffe2a0)
    at main.cpp:24
#26 0x00007ffff7a18af0 in Gtk::ToggleButton_Class::toggled_callback(_GtkToggleButton*) () from /usr/lib/x86_64-linux-gnu/libgtkmm-3.0.so.1
#27 0x00007ffff46bf245 in g_closure_invoke ()
   from /usr/lib/x86_64-linux-gnu/libgobject-2.0.so.0
#28 0x00007ffff46d083b in ?? ()
   from /usr/lib/x86_64-linux-gnu/libgobject-2.0.so.0
#29 0x00007ffff46d9778 in g_signal_emit_valist ()
   from /usr/lib/x86_64-linux-gnu/libgobject-2.0.so.0
#30 0x00007ffff46d99df in g_signal_emit ()
   from /usr/lib/x86_64-linux-gnu/libgobject-2.0.so.0
#31 0x00007ffff63ecb4d in ?? () from /usr/lib/x86_64-linux-gnu/libgtk-3.so.0
#32 0x00007ffff798a4a0 in Gtk::Button_Class::clicked_callback(_GtkButton*) ()
   from /usr/lib/x86_64-linux-gnu/libgtkmm-3.0.so.1
#33 0x00007ffff46bf474 in ?? ()
   from /usr/lib/x86_64-linux-gnu/libgobject-2.0.so.0
#34 0x00007ffff46d9087 in g_signal_emit_valist ()
   from /usr/lib/x86_64-linux-gnu/libgobject-2.0.so.0
#35 0x00007ffff46d99df in g_signal_emit ()
   from /usr/lib/x86_64-linux-gnu/libgobject-2.0.so.0
#36 0x00007ffff63ec936 in gtk_toggle_button_set_active ()
   from /usr/lib/x86_64-linux-gnu/libgtk-3.so.0
#37 0x0000000000405e12 in <lambda()>::operator()(void) const (
    __closure=0x74f4f8) at main.cpp:73
#38 0x000000000040811a in std::_Bind_simple<main()::<lambda()>()>::_M_invoke<>(std::_Index_tuple<>) (this=0x74f4f8) at /usr/include/c++/4.9/functional:1700
#39 0x0000000000407fa9 in std::_Bind_simple<main()::<lambda()>()>::operator()(void) (this=0x74f4f8) at /usr/include/c++/4.9/functional:1688
#40 0x0000000000407e9e in std::thread::_Impl<std::_Bind_simple<main()::<lambda()>()> >::_M_run(void) (this=0x74f4e0) at /usr/include/c++/4.9/thread:115
#41 0x00007ffff3f47970 in ?? () from /usr/lib/x86_64-linux-gnu/libstdc++.so.6
#42 0x00007ffff37650a4 in start_thread (arg=0x7fffe7fff700)
    at pthread_create.c:309
#43 0x00007ffff349a04d in clone ()
    at ../sysdeps/unix/sysv/linux/x86_64/clone.S:111

也许有趣的是

#24: 0x000000000040b0b5 in led_label_t::value_changed (this=0x7fffffffe2a0, 
    value=...) at main.cpp:38)

这是 add_pixlabel(path, value);被调用。

我做错了什么?

注意: 我发现,这种段错误并不总是出现,在我的台式机上,每 10 次调用就会出现一次错误。 (英特尔 i7-3xxx) 在我的笔记本电脑上,几乎每次调用时都会出现错误 (Intel i5-3xxx)

从多个线程访问和更改 UI 组件总是很棘手。 UIs 需要快速响应用户输入,因此他们不能等待后台任务完成。因此 UI 组件很少受到互斥锁或其他同步的保护。你写,它发生了。除非遇到其他事情。

如果您从两个线程写入...糟糕。

当另一个线程读取时,您正在写入一半...糟糕。

例如,当触发屏幕刷新时,线程 4 正在通过将新字符串写入标签的方式进行部分处理。如果标签的后端是 c 风格的字符串,则终止 NULL 可能已被覆盖,并且标签写入从末尾运行到错误的 RAM 中。

各种各样的事情都可能出错,有些会幸存下来,或者更糟的是,看起来像。最好将所有 UI 管理都放在一个线程中,让其他线程对 UI 线程的更新进行排队。首先查看 Model View Controller,然后根据需要尝试相关模式。

根据@user4581301的回答,我现在找到了解决办法。他是对的,gtkmm 不支持多线程。 (更准确地说,libsigc++ 和 sigc::trackable 不是线程安全的)

However, care is required when writing programs based on gtkmm using multiple threads of execution, arising from the fact that libsigc++, and in particular sigc::trackable, are not thread-safe.

Quote from gtkmm documentation.

因此我使用 Glib::Dispatcher 在 window.

的 gtkmm-Main-Loop 上下文中执行 set_label() - 方法

这是代码,它在我的机器上不再出现段错误(即使重试多次)

#include <boost/asio.hpp>
#include <boost/asio/steady_timer.hpp>

#include <cassert>
#include <iostream>
#include <thread>
#include <mutex>
#include <gtkmm/application.h>
#include <gtkmm/window.h>
#include <gtkmm/togglebutton.h>
#include <glibmm/dispatcher.h>

#define LOG()                                                              \
    std::cout << (std::chrono::system_clock::now() - start).count() << " " \
              << std::this_thread::get_id() << ": "

auto start = std::chrono::system_clock::now();

class led_label_t : public Gtk::ToggleButton {
public:
    using value_list_t = std::vector<Glib::ustring>;
    using lock_t = std::lock_guard<std::mutex>;
    using action_queue_t = std::vector<Glib::ustring>;

    led_label_t(Glib::ustring label = "<no data>", bool mnemonic = false)
        : Gtk::ToggleButton(std::move(label), std::move(mnemonic)),
          _values{"SEL1", "SEL2"} {}

    void set_dispatcher(Glib::Dispatcher* dp) {
        _dp = dp;
        _dp->connect([this](void) { dispatcher_task(); });
    }

protected:
    virtual void on_toggled(void) override {
        LOG() << "Clicked Button." << std::endl;
        {
            lock_t lock(_action_mtx);
            auto value = _values[get_active()];
            _action_queue.push_back({value});
            LOG() << "Added label into queue " << value << std::endl;
            if (_action_queue.size() > 1) {
                return;
            }
        }
        _dp->emit();
    }

    void dispatcher_task(void) {
        Glib::ustring label;
        for (;;) {
            {
                lock_t lock(_action_mtx);
                if (_action_queue.size() == 0) {
                    return;
                }
                label = *_action_queue.begin();
                _action_queue.erase(_action_queue.begin());
            }
            set_label(label);
            LOG() << "Set the label " << label << std::endl;
        }
    }

private:
    mutable std::mutex _action_mtx;
    action_queue_t _action_queue;

    value_list_t _values;
    Glib::Dispatcher* _dp;
};

int main(void) {
    auto app = Gtk::Application::create();
    Gtk::Window window;
    window.set_default_size(200, 200);

    led_label_t inst{};
    inst.show();
    window.add(inst);

    auto f = [&inst, &window]() {
        using namespace std::chrono_literals;
        boost::asio::io_service io;
        {  // wait for startup
            boost::asio::steady_timer t{io, 100ms};
            t.wait();
        }

        bool toggle = true;
        for (auto i = 0; i < 200000; i++) {
            // wait until next simulated button click
            boost::asio::steady_timer t{io, 250us};
            t.wait();
            LOG() << "i=" << i << std::endl;
            inst.set_active(toggle);
            toggle = !toggle;
            LOG() << "finished" << std::endl;
        }
    };

    std::thread c1(f);
    std::thread w([&app, &window, &inst]() {
        Glib::Dispatcher dp;
        inst.set_dispatcher(&dp);
        app->run(window);
    });
    c1.join();
    window.hide();
    w.join();
    return EXIT_SUCCESS;
}