28 Oct 2009, 00:57
Generic-user-small

Rob Jones (10 posts)

I’ve got a command-line Ruby program that fetches various data from the web, manipulates it and does a few bits and pieces with files. I’m transplanting it into a RubyCocoa environment so that it’s more pleasant for my colleagues to use. I’m trying to figure out how to have the GUI update while all of that work is going on. For instance, I want to be able to update a text view with all the messages I’m sending its controller about progress on various tasks.

If I were writing in Java, I’d just create a new thread to do the work rather than hogging the thread that responds to GUI events. But apparently RubyCocoa doesn’t work with threads. So how do RubyCocoa programmers allow lengthy tasks to run without freezing the GUI?

Couldn’t see anything in the otherwise super-helpful Programming Cocoa with Ruby book. Couldn’t see any answers on the web either. Any suggestions?

28 Oct 2009, 10:31
Generic-user-small

Rob Jones (10 posts)

Hmm. I think I’m answering my own question here. This is an excerpt from the Mac OS X Reference Library:

Multithreading With Ruby on Mac OS X

Because Ruby in its latest stable version (the 1.8 branch) is not thread-safe, you cannot call the Ruby runtime in a thread other than the main one. When Ruby is bridged to Objective-C this creates problems because Objective-C isn’t able to call back to Ruby in a secondary thread. (If it did, an application would crash.) The version of RubyCocoa on Leopard consequently routes calls from Objective-C to Ruby so that all are on the main thread.

Ruby 1.8 also implements its threading model using the setjmp and longjmp primitives; this can sometimes cause unexpected behavior when a Ruby thread calls a Cocoa object, especially autorelease pools. Consequently, both the Ruby interpreter and the RubyCocoa bridge have been modified to properly handle these situations by saving and restoring the appropriate context variables during Ruby thread switching.

Fortunately, the next stable release of Ruby (the 2.0 branch) will be thread-safe and will use native threads. Unfortunately, this is not the version installed on Leopard.”

Seems to suggest we have to wait before a RubyCocoa app can do two things at once. That’s a little disappointing. Any workarounds?

28 Oct 2009, 11:55
Generic-user-small

Gregory Clarke (15 posts)

If all you want to do is update a progress bar and/or message, you can do that from your code by sending the displayIfNeeded message to the interface item. For example, I have the ib_outlets :progressView, :progressIndicator and :progressMessage, then if I increment the progressIndicator or change the progressMessage I just have to send @progressView.displayIfNeeded to update that part of the frozen GUI.

29 Oct 2009, 12:57
Generic-user-small

Rob Jones (10 posts)

Cool. Thanks Gregory. I’ll take a look at that. In the meantime, I’ve deployed a monstrous kludge. Seeing as how this app is just for colleagues to use, not paying customers, I’ve split it into two apps. One does the work and has a blank window; the other has all the text fields and buttons, and listens for distributed notifications about how it’s all going. And it works. The text view in one app updates with progress messages from the other app. But if I can get your suggestion working it would be a lot better, and enable me to preserve a bit of my dignity.

28 Oct 2009, 18:55
Generic-user-small

Gregory Clarke (15 posts)

I forgot to explain that my :progressIndicator and :progressMessage are inside the :progressView sheet, so I send it the displayIfNeeded. If you just had a :progressIndicator in your main interface that you needed to increment, you can call @progressIndicator.displayIfNeeded to update it. You’ll still get a “spinning beachball” while your app does the work, but at least the interface will update.

29 Oct 2009, 08:04
Generic-user-small

Rob Jones (10 posts)

I zipped my two apps back into one and I’m in business, thanks. It’s a bit stuttery in places, but I do see a stream of progress messages in the text view as the app bustles around doing things.

Thanks again. Allowing for gaps, this is about day four since I got your book and I’ve already got my first Ruby Mac app looking pretty professional (on the surface at any rate). Never written anything for a Mac before so I’m pretty pleased.

08 Dec 2009, 18:46
England-small_pragsmall

Brian Marick (56 posts)

The 0.5 release of MacRuby (http://macruby.org) will support native threads. It’s currently in second beta. The switch from RubyCocoa to MacRuby ought to be pretty painless, though I’m waiting for 0.5 final myself.

07 Apr 2010, 23:34
Generic-user-small

Gregory Clarke (15 posts)

Even more exciting - I’ve recently found out that MacRuby will support Grand Central Dispatch: http://www.macruby.org/documentation/gcd.html

01 Dec 2011, 00:59
Generic-user-small

Dave Howell (2 posts)

Having the UI freeze up has been the bane of my Mac programming for years, first using AppleScript Studio, then RubyCocoa. I didn’t even think about trying to multi-thread it until this most recent app exasperated me again, but it seems like massive overkill. “Yes, we’ve got some thinking to do, but when I ask you to update the window, do it, then come back for more thinking!” But calling any and all of the various ‘.display’ options was utterly useless, because Cocoa won’t (or at least, never did for me) update the display until it goes through the event-processing run loop, and it wasn’t doing that until my processing subroutine exited, no matter how much I begged it during the routine to take a little break and update the UI.

I have finally found the secret solution, rather by accident. Certainly nobody (that I could find) ever mentioned this in a forum online, nor did the Apple documentation seem to notice the problem nor provide a solution. Even though I have thought for years that what I wanted to do was tell it “Fine, if you won’t update the screen until you’re back in the run loop, then go do that! Go take a trip through the loop, then come back” I could never find anything that would tell me how. It’s all taken care of for me, you know.

Today, at long last, I have found the secret command to do exactly that: NSRunLoop.currentRunLoop.runMode_beforeDate_(NSDefaultRunLoopMode, nil)

This tells Cocoa to take a single pass through the event loop. I don’t have to call “.displayIfNeeded” or anything else; all the UI items that are connected to outlets or bound to variables or whatnot are updated just like usual.

Because, really, I shouldn’t need threads or forcing a display run (even if it had ever worked for me) for something like

@ reportStatus(“Loading File..”)@ @loadFile@ @reportStatus(“Parsing File…”)@ @parseFile@ @reportStatus(“Extracting tasty bits…”)@ @extractBits@

and so on

01 Dec 2011, 01:08
Generic-user-small

Dave Howell (2 posts)

In case anybody’s wondering why I’m still using RubyCocoa instead of MacRuby, well, I have six Macs currently powered up and doing useful things for me, and three or four more in storage that I could deploy if needed, and exactly one of those machines has an Intel processor. My little TiBook for playing old games, the two Mac Cubes (one’s my alarm clock, the other’s my VoIP phone) and my dual-G4 XServe are all PowerPC machines, and thus must run 10.4. Ergo, RubyCocoa.

{rant duration=’brief’} You’d think using the One-click Ruby Installer for 10.4 would be a great way to get Ruby, Gems, RubyCocoa, and some other stuff on an old machine like that. It’s kinda unfortunate that they hard-coded “-arch i386” into the rbconfig.rb file; it really messes up any attempt to install gems. {/rant}

  You must be logged in to comment