Resolvendo (parcialmente) o problema de threads no QtRuby4

Bem, comecei a brincar com QtRuby e ja conheci o problema de threads: o QtRuby não tem suporte a threads ainda, nada da GUI pode ser alterada fora da thread mãe e as threads do ruby não rodam enquanto a GUI está sendo executada.

É nessa hora que juntamos todo o nosso conhecimento e o colocamos em prática junto com a habilidade de POG.

Primeiramente vamos criar a nossa classe Qt::Thread POG-style

class Qt::Thread < ::Thread
    class ThreadsTimer < Qt::Object
        protected
        def timerEvent(ev)
        end
 
        def initialize
            super
            startTimer 0
        end
    end
 
    @threads_timer = nil
 
    def Thread.enable_ruby_threads
        # Start timer if only it hasn't been started yet
        return if @threads_timer
 
        @threads_timer = ThreadsTimer.new
    end
    private_class_method(:enable_ruby_threads)
 
    def Thread.disable_ruby_threads
        # Stop timer only if we have main thread and just one custom
        # which will be stopped right after this call
        return unless Thread.list.count == 2
 
        @threads_timer.dispose
        @threads_timer = nil
    end
    private_class_method(:disable_ruby_threads)
 
    def self.decorate_block(*args, &block)
        enable_ruby_threads
        proc{ block.call(*args); disable_ruby_threads }
    end
    private_class_method(:decorate_block)
 
    def self.new(*args, &block)
        super(*args, &decorate_block(*args, &block))
    end
 
    def self.start(*args, &block)
        super(*args, &decorate_block(*args, &block))
    end
 
    def self.fork(*args, &block)
        super(*args, &decorate_block(*args, &block))
    end
end

O Qt trava a VM do ruby impedindo que as threads normais rodem, o jeito para ‘burlar’ isso é criando um timer, essa classe tem um timer próprio que é iniciado(caso esteja parado) quando você chama o new/start/fork e é parado quando uma thread acaba(caso não haja outras threads rodando), evitando assim o alto processamento desnecessário que o timer produz.

Para mexer na GUI de dentro de uma thread vamos criar a classe Qt::MainThread

class Qt::MainThread
 
    def initialize(*a, &blk)
        self.class.threads << [blk, a] if block_given?
    end
 
    def self.threads
        @@threads ||= []
    end
 
end

O que essa classe faz é basicamente pegar todo o bloco e argumentos enviados para ela e armazenar em uma array.

Essa ‘thread’ deverá ser usada apenas quando você precisar fazer alterações na GUI porque ela irá rodar na thread mãe e irá bloquear a GUI.

Agora, em um widget qualquer(eu prefiro colocar no ‘MainWindow’)

def initialize
  #...
  startTimer(1000)
end
 
def timerEvent(ev)
  until Qt::Thread.threads.empty?
    block, args = Qt::Thread.threads.shift
    block.call(*args)
  end
end

Uma vez por segundo o evento timerEvent é chamado e verifica se existem blocos a serem rodados no Qt::MainThread.

Um exemplo de como usar esse código:

label = Qt::Label.new 'Espere, fazendo uma coisa demorada.'
Qt::Thread.new do
  retorno = faca_alguma_coisa_demorada
  Qt::MainThread.new do
    label.setText('TERMINADO!')
  end
end

Devido ao fato das threads do ruby só executarem por causa do timer, elas serão um pouco mais lentas. Se você aumentar o tempo do timer elas se tornam mais lentas aindas.

Deixe uma resposta