Skip to content

HowTo : Events and threading

Martin Corino edited this page Mar 28, 2024 · 2 revisions
     About      FAQ      User Guide      Reference documentation

Events and threading

Using Ruby threads with wxRuby is definitely possible but requires taking care of two key issues:

  1. provide Ruby threads with the necessary time slices to process
  2. provide a means for Ruby threads to post events to update GUI elements

Allowing Ruby threads to run

The threading model implemented by Ruby will cause context (thread) switching at certain moments in the processing of Ruby application code; most prominently when calling a method and when returning from a method. This means that ordinarily processing time would be fairly distributed among threads (depending on the code executed in those threads).

With wxRuby however processing occurs only partly within Ruby context. The main application thread runs the application's event loop which is natively implemented and will call any registered event handlers whenever events are received. Those event handlers with wxRuby applications will most often be Ruby code (but not always in case of standard event handlers for various widgets) calling other (Ruby) methods. Calling event handlers would thus most likely cause regular context (thread) switches.
There is however no guarantee that the event loop will receive regular events to handle as this is mostly dependent on user interaction (mouse clicks, key presses) and in fact it could easily occur that the event loop is inactive for significant periods (at least in computing terms). During such periods there would be no (or only sporadic) calls into Ruby code causing any threads spawned from the main thread to be mostly dormant which is probably not an acceptable situation.

Fortunately we can solve this by using wxRuby Wx::Timer objects. By registering simple repetitive timers we can explicitly cause very regular context (thread) switches without having to rely on user instigated events. The following example shows a global application timer achieving just that.

class MyApp < Wx::App
  
  def on_init
    # Register a global timer to provide explicit context switches.
    # The timer will run every 1/40 second (25ms). Higher values
    # will make the other threads run more often, but will
    # eventually degrade the responsiveness of the GUI.
    Wx::Timer.every(25) { Thread.pass }
  end
  
end

Of course you could also apply timers more fine grained if you only have threads started under certain conditions and/or for a limited period.

Allowing Ruby threads to update the GUI

As the event handling mechanism (event loop) in wxRuby is designed for single thread use it is not a good idea to synchronously 'send' events from Ruby threads (other than the main thread) as that may (likely) cause race conditions. Thus, updating the GUI from Ruby threads (other than the main thread) requires (asynchronously) posting events in the event queue that the event loop in the main thread (when active again) can than detect and call any registered event handlers for. The event posting functionality is thread safe.

In principle posting existing, standard, events may all that is needed in very simple cases. If so, code like the following in a Ruby thread might suffice:

Thread.new do

  # do something important in this thread ... 

  # post an EVT_UPDATE_UI to trigger an evt_update_ui installed handler
  # which can check if and what to update
  frame.event_handler.queue_event(Wx::UpdateUIEvent.new(frame.id))
end

In most cases however matters quickly become more complex added needs like communicating context specific data and more controlled event targeting.

There are basically two approaches to solve this:

  1. define and use custom events to post and handle;
  2. use the generic asynchronous processing support offered by Wx::EvtHandler#call_after.

Using custom events

The following code shows a custom event class for use in threaded update signaling.

# A custom type of event associated with a target control. Note that for
# user-defined controls, the associated event should inherit from
# Wx::CommandEvent rather than Wx::Event.
class ProgressUpdateEvent < Wx::CommandEvent
  # Create a new unique constant identifier, associate this class
  # with events of that identifier, and create a shortcut 'evt_update_progress'
  # method for setting up this handler.
  EVT_UPDATE_PROGRESS = Wx::EvtHandler.register_class(self, nil, 'evt_update_progress', 0)

  def initialize(value, gauge)
    # The constant id is the arg to super
    super(EVT_UPDATE_PROGRESS)
    # simply use instance variables to store custom event associated data
    @value = value
    @gauge = gauge
  end

  attr_reader :value, :gauge
end

This custom event class can than be used to asynchronously queue events (using Wx::EvtHandler#queue_event) like this:

# show ten gauges
10.times do |gauge_ix|
  gauge = Wx::Gauge.new(panel, :range => STEPS)
  # For each gauge, start a new thread in which the task runs
  Thread.new do 
    # The long-running task
    STEPS.times do | i |
      sleep rand(100) / 50.0  # perform an iteration of the long-running task 
      # Update the main GUI asynchronously
      frame.event_handler.queue_event(ProgressUpdateEvent.new(i+1, gauge_ix))
    end
  end
  @gauges << gauge
  sizer.add(gauge, 0, Wx::GROW|Wx::ALL, 2)
end

Using asynchronous execution

This involves using the Wx::EvtHandler#call_after method to schedule arbitrary code (Proc, lambda, block or Method) for asynchronous execution. In actuality this method queues a standard event with the code to be executed contained as a data member. The event loop in the main thread will execute this contained code (which maintains it's execution scope due to Ruby 'magic') on detection of the event after any other events have been handled.

Using this method the example above could be written as follows:

10.times do |gauge_ix|
  gauge = Wx::Gauge.new(panel, :range => STEPS)
  # For each gauge, start a new thread in which the task runs
  Thread.new do 
    # The long-running task
    STEPS.times do | i |
      sleep rand(100) / 50.0  # perform an iteration of the long-running task
      # Update the main GUI asynchronously by scheduling the Frame's #update_gauge method with arguments `gauge_ix` and `i+1`
      frame.event_handler.call_after(:update_gauge, gauge_ix, i+1)
    end
  end
  @gauges << gauge
  sizer.add(gauge, 0, Wx::GROW|Wx::ALL, 2)
end

For a complete example regarding wxRuby threading see the threaded example distributed with wxRuby3.

Clone this wiki locally