TweetFollow Us on Twitter

Threaded Apple Events

Threading Apple Events

Or: How I Learned to Stop Worrying and Love to Bomb

By Grant Neufeld, InfoDesign Corporation

Note: Source code files accompanying article are located on MacTech CD-ROM or source code disks.

As Jon Wiederspan points out elsewhere in this issue, threading is becoming a serious problem for webmasters intent on getting speed out of their servers. In C the problem is compounded because the AppleEvent Manager does not handle multiple simultaneous events (and Apple events are used to communicate with CGI applications). Grant Neufeld has developed a solution to this problem and distributes a CGI framework that implements it for you. In this article, he shows us how it's done.

On the Thread Manager, see also:

For Frontier as a possible solution to threading difficulties, see MacTech Magazine 12.1 (January 1966) 63-65; see also the technologies discussed by Jon in our MacWorld Expo report. On writing your CGI in C, see MacTech Magazine 11.9 (September 1995) 33-42.

The Problem of Reentrancy

As I recently discovered, trying to combine Apple events with the Thread Manager is an awkward and mind-altering experience. There are some fundamental - and obscure - requirements that are easy to miss (or so I'd like everyone to believe, so that I don't have to be so embarrassed by my own oversight).

The fundamental problem is that AEProcessAppleEvent is non-reentrant. So, if you call it, you can't call it again until the event finishes processing. I didn't know this when I originally designed the threading for my CGI framework, so I happily set about an implementation whereby I could call AEProcessAppleEvent multiple times before finishing the first call - all through the glory of the Thread Manager. My incorrect code looked like:

/* warning: bad code! Don't try this at home! */
/* Called from main event loop when a high level event arrived. */
void doHighLevelEvent ( EventRecord *theEvent )
{
 OSErr     theErr;
 ThreadID  theThread;
 
 if ( gHasThreadMgr ) {
// MyNewThreadFromPool is just a custom method to simplify
// using threads from a pool.
// Remember: this particular example of Apple event threading
// is wrong - don't do it this way!
 theErr = MyNewThreadFromPool ( doAEThread, theEvent,
 (void**)nil, &theThread );
 }
 if ( !gHasThreadMgr || (theErr != noErr) ) {
// If threading isn't available, or the attempt to thread failed,
// process the Apple event without threading.
 theErr = AEProcessAppleEvent ( theEvent );
 }
}

/* The thread entry function that was used to process the Apple event. */
pascal void * doAEThread ( void *theEvent )
{
 OSErr     theErr;
 ThreadID  currentThread;
 
 theErr = AEProcessAppleEvent ( (EventRecord *)theEvent );
 
 GetCurrentThread ( &currentThread );
 DisposeThread    ( currentThread, (void *)theErr, true );
 
 return (void *)theErr;
}

The amazing thing is that nobody - including me - caught the error until Wayne K. Walrath took a look at the code (in the eleventh release) and noticed my mistake. By then, programs were already shipping and in commercial use - including my own Random URL CGI.

So, why didn't anyone notice the code crashing? The code we all had in our threads happened to be fast enough to finish before new Apple events came in. This doesn't mean it would never crash; given a situation where two Apple events came in almost simultaneously, the code would certainly flop.

Thankfully, I had the prescience to include a suitable disclaimer and label the code "beta" and "subject to errors" to cover my legal posterior. However, my public esteem was in jeopardy! Worse still, I couldn't touch the code for a few days after receiving the bug report because of job obligations. Actually, that was a good thing because the time spent thinking about the code gave me a good basis to start from when I did actually get down to work.

Moral: Think before you code. (Carefully reading the documentation isn't a bad idea either!)

Suspending Apple Events

My original code attempted to thread every Apple event. This is not the case with the corrected code. A new thread should only be initiated within the context of an AEProcessAppleEvent call if the Apple event has been successfully suspended. Suspension of Apple events involves calls to the functions AESuspendTheCurrentEvent and AEResumeTheCurrentEvent.

If the Apple event does not suspend, you can't make any further calls to AEProcessAppleEvent, so you must not create a sub-thread unless you implement a mechanism to prevent calls to AEProcessAppleEvent until the current Apple event finishes. If you want to be creative, you can write your own Apple event dispatcher to allow reentrancy, thereby making it easier to deal with threads. People might call you crazy, but you can do it.

Which brings us to the obligatory complaint about lack of sufficient (and clear) documentation (which is part of the reason I'm writing this). Inside Macintosh: Interapplication Communication doesn't sufficiently cover the use of the necessary calls and provides no sample code for suspending Apple events (see IM:IAC 4.85-88). Hopefully, this article helps fill that gap.

The CGI Apple event is the only one I'm concerned about threading for my framework, so I didn't bother with any of the other events. I handle the Apple event arrival in the normal way with a handler installed to be called by AEProcessAppleEvent:

/* Apple event Handler for the CGI WWW sdoc event */
pascal OSErr
CGIAESearchDoc ( AppleEvent *theAppleEvent,
 AppleEvent *theReply, long Reference )
{
 OSErr     theErr;
 CGIHdl    theCGIHdl;
 ThreadID  theThread;
 
// Allocate the CGIHdl data structure - zeroing out its contents.
 theCGIHdl = (CGIHdl)
 MyNewHandleClear ( sizeof(CGIrecord), &theErr );
 if ( theCGIHdl == nil ) {
 return theErr;
 }
 
// Store references to the apple event and reply records.
 (*theCGIHdl)->appleEvent = *theAppleEvent;
 (*theCGIHdl)->replyEvent = *theReply;
 
 if ( gHasThreadMgr ) {
// It is necessary to suspend the Apple event in order to thread its processing
// because of some real weirdness with AEProcessAppleEvent not being
// "reentrant." This means you can't be processing multiple Apple events at
// the same time, so they have to be ‘suspended' if you want to deal with more
// than one (I.E., multi-threaded processing).
 theErr = AESuspendTheCurrentEvent ( theAppleEvent );
 if ( theErr == noErr) {
 (*theCGIHdl)->suspended = true;
// Apple event has been suspended, so we can spawn a thread for processing.
 theErr = MyNewThreadFromPool (
 CGIAESearchDocProcessThread, theCGIHdl,
 (void**)nil, &theThread );
 }
 }
 
 if ( !gHasThreadMgr || (theErr != noErr) ) {
// If threading isn't available, or the attempt to thread failed, or the attempt to
// suspend the Apple event failed, process the Apple event without threading.
 theErr = cgiAESearchDocProcess ( theCGIHdl );
 } else {
// We suspended the Apple event, and spawned the thread, now let's start it.
 YieldToThread ( theThread );
 }
 
 return theErr;
}

The support for threading begins in the CGIAESearchDoc function. First, I confirm the presence of the Thread Manager. If it is available, I attempt to suspend the Apple event so that it will be safe to process subsequent Apple events before finishing the current one. If the suspension is successful, I call my thread function for the event, which in turn calls the function that does the actual work of handling the event request.

/* Entry point for CGI handler thread. threadParam must not be nil. */
pascal void *
CGIAESearchDocProcessThread ( void *threadParam )
{
 OSErr     theErr;
 CGIHdl    theCGIHdl;
 ThreadID  currentThread;
 
// The threadParam is used to pass the CGIHdl.
 theCGIHdl = (CGIHdl)threadParam;
 
 theErr = cgiAESearchDocProcess ( theCGIHdl );
 
// Find the ID of current thread and use DisposeThread to dispose of it so that 
// my custom thread termination procedure will be used to recover this thread's
// allocation for the thread pool.
 GetCurrentThread ( &currentThread );
 DisposeThread ( currentThread, (void *)theErr, true );
 
// This line below is actually irrelevant, since the DisposeThread call above
// will result in the immediate termination of this thread.
// I keep it in because a return result is needed for the compiler not to issue a
// warning (and I have the "treat all warnings as errors" flag set in my
// compiler, like every programmer should).
 return (void *)theErr;
}

The basic flow of my CGI Apple event handling (if we ignore the threading) starts with CGIAESearchDoc which leads to cgiAESearchDocProcess (which calls CGIAEResumeComplete if the Apple event is suspended), finishing with cgiAEComplete. This structure allows the necessary threading and Apple event "wrapper" functions to be put around the cgiAESearchDocProcess. If the Apple event is successfully suspended, an attempt is made to create the thread, which then carries the remainder of the processing. If either the suspend or thread fails, processing will still occur - just without threading.

Resuming the Apple event

cgiAESearchDocProcess checks the suspension of the Apple event to determine how it should call the completion function. If the Apple event was suspended, it must be resumed using AEResumeTheCurrentEvent before cgiAEComplete can be called.

/* Process the CGI WWW sdoc Apple event.
    theCGIHdl must be valid (non-nil) and unlocked. */
static OSErr
cgiAESearchDocProcess ( CGIHdl theCGIHdl )
{
 OSErr       theErr;
 AppleEvent  theAppleEvent;
 
// Copy the AppleEvent record pointer into a local variable for faster access.
 theAppleEvent = (*theCGIHdl)->appleEvent;
 
// The following section (not shown in this listing) is where the parameters are
// pulled from the CGI Apple event and allocated in the CGI Handle.
// That is followed by a call to the application specific CGI handler function.
 
 if ( (*theCGIHdl)->suspended ) {
// We're in a suspended Apple event, so we'll need to resume the
// Apple event to have it complete and return the reply properly.
 theErr = AEResumeTheCurrentEvent (
 &theAppleEvent, &((*theCGIHdl)->replyEvent),
 vCGIAEResumeCompleteUPP, (long)theCGIHdl );
 } else {
// We weren't suspended, but still need to take care of the Apple event
// reply record.
 theErr = cgiAEComplete ( theCGIHdl );
 }
 
 return theErr;
}

/* Call the event completion function (cgiAEComplete) when resuming
     suspended Apple events. theReference must be a CGIHdl. */
pascal OSErr
CGIAEResumeComplete ( const AppleEvent *theAppleEvent,
 AppleEvent *theReply, long theReference )
{
 OSErr  theErr;
 
 theErr = cgiAEComplete ( (CGIHdl)theReference );
 
 return theErr;
}

/* Complete the CGI Apple event. theCGIHdl must be valid. */
static OSErr
cgiAEComplete ( CGIHdl theCGIHdl )
{
 OSErr  theErr;
    
 HLock ( (Handle)theCGIHdl );
 
 if ( (*theCGIHdl)->responseData != nil ) {
// If the user's "MyCGIProcess" function set the responseData properly, return it.
 theErr = AEPutParamPtr ( &((*theCGIHdl)->replyEvent),
 keyDirectObject, typeChar,
 (Ptr)((*theCGIHdl)->responseData),
 (*theCGIHdl)->responseSize );
 } else {
// If the user's "MyCGIProcess" failed to set the responseData properly,
// return an error header.
 theErr = AEPutParamPtr ( &((*theCGIHdl)->replyEvent),
 keyDirectObject, typeChar, (Ptr)gHTTPHeaderErr,
 gHTTPHeaderErrSize );
 }
 
 HUnlock ( (Handle)theCGIHdl );
    
 cgiDisposeHandle ( theCGIHdl );
 
 return theErr;
}

Using Preallocated Threads

One thing to be concerned about is overloading on threads. Having a handful of threads available can improve performance, but running dozens of threads can bog things down - especially in terms of memory usage, since each thread has its own stack allocated in the application heap. Because of this, creating threads can quickly eat up a significant chunk of memory. You can reduce this somewhat by calculating the maximum total space you will need for the thread stack and using that value in place of the default when allocating threads. However, memory use by threads will still be a significant matter.

A good strategy is to preallocate a limited number of threads and use only those. This has the double advantage of increasing the thread allocation speed and reducing heap fragmentation. In your application initialization sequence, you'll need to allocate a pool of threads after confirming that the Thread Manager is available.

// Pre-allocate the required number of threads.
// kStartupThreadsPreallocate is a constant defined in "MyConfiguration.h"
// which I use to define the total number of threads to preallocate
 CreateThreadPool ( kCooperativeThread,
 kStartupThreadsPreallocate, nil );

You will want to use a wrapper call to NewThread to put the current thread to sleep if there aren't any available threads in the pool. This is done so that one of the active threads may have time to complete, at which point the finishing thread will "wake up" the sleeping thread, allowing it to continue on from the point where it went to sleep, with a thread (the one that just finished) now available from the preallocated pool.

/* Allocate a new thread from the existing pool of threads.
    If there are no threads available, yield to other threads until one finishes.
    The Thread Manager must be available for this function to work. */
OSErr
MyNewThreadFromPool ( ThreadEntryProcPtr threadEntry,
 void * threadParam, void ** threadResult,
 ThreadID * threadMade )
{
 OSErr     theErr;
 short     threadsFree;
 ThreadID  currentThread;
 
 theErr = GetFreeThreadCount ( kCooperativeThread,
 &threadsFree );
 if ( theErr == noErr ) {
 theErr = GetCurrentThread ( &currentThread );
 }
 if ( (theErr == noErr) && (threadsFree == nil) ) {
// Put the current thread to sleep, to be woken up when a thread becomes available.
 gThreadSleeper = currentThread;
 theErr = SetThreadState ( currentThread,
 kStoppedThreadState, nil );
 }
 if ( theErr == noErr ) {
// Install the new thread using a premade thread from the pool.
 theErr = NewThread ( kCooperativeThread,
 threadEntry, threadParam, nil,
 kFPUNotNeeded + kUsePremadeThread,
 threadResult, threadMade );
 }
 if ( theErr == noErr ) {
// Set the termination function for the thread.
 SetThreadTerminator ( *threadMade,
 myThreadTermination, nil );
 
// Increment the total number of sub-threads.
 ++gThreadTotal;
 
// Decrease the sleep ticks so we'll take more processing time.
 gSleepTicks = kSleepTicksWhenBusy;
 }
 
 return theErr;
}

A thread termination callback procedure is used to wake up the sleeping thread when another thread is finished.

/* The Thread Manager must be available for this function to work. */
pascal void
myThreadTermination ( ThreadID threadTerminated, void
 *terminationProcParam )
{
 if ( gThreadSleeper != nil ) {
// Wake up the sleeping thread so that it may be called when other threads yield.
 theErr = SetThreadState ( gThreadSleeper,
 kReadyThreadState, nil );
 gThreadSleeper = nil;
 }
 
// Lower the count of sub-threads.
 --gThreadTotal;
 
 if ( gThreadTotal == nil ) {
// If there are no more threads, reset the sleep ticks to normal.
 gSleepTicks = kSleepTicks;
 }
}

When to Thread

After considering all this, you should ask yourself whether you really need to thread your Apple event handlers. The keys to answering this question are whether there are any points in the handler where it would be appropriate to yield the processing to other threads, and, more importantly, whether the handler is big and slow enough to see performance benefits from threading. Keep in mind that there is overhead and complexity added when you use cooperative threading.

As an example, I didn't thread my Random URL CGI because its response time (with rare exception) is under a second. To thread it would only add time to the total process without any real improvement in performance (not to mention increased memory requirements). On the other hand, a form-handling CGI I'm working on is threaded, because it will frequently have to talk (through Apple events) to other applications, which can slow things down appreciably (and waiting for a response is a great time to yield processing).

Further Considerations

If your application spawns threads from any threads apart from the main thread, you will need to maintain a queue of sleeping threads rather than just the one stored in this article's code. Another important consideration, that hasn't been covered here, is to make sure that WaitNextEvent is still periodically called if the main thread is put to sleep. You don't absolutely have to do that, but it is strongly recommended so that other applications and processes can have time to run. You may want to think about resetting the calling application's Apple event timeout timer when an event is taking a long time to process. Additionally, any CGI that wants to support the new "Send Partial" multi-stage reply mechanism in WebSTAR will need to make more advanced use of threading and Apple events.

Grant's CGI framework comes with project files for the CodeWarrior, THINK C and Symantec Project Manager environments.

Discussion mailing list: grantcgi@arpp.carleton.ca with the subject set to: help

Home page: http://arpp.carleton.ca/cgi/framework/

 

Community Search:
MacTech Search:

Software Updates via MacUpdate

VueScan 9.5.75 - Scanner software with a...
VueScan is a scanning program that works with most high-quality flatbed and film scanners to produce scans that have excellent color fidelity and color balance. VueScan is easy to use, and has... Read more
Opera 44.0.2510.1449 - High-performance...
Opera is a fast and secure browser trusted by millions of users. With the intuitive interface, Speed Dial and visual bookmarks for organizing favorite sites, news feature with fresh, relevant content... Read more
Opera 44.0.2510.1449 - High-performance...
Opera is a fast and secure browser trusted by millions of users. With the intuitive interface, Speed Dial and visual bookmarks for organizing favorite sites, news feature with fresh, relevant content... Read more
Skim 1.4.29 - PDF reader and note-taker...
Skim is a PDF reader and note-taker for OS X. It is designed to help you read and annotate scientific papers in PDF, but is also great for viewing any PDF file. Skim includes many features and has a... Read more
FontExplorer X Pro 6.0.2 - Font manageme...
FontExplorer X Pro is optimized for professional use; it's the solution that gives you the power you need to manage all your fonts. Now you can more easily manage, activate and organize your... Read more
1Password 6.7.1 - Powerful password mana...
1Password is a password manager that uniquely brings you both security and convenience. It is the only program that provides anti-phishing protection and goes beyond password management by adding Web... Read more
Vivaldi 1.9.818.44 - An advanced browser...
Vivaldi is a browser for our friends. In 1994, two programmers started working on a web browser. Our idea was to make a really fast browser, capable of running on limited hardware, keeping in mind... Read more
Vivaldi 1.9.818.44 - An advanced browser...
Vivaldi is a browser for our friends. In 1994, two programmers started working on a web browser. Our idea was to make a really fast browser, capable of running on limited hardware, keeping in mind... Read more
Skim 1.4.29 - PDF reader and note-taker...
Skim is a PDF reader and note-taker for OS X. It is designed to help you read and annotate scientific papers in PDF, but is also great for viewing any PDF file. Skim includes many features and has a... Read more
1Password 6.7.1 - Powerful password mana...
1Password is a password manager that uniquely brings you both security and convenience. It is the only program that provides anti-phishing protection and goes beyond password management by adding Web... Read more

Latest Forum Discussions

See All

Fire Emblem Heroes event announces new m...
As reported yesterday, Nintendo was gearing up a live press event for their popular mobile game,Fire Emblem Heroes. While the stream revealed a lot of new things, the event was entirely in Japanese. Luckily we have a rundown of what was announced... | Read more »
Best games we played this week
Another week, another slate of new mobile games. Although there weren't as many big name releases as last week, there were plenty of unique video game titles that came out that's sure to keep you interested over the weekend. Everything from classic... | Read more »
Olli by Tinrocket (Photography)
Olli by Tinrocket 1.0 Device: iOS iPhone Category: Photography Price: $2.99, Version: 1.0 (iTunes) Description: Get drawn in with Olli by TinrocketOlli instantly turns your everyday moments into hand-drawn art and animations. • Watch... | Read more »
Penarium (Games)
Penarium 1.0 Device: iOS Universal Category: Games Price: $1.99, Version: 1.0 (iTunes) Description: | Read more »
Fire Emblem Heroes is way more profitabl...
Profits for Nintendo's mobile game Fire Emblem Heroes are apparently impressive enough to beat out other Nintendo titles likeSuper Mario Run, despite having 10 times fewer downloads. [Read more] | Read more »
Classic series Robot Unicorn Attack 3 no...
The classic Adult Swim browser game, Robot Unicorn Attack, branched off into a series of popular mobile games. Now, the latest entry into the series, Robot Unicorn Attack 3, is available for iOS and Android mobile devices. [Read more] | Read more »
Sudoku Sweeper (Games)
Sudoku Sweeper 1.0 Device: iOS Universal Category: Games Price: $2.99, Version: 1.0 (iTunes) Description: A minimalist mashup of Minesweeper and Sudoku. Logic puzzle perfection. Every row, column and zone contains a bomb and one of... | Read more »
Under Leaves (Games)
Under Leaves 1.0.0 Device: iOS Universal Category: Games Price: $1.99, Version: 1.0.0 (iTunes) Description: Journey into the forest, the jungle or the depths of the deep blue sea. Find chestnuts for the pigs, a caterpillar for the... | Read more »
Ninja Pizza Girl (Games)
Ninja Pizza Girl 1.0 Device: iOS Universal Category: Games Price: $2.99, Version: 1.0 (iTunes) Description: In the not-so-distant future, rampart traffic congestion has resulted in only one way to deliver pizzas across town in thirty... | Read more »
SCRAP (Games)
SCRAP 1.0 Device: iOS Universal Category: Games Price: $2.99, Version: 1.0 (iTunes) Description: That day, for no apparent reason, SCRAP decided to wake up and run. He had to, because his activation was a mistake the "Factory" could... | Read more »

Price Scanner via MacPrices.net

13-inch 2.7GHz Retina MacBook Pro, Apple refu...
Apple has Certified Refurbished 13″ 2.7GHz/128GB Retina MacBook Pros available for $200 off MSRP. An Apple one-year warranty is included with each model, and shipping is free: - 13″ 2.7GHz/128GB... Read more
13-inch Gray 2.9GHz/512GB Touch Bar MacBook P...
Amazon has the 13″ Space Gray 2.9GHz/512GB Touch Bar MacBook Pro (model MNQF2LL/A) in stock today and on sale for $150 off MSRP. Shipping is free: - 13″ 2.9GHz/512GB Touch Bar MacBook Pro Space Gray... Read more
15-inch 2.7GHz Space Gray Touch Bar MacBook P...
B&H Photo has the 15″ 2.7GHz Space Gray Touch Bar MacBook Pro in stock today and on sale for $2599…$200 off MSRP. Shipping is free, and B&H charges NY & NJ sales tax only: - 15″ 2.7GHz... Read more
13-inch 2.9GHz/256GB Space Gray Touch Bar Mac...
B&H Photo has the 13″ 2.9GHz/256GB Space Gray Touch Bar MacBook Pro in stock today and on sale for $150 off MSRP including free shipping plus NY & NJ sales tax only: - 13″ 2.9GHz/256GB Touch... Read more
21-inch iMacs on sale for up to $151 off MSRP
B&H Photo has 21″ iMacs on sale for up to $151 off MSRP, each including free shipping plus NY sales tax only: - 21″ 3.1GHz iMac 4K: $1348 $151 off MSRP - 21″ 2.8GHz iMac: $1199.99 $100 off MSRP... Read more
Weekend deal: Up to $420 off new MacBook Pros...
Apple has Certified Refurbished 2016 15″ and 13″ MacBook Pros available for $230 to $420 off original MSRP. An Apple one-year warranty is included with each model, and shipping is free: - 15″ 2.6GHz... Read more
Price drop: 15-inch 2.2GHz Retina MacBook Pro...
Amazon has dropped their price on 15″ 2.2GHz Retina MacBook Pros (MJLQ2LL/A) to $1709.99 including free shipping. Their price is $290 off MSRP for this model. Note that stock may sell out quickly at... Read more
2.8GHz Mac mini on sale for $899, save $100
B&H Photo has the 2.8GHz Mac mini (model number MGEQ2LL/A) on sale for $899 including free shipping plus NY & NJ sales tax only. Their price is $100 off MSRP. Read more
Check Apple prices on any device with the iTr...
MacPrices is proud to offer readers a free iOS app (iPhones, iPads, & iPod touch) and Android app (Google Play and Amazon App Store) called iTracx, which allows you to glance at today’s lowest... Read more
New System Clock for macOS by B-Eng Now Avail...
Fehraltorf, Switzerland based B-Eng has announced the release and immediate availability of System Clock, the company’s new system monitor and information app developed exclusively for macOS. System... Read more

Jobs Board

*Apple* Mobile Master - Best Buy (United Sta...
**493714BR** **Job Title:** Apple Mobile Master **Location Number:** 001024-Weatherford-Store **Job Description:** **What does a Best Buy Apple Mobile Master Read more
*Apple* OS X Server Administrator (Active Se...
** Apple OS X Server Administrator \(Active Secret Clearance\)** **Description** Come be a part of a top notch team, apply today\!\! Tuva TUVA provides turnkey Read more
*Apple* Mac Computer Technician - GeekHampto...
…complex computer issues over the phone and in person? GeekHampton, Long Island's Apple Premium Service Provider, is looking for you! Come work with our crew Read more
Best Buy *Apple* Computing Master - Best Bu...
**501846BR** **Job Title:** Best Buy Apple Computing Master **Location Number:** 001126-South Bay Center-Store **Job Description:** **What does a Best Buy Apple Read more
Consultant or Sr. Consultant, *Apple* Allia...
…improve our business and your clients will be heard.Project Manager, Apple AllianceLocation:San Francisco preferred, open to other locationsLevel:Consultant or Sr. Read more
All contents are Copyright 1984-2011 by Xplain Corporation. All rights reserved. Theme designed by Icreon.