The following program illustrates the problem. The display is a simple menu with a 'start' item on it. Selecting 'start' will begin a thread displaying ten messages into the text control; the task is just to add up a lot of numbers. Although this works, none of the messages appears until the entire 'busy_task' method is complete.
require 'wx'
class MyFrame < Wx::Frame
def initialize
super(nil, :title => "Thread example")
set_menu_bar menubar
@text = Wx::TextCtrl.new(self, :style => Wx::TE_MULTILINE)
@tasks = 0
end
def menubar
menubar = Wx::MenuBar.new
file_menu = Wx::Menu.new
about_item = Wx::MenuItem.new(file_menu, Wx::ID_ANY, "About")
evt_menu(about_item) { @text.append_text "About\n" }
file_menu.append_item about_item
start_item = Wx::MenuItem.new(file_menu, Wx::ID_ANY, "Start")
evt_menu(start_item) { busy_task}
file_menu.append_item start_item
menubar.append(file_menu, "File")
menubar
end
def busy_task
@tasks += 1
tasks = @tasks
10.times do
@text.append_text "running #{tasks} ... #{Time.new}\n"
(1...1000_000).inject :+
end
@text.append_text "Thread #{tasks} done\n"
end
end
Wx::App.run do
MyFrame.new.show
end
Threads and Timers
We can improve on the above code by placing the work within the 'busy_task' method into a thread:
def busy_task
# start task in a separate thread
Thread.new do
@tasks += 1
tasks = @tasks
10.times do
@text.append_text "running #{tasks} ... #{Time.new}\n"
(1...1_00_000).inject :+
end
@text.append_text "Thread #{tasks} done\n"
end
end
end
This will indeed work. The GUI stays responsive, and more than one thread can be active, by selecting the 'start' menu item again. However, you may find the working thread seems to stop doing anything at times; this happens if I switch focus to another window, so the ruby application is running in the background.
The second improvement is to add a Timer. This is created in the initialize method for the frame. The Timer's role is to wake up after a short period of time, and pass processing control to the next thread. In this case, I make the Timer wake up after 10 milliseconds.
def initialize
super(nil, :title => "Thread example")
set_menu_bar menubar
# -- this timer interrupts every 100ms and gets next thread active
# -- if you don't have the timer, the GUI does not update if window
# in background
timer = Wx::Timer.new(self, Wx::ID_ANY)
evt_timer(timer.id) {Thread.pass}
timer.start(10)
@text = Wx::TextCtrl.new(self, :style => Wx::TE_MULTILINE)
@tasks = 0
end
Summary
Worker Threads and a Timer to make sure the Threads are kept active is probably the key to keeping wxRuby's GUI active under large-running tasks, although I expect to have to do some experimenting in future. For better information, Mario Steele has described the principles here and in a tutorial.
Many thanks for this post.... you've solved my problem :P
ReplyDeleteBertini Carlo [WaYdotNET]
www.waydotnet.com