Before we get started here, let me state that I am using Ruby 1.9.1 (I refuse to look back!), and that I have not tested this solution on Ruby 1.8.6, but it should work there as well, though I may have some 1.9-isms in my code. Should be easy enough to spot.
I am working on writing an application in Ruby that can talk to an Windows application that has an ActiveX COM Automation object exposed. Ruby is basically the wrapper so that I can access the application from the Linux side of the world. So, I am using Ruby’s DRb to bridge those worlds because, after all, I am the Linux Bloke!
Well, as you may have guessed, I ran into problems with this approach. I simply could not call the COM objects from a call initiated with DRb, though I could call them directly just fine. After scratching my head a bit, I figured it out.
The win32ole module that runs on the Windows side of the world in Ruby only wants to run in the same thread that it was started in. win32ole is simply not thread-safe, and this has to do in large part to how ActiveX works under Windows. No need to delve into the gory details as we want code that works already!
DRb is very much all about threads. The DRb Server runs in a separate thread, and threads are launched each time a DRb request comes in. Threads abound like crazy! After all, it is very clear that the implementation of DRb was based, in part, on the Java threading model and Java’s RMI. But we knew that. We know that Ruby Threads parrot Java Threads. And I’ve done a lot of work with Java Threads in the past and almost feel a bit of “déjà vu” in working with them in Ruby. Oh the days…
But I digress.
We have a major problem here. How do we get around it, without having to throw out DRb and doing something funky like writing some custom RPC bit just to make Windows happy?
Well, as you may have guess, the Linux Bloke created the very solution you need!! Funnel!
Funnel works by wrapping a given object with a “meta” object that can then be called from any thread. All the calls are actually queued up and processed by the thread the target object wants to run in. The calling threads block until the target object returns the call, and the result objects are stuffed somewhere so that the calling thread can find them.
It’s all very transparent and you need not do anything special — much. You will need to call process_funnel_messages() in the funneled thread. And you may do this once in which case process_funnel_messages() will loop forever and never return, or you can call it at regular intervals if you need to do other processing in that same thread.
You, of course, can use Funnel anywhere you need to funnel calls from multiple threads to a single thread to access something that is not inherently thread-safe or thread-aware.
The downloadable code is posted here:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 |
=begin rdoc Funnel created by Fred Mitchell (LinuxBloke.com) on 2010-06-05 =Funnel -- funnel calls to an object to a specific thread that created said object. With some systems, like win32ole, the system basically wants to run on the same thread the system was started on. To facilitate that need in a multi-threaded environment, we create the Funnel. The Funnel wrapper on an object will basically intercept all method calls and funnel those calls to the wrapped object in the thread it was created in. The caller thread will basically block until the Funnel calls the target object's method and will be given, as a return, the result object of that call. The Funnel thread will basically sit in a loop waiting for something to come in, and wake up to process the entries, then go back to sleep until the next ones come in. Any exceptions (or errors) that occur in the Funnel shall be thrown to the caller thread, as though the exception took place in that thread. This code is released under the GPLv3. =end module Funnel class Wrapper def initialize(target) @targetOb = target @targetThr = Thread.current @targetThr[:methQueue] = [] if @targetThr[:methQueue].nil? end def method_missing(meth, *parms) Thread.current[:methResult] = :nothing_yet @targetThr[:methQueue] << [@targetOb, meth, Thread.current, parms] # Thing is, we may have gotten a response already! while Thread.current[:methResult] == :nothing_yet if @targetThr.stop? @targetThr.wakeup # Thread.stop end Thread.pass end Thread.current[:methResult] end end # Called by the orginal thread to process object messages. # This function never returns. def process_funnel_messages(loop_forever = true) begin meth = nil (ob, meth, thr, parms) = Thread.current[:methQueue].shift unless Thread.current[\ :methQueue].nil? unless meth.nil? begin thr[:methResult] = ob.send(meth, *parms) thr.run rescue thr.raise($!) end else Thread.stop if loop_forever end end while loop_forever end def wrap(target) Wrapper.new(target) end end |
And here is an example of its use:
?Download example_of_funnel_use.rb
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
require 'funnel' include Funnel class StupidThreadUnsafeThing def callme puts "*** I've been called. My thread is" p Thread.current puts end end stut = StupidThreadUnsafeThing.new # This is the easy to use wrapper fstut = wrap stut stut.callme Thread.new do 10.times do |i| sleep 1 Thread.new { puts "XXX #{i} calling stut from thread" p Thread.current fstut.callme } end exit end # Here we loop forever processing messages. # Optionally, we could call this repeateady # to process messages by using a parameter of # "false". process_funnel_messages |
This code is fairly straightforward, as you can see. If there is enough interest, I’ll consider turning this into a gem.