HowTo : Events and concurrency - mcorino/wxRuby3 GitHub Wiki

     About      FAQ      User Guide      Reference documentation

Events and Concurrency

Ruby (>= 3.0) essentially offers three types of concurrency models:

  1. the Thread class offering a (more or less) preemptively scheduled, green, threading model
  2. the Fiber class offering a cooperative concurrency model
  3. the Ractor class offering a 'true' parallel executing threading model (quotes explained below)

Green Threads

The Ruby Thread class creates a separate native thread (Posix or Win32) for each Thread instance running the code for that instance from that thread. However the Thread scheduler uses the GVL (Global VM Lock) to assure only one thread will ever run at any time. In regular Ruby applications the scheduler would normally take care of thread switching preemptively based on certain specific states of the application like waiting for IO for example (possibly assisted by user code explicitly passing control at certain points in the application). In wxRuby3 however the application often cycles idly in the main event loop which is (normally) run by wxWidgets and which has no knowledge of Ruby's Thread scheduler requirements which would block Thread switching at these times allowing only the main thread to run.

To solve this 'event loop idling' problem wxRuby3 implements a custom event loop that takes the Ruby threading requirements into account. The result is worry free 'preemptive' thread scheduling when using Ruby Thread instances.

Secondly there is the issue of how to update GUI elements from a Ruby thread. wxRuby3 applications, like wxWidgets, are essentially single threaded applications and most methods are not thread-safe. So a multithreaded wxRuby3 application needs a thread-safe way to update the GUI elements from a thread other than the main thread.

Allowing Ruby threads to run

To allow Ruby threads to be scheduled to run wxRuby3 implements a custom main event loop that makes sure to provide the Ruby thread scheduler opportunity to switch threads when the event loop is 'idling' (not dispatching/processing events). This is a transparent process that does not require any user supplied support.

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. Also it is important that the main thread running the GUI event loop does not get blocked waiting for update events from any other threads.
Thus, updating the GUI from Ruby threads (other than the main thread) requires thread-safe (asynchronously) posting of 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. To do this wxRuby provides the threadsafe Wx::EvtHandler#queue_event and Wx::EvtHandler#call_after methods.

In principle posting existing, standard, events may be 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 adding 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.

In addition, Ruby Thread::Queue can be used to safely communicate between threads.

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 then 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 and threading see the threaded example distributed with wxRuby3.

Using Thread::Queue

There is yet another option to provide feedback data from Ruby worker threads to the main thread and that is using Ruby's own thread-safe Thread::Queue class in combination with either timer or idle events.

In this case the worker threads would 'post' updates in a Thread::Queue instance shared with the main thread like this:

@queue = Thread::Queue.new
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(@queue) do |queue| 
    # 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 queueing the Frame's update arguments `gauge_ix` and `i+1`
      queue << [gauge_ix, i+1]
    end
  end
  @gauges << gauge
  sizer.add(gauge, 0, Wx::GROW|Wx::ALL, 2)
end

An event handler for some timer event or the idle event could then retrieve the update(s) from the queue in the main thread and update the GUI. As it is important not to block the event loop for too long (to keep the application responsive) the update(s) should be retrieved by a 'non-blocking' #pop, #deq or #shift.

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

Fibers

Ruby fibers do not create separate threads but implement a cooperative concurrency model based on restartable code blocks. Running Ruby fibers requires user supplied control code to resume and wait for fibers for as long as needed to complete the tasks run by the fibers.

In wxRuby this control can be handled by the handlers for regularly occurring events like the Idle event or Timer events.

As fibers do not run in separate threads there are no special thread safety requirements for fibers (as long as they are not run in a separate Thread or Ractor). However, the fact that there is no preemptive scheduling does increase the risks for event loop blocking.
Any fiber that runs a long-lasting task will block the main thread (and with that in wxRuby3 the event loop) when the fiber is resumed and allowed to run its course without yielding. To prevent blocking the GUI parts of the application it is therefor imperative to organize the tasks in short lasting cycles while yielding in between cycles. Thus allowing the main thread time to resume the event loop before resuming a fiber again.

Basically this could look like:

10.times do |worker|
  # For each worker, start a new fiber in which the task runs
  Fiber.new do 
    # The long-running task
    STEPS.times do | i |
      sleep rand(100) / 50.0  # perform an iteration of the long-running task
      # update and yield
      Filber.yield i+1
    end
  end
end

An event handler for some timer event or the idle event could then regularly resume and retrieve the fiber update(s) in the main thread and update the GUI (which could also be done from within the fiber and then yield without value).

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

Ractors

The Ruby Ractor class provides a relatively new (and still unfinished) threading option.

Like the Thread instances Ractor instances run tasks in separate native threads but in this case without acquiring a GVL to prevent running multiple threads in parallel (in fact there exists a separate GVL for each Ractor thread). In principle this provides the option of true parallelism for Ruby applications.
'In principle' because the 'gotcha' lies in the fact there is a still another global lock higher up in the Ruby VM hierarchy that needs to be acquired for certain VM-global sensitive operations which will prevent all but a single Ractor instance to run.

In regular Ruby applications this would only ever be for very short time slices that would not be really noticeable in the parallel execution of the Ractor threads. Unfortunately it turns out that when calling a native method supplied by any native extension (like wxRuby3) the global VM-lock will stay acquired by the Ractor thread executing the call for as long as the call lasts. With long-running methods like the event loop method provided by wxRuby3 that would mean (like with regular Thread tasks, see above) that (at least) while idling all other Ractor threads would be blocked essentially cancelling effective parallelism.

The custom event loop implemented by wxRuby3 (see Green threads) solves this 'event loop idling' problem for Ractor threads as well though. The result is worry free 'parallelism' for Ractor threads.

Allowing Ruby Ractor threads to run

To allow Ruby Ractor threads to be scheduled to run wxRuby3 implements a custom main event loop that makes sure that the Ractor threads are allowed to run. This is a transparent process that does not require any user supplied support.

Allowing Ruby threads to update the GUI

Ractor threads require the same thread safety precautions as the Ruby green threads do with the added restriction that Thread::Queue can not be used to communicate between Ractor threads. Instead of Thread::Queue Ractor threads have (a) built-in communication port(s) to exchange data in a threadsafe way.

As it is not possible to receive data from Ractor ports in a non-blocking way it would be however strongly advisable to use the threadsafe event posting methods of wxRuby3 (see above). The object sharing restrictions of Ractor threads however, do not allow to pass wxRuby event handler instances (windows or otherwise) from the main thread to worker threads.
To work around this problem wxRuby3 provides the custom Wx::EvtHandler#make_shared extension method for the Wx::EvtHandler class which will return a Ractor 'shareable' event handler reference. This reference is an instance of the custom extension class Wx::RT::SharedEvtHandler which provides only two methods Wx::RT::SharedEvtHandler#clone and Wx::RT::SharedEvtHandler#queue_event.

The Wx::RT::SharedEvtHandler#clone method creates a duplicate, shareable, reference for the same event handler (Window or otherwise).

The Wx::RT::SharedEvtHandler#queue_event method allows to asynchronously queue events to be handled in the main threads event loop. The events queued must be Wx::RT::ThreadEvent (or derived) instances.

NOTE: To ensure thread safety and allow for Ractor shareability issues the events posted by this method will not maintain any Ruby state (instance variable, object identity). Only the core (C++) event state will be transferred. The main purpose of derived thread events is therefor the possibility to define and handle user defined event types.

Using these methods data can either be communicated using events or events can be used to communicate availability of data in a Ractor port in a non-blocking way like this:

# show ten gauges and start ten Ractor worker threads each updating one gauge
@gauges = []
@workers = []
10.times do |worker|
  gauge = Wx::Gauge.new(panel, :range => STEPS)
  pin = Ractor::Port.new
  pmon = Ractor::Port.new
  # pass in a shareable reference to the frame window containing the gauges
  r = Ractor.new(worker, self.make_shared, pin) do |worker_id, evt_handler, pout|
    # 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
      pout.send(i+1) # send data over the Actor output port (non-blocking)
      # create a thread event to signal the frame in the main thread
      evt = Wx::RT::ThreadEvent.new
      evt.set_int(worker_id) # pass the worker (gauge) id we're updating 
      evt_handler.queue_event(evt) # asynchronously signal frame
    end
    pout.send(-1) # send 'ready' data
    # signal the frame in the main thread
    evt = Wx::RT::ThreadEvent.new
    evt.set_int(worker_id)
    evt_handler.queue_event(evt)
  end
  r.monitor(pmon) # start up the monitoring port
  @gauges << gauge
  @workers << [r, pin, pmon]
  sizer.add(gauge, 0, Wx::GROW|Wx::ALL, 2)
end

# a thread event handler handles the posted updates  
def on_thread_event(evt)
  w = evt.get_int
  step = @workers[w][1].receive # #receive is blocking but at this point we now know data is waiting
  if step >= 0
    update_gauge(w, step)
  else
    rc = @workers[w][2].receive # check the monitoring port now we know the Actor should have finished
    Wx.message_box("Worker ##{w} aborted with error.", 'Worker Error',
                   Wx::OK|Wx::CENTRE|Wx::ICON_ERROR, self) if rc == :aborted
    @workers[w] << :stopped
  end
end

NOTE: As the Ractor interface is still in development API differences between Ruby versions exist. The sample snippet above shows code compatible with the Ruby 4.0 interface. Ruby 3.x requires slightly different coding. The full example mentioned below provides sample coding for both versions.

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

⚠️ **GitHub.com Fallback** ⚠️