Search code examples
rubyqtsignalsfreezeemit

ruby gem qtbindings' emit makes app freeze


My app freezes when I use emit to update status on GUI.

I want to know the reason or how to avoid this freeze. Thanks for your review.

My test environment

The demo app is present below.

#!/usr/bin/env ruby
# encoding: UTF-8
#

require 'Qt'

class App < Qt::MainWindow
    signals 'test()'
    slots   'on_test()'

    def initialize
        super

        @label = Qt::Label.new
        self.centralWidget = @label

        self.show

        connect self, SIGNAL('test()'), SLOT('on_test()')
        start_count
    end

    def start_count
        Thread.new do
            loop {
                emit test()
            }
        end
    end

    def on_test()
        @label.text = @label.text.to_i + 1
    end
end

app = Qt::Application.new(ARGV)
App.new
app.exec

@hyde Thank you for you answer.

Solution 2 of qtbindings seems to be no help.

connect self, SIGNAL('test()'), SLOT('on_test()')
=> 
connect self, SIGNAL('test()'), SLOT('on_test()'), Qt::BlockingQueuedConnection

Solution 1 was tested and the app runs fluently.

The code of Solution 1:

#!/usr/bin/env ruby
# encoding: UTF-8
#

require 'Qt'

class App < Qt::MainWindow
    slots   'on_test()'

    def initialize
        super

        @label = Qt::Label.new
        self.centralWidget = @label

        self.show

        @c = Qt::AtomicInt.new

        start_count
        start_timer
    end

    def start_count
        Thread.new do
            loop {
                @c.fetchAndAddRelaxed(1)
            }
        end
    end

    def start_timer
        t = Qt::Timer.new(self)
        t.start(16)
        connect t, SIGNAL('timeout()'), SLOT('on_test()')
    end

    def on_test()
        @label.text = @c.fetchAndAddRelaxed(0) + 1
    end
end

app = Qt::Application.new(ARGV)
App.new
app.exec

Solution

  • Speculation on reason: the Qt main thread event loop never gets to process application events, because it is too busy delivering queued signals. The signals are queued, because they are between threads, so slot gets called in the right thread, independent of the emitting thread (and they must be queued in this case, because slot manipulates GUI objects, which is only allowed from the main thread)


    There are a few alternatives to solve this. I don't really know Ruby, or its Qt bindings, so here's just a rough outline:

    Solution 1: let the thread loop go around as fast as it can:

    1. Create an atomic variable. Either use some Ruby type, or QAtomicInt (I assume that's possible with the Ruby Qt bindings).

    2. In your thread loop, just increment the atomic variable, instead of emitting a signal.

    3. Add a QTimer for updating the @label.text at desired intervals. If the user is supposed to be able to read the number, I'd suggest something like 500 ms interval. Minimum sensible interval is something like 16 ms for ~60 fps update rate.

    4. Connect the timer timeout to on_test and get the value of the atomic integer to update the text.

    That way updating of the numeric value is independent of displaying it.

    Solution 2: make the thread block until emitted signal is actually delivered:

    Use connection type BlockingQueuedConnection (note: do not use with single thread). Add that connection type to the signal connection statement (however you do it with Ruby Qt). Then the emitting thread will block until the target slot is actually called, so the signals will only be emitted at the rate they can be processed, no faster.