This is ThreadedApp.m in view mode; [Download] [Up]
/* * ThreadedApp: adds support for a multi-threaded application. * (c) 1997 Chris Roehrig <croehrig@House.ORG> */ #import "ThreadedApp.h" #import <objc/objc-runtime.h> @implementation ThreadedApp : Application // export global to identify the main thread... cthread_t mainThread; /***************************************************************** * Basic Thread support */ // simple "function control block" to invoke a method struct _fcb { IMP function; id target; SEL selector; any_t arg; mutex_t lock; condition_t cond; volatile BOOL done; }; static void method_dispatcher( struct _fcb *fcb ) { struct _fcb myfcb; // lock the fcb... mutex_lock( fcb->lock ); // copy the args from parent's stack-based struct... // (ignore the lock and cond; they are only copied by reference) myfcb = *fcb; fcb->done =YES; // let parent know it's ok to exit and destroy my args condition_signal( fcb->cond ); mutex_unlock( fcb->lock ); // call the method... myfcb.function( myfcb.target, myfcb.selector, myfcb.arg ); } - (void)detachNewThreadSelector:(SEL)aSelector toTarget:(id)aTarget withObject:(id)anArgument { // allocate all data off stack... struct _fcb fcb; struct mutex m; struct condition c; fcb.function = [aTarget methodFor:aSelector]; if( fcb.function ){ fcb.target = aTarget; fcb.selector = aSelector; fcb.arg = (any_t) anArgument; mutex_init( &m ); fcb.lock = &m; condition_init( &c ); fcb.cond = &c; fcb.done = NO; mutex_lock( &m ); cthread_detach( cthread_fork((any_t(*)())method_dispatcher,(any_t)&fcb) ); // wait for thread to copy its args before exiting and // destroying them! while( !fcb.done ) condition_wait( &c, &m ); mutex_unlock( &m ); mutex_clear( &m ); condition_clear( &c ); } } - (void)exitCurrentThread { cthread_exit(0); } - (void)sleepCurrentThread:(int)msec { thread_switch(THREAD_NULL, SWITCH_OPTION_WAIT, msec); } /***************************************************************** * Callback support * * Callbacks are done by sending the main AppKit thread a Mach message * requesting it to invoke a method. * * The callback Mach message is faked as a simple message * (i.e. no out-of-line data), since the message never crosses task/memory * spaces, and so memory references are still valid. * * The Mach message handler is invoked by the DPSGetEvent routine * called by the Application's event loop. It monitors the port and * calls the handler when messages arrive on it. If DPSGetEvent * (or DPSPeekEvent) is NOT being called, the messages will not be * handled. The AppKit thread MUST remain responsive to events in order * for messages to be handled. * * For fast callbacks, the Mach message handler invokes the callback method * directly. The DPSGetEvent routine is not reentrant and so the callback * method cannot consume events. In particular, this means it cannot * use Locks since they use event loops (see Locks, below). The callback * method can be blocking (wait) or non-blocking. * * For safe and slow callbacks, the Mach message handler does not invoke the * callback method directly. Instead, it posts an (NX_APPDEFINED) * EV_CALLBACK event to the Application's event queue which gets handled * by the Application's applicationDefined: method. This is to allow the * called-back method to run modal event loops itself. (In particular, this * is necessary if the called-back method uses locks to communicate with * other threads (see Locks, below). * The DPSGetEvent routine is not reentrant, so it is not possible for * the message handler to eventually call DPSGetEvent again. By posting * an event to the event queue, the message handler returns, DPSGetEvent * returns, and the EV_CALLBACK event is processed by the Application's * event loop, as if it is any other event. This allows the called-back * method to have full access to the AppKit's features and to run its * own event loop if it desires. * * Locks * It is possible for the main (AppKit) thread to use Locks and * ConditionLocks to synchronize with its other threads. However, it * is forbidden for these Locks to block since that would prevent messages * from being received and may cause other threads to block waiting to * send messages to the main thread, potentially resulting in deadlock. * The AppKit thread MUST remain responsive to events! * * The CJRLock and CJRConditionLock objects handle the case where * the main AppKit thread acquires a lock. They do this by * testing to see if they can acquire the lock (mutex_try_lock). If * they can, they've succeeded. If not, they start a modal loop getting * NX_APPDEFINED events, and looking for EV_UNLOCK events. When one is * received, this indicates that another thread has unlocked a mutex. * and we try to acquire the lock again. This loops until success. * This is a sort of non-blocking "block" since other events aren't * processed during the block (they remain on the queue, however). * The CJRLock's unlock methods send the required EV_UNLOCK messages. * * In addition, EV_CALLBACK events are continued to be processed during * this "blocking" modal loop. This has some bizarre consequences: * Another callback method can be dispatched and preempt a callback method * that is currently blocked waiting for lock to come free. This * preemptive callback method can now also "block" waiting for a lock. * The ThreadedApp's lock methods are re-entrant, so this doesn't pose * a problem. As in all threaded programming, you should not rely * on the order that callbacks might occur. * */ // data for wait callbacks struct waitdata { struct mutex lock; // statically allocated struct condition cond; volatile id retval; volatile BOOL done; }; // thresholds for messages #define FAST_THRESHOLD (NX_MODALRESPTHRESHOLD+1) #define BASE_THRESHOLD NX_BASETHRESHOLD // types and options for our Mach messages and events... // message/event type (0-255)... #define EV_UNLOCK 1 // threadDidUnlock event #define EV_CALLBACK 2 // callback event // message/event options (bit set)... #define EV_CALLBACK_ARG 0x0100 // callback method takes an arg #define EV_CALLBACK_WAIT 0x0200 // block and wait for method to return #define EV_CALLBACK_POST 0x0400 // post the callback as an event // callback message... #define CALLBACK_DATA 4 typedef struct { msg_header_t h; msg_type_t t; id target; SEL sel; id arg; struct waitdata *data; } callbackMsg; // unlock message... #define UNLOCK_DATA 1 typedef struct { msg_header_t h; msg_type_t t; id lock; } unlockMsg; // global variables... static BOOL blocked; // YES if the app is "blocked" in an event loop // waiting for an appropriate EV_UNLOCK event static int block_count; // number of nested blocks (diagnostic) static BOOL fastPending; // YES if a fast callback is in progress // callback ports static port_t fastPort, basePort; static void post_unlock_event( unlockMsg *msg ) { NXEvent ev; ev.type = NX_APPDEFINED; ev.location.x = 0; ev.location.y = 0; ev.time = 0L; ev.flags = 0; ev.window = 0; ev.data.compound.subtype = msg->h.msg_id; ev.data.compound.misc.L[0] = (long) msg->lock; /* the lock object */ ev.data.compound.misc.L[1] = 0; /* unused */ ev.ctxt = [NXApp context]; if( DPSPostEvent( &ev, FALSE ) != 0 ){ printf( "handle_unlock: DPSPostEvent cannot post EV_UNLOCK\n" ); } } static void post_callback_event( callbackMsg *msg ) { NXEvent ev; /* * ok, uck. I've got 4 int-sized parms to send and here they go: * target goes in L[0] * data goes in L[1] * sel goes in flags * arg goes in window * * Doesn't matter what they are, since the window-manager didn't make * the message anyways. */ ev.type = NX_APPDEFINED; ev.location.x = 0; ev.location.y = 0; ev.time = 0L; ev.flags = (int) msg->sel; ev.window = (int) msg->arg; ev.data.compound.subtype = msg->h.msg_id; ev.data.compound.misc.L[0] = (long) msg->target; ev.data.compound.misc.L[1] = (long) msg->data; ev.ctxt = [NXApp context]; if( DPSPostEvent( &ev, FALSE ) != 0 ){ printf( "handle_callback: DPSPostEvent cannot post EV_CALLBACK\n" ); } } static void do_callback( callbackMsg *msg ) { int options; id ret; struct waitdata *d; options = msg->h.msg_id & 0xff00; if( options & EV_CALLBACK_WAIT ){ d = msg->data; mutex_lock( &d->lock ); } fastPending = YES; if( options & EV_CALLBACK_ARG ){ ret = [msg->target perform:msg->sel with:msg->arg]; } else { ret = [msg->target perform:msg->sel]; } fastPending = NO; if( options & EV_CALLBACK_WAIT ){ d->retval = ret; d->done = YES; condition_signal( &d->cond ); mutex_unlock( &d->lock ); } } static void callbackHandler( msg_header_t *msg ) /* * This is called from DPSGetEvent or DPSPeekEvent if there * is a message on the queue. * This handler converts the Mach message to the appropriate * NXEvent and posts it to the event queue (similar to what the * NXTrackingTimer does). * For fast callbacks, it invokes the method directly. */ { int msg_type, msg_options; #if 0 putchar('*'); fflush(stdout); #endif msg_type = msg->msg_id & 0x00ff; msg_options = msg->msg_id & 0xff00; switch( msg_type ){ case EV_UNLOCK: // no point sending UNLOCK events if no one cares... if( blocked ){ post_unlock_event( (unlockMsg *)msg ); } break; case EV_CALLBACK: if( msg_options & EV_CALLBACK_POST ){ // callbacks posted as events... post_callback_event( (callbackMsg *)msg ); } else { // fast callbacks; do them here... do_callback( (callbackMsg *)msg ); } break; default: printf( "unknown message: 0x04%x\n", msg->msg_id ); break; } } - (NXEvent*)getNextEvent:(int)mask waitFor:(double)timeout threshold:(int)level /* * Override the master NXApp event getter to get and dispatch NX_APPDEFINED * events if they aren't already included in the mask. This allows * callbacks and EV_UNLOCK events to get through unsuspecting modal loops * such as for handling mouse-down events. */ { NXEvent *ev; if( mask & NX_APPDEFINEDMASK ){ // caller must know what they're doing... return [super getNextEvent:mask waitFor:timeout threshold:level]; } mask |= NX_APPDEFINEDMASK; for(;;){ ev = [super getNextEvent:mask waitFor:timeout threshold:level]; if( ev->type != NX_APPDEFINED ) break; [self applicationDefined:ev]; } return ev; } - (NXEvent*)getNextEvent:(int)mask { NXEvent *ev; if( mask & NX_APPDEFINEDMASK ){ // caller must know what they're doing... return [super getNextEvent:mask ]; } mask |= NX_APPDEFINEDMASK; for(;;){ ev = [super getNextEvent:mask]; if( ev->type != NX_APPDEFINED ) break; [self applicationDefined:ev]; } return ev; } - applicationDefined:(NXEvent *)ev /* * This method receives the NX_APPDEFINED events dispatched by the * Application's run: and sendEvent: methods (but not by other * AppKit modal loops: Button, Slider, etc.) * This is where we perform the slow callbacks. */ { id ret; int ev_type, ev_options; id target, arg; SEL sel; struct waitdata *d; ev_type = ev->data.compound.subtype & 0x00ff; ev_options = ev->data.compound.subtype & 0xff00; switch( ev_type ){ case EV_UNLOCK: // Don't need this now; must be left on the queue from a // previous lock: modal loop below... // printf( "applicationDefined: Received a rogue EV_UNLOCK!\n" ); break; case EV_CALLBACK: /* uck. See handle_callback above */ target = (id) ev->data.compound.misc.L[0]; sel = (SEL) ev->flags; arg = (id) ev->window; if( ev_options & EV_CALLBACK_WAIT ){ d = (struct waitdata *)ev->data.compound.misc.L[1]; mutex_lock( &d->lock ); } // The event data in ev can be invalidated by this method // invocation if it recursively calls DPSGetEvent. // That's ok; we've now got a local copy of everything we need. if( ev_options & EV_CALLBACK_ARG ){ ret = [target perform:sel with:arg]; } else { ret = [target perform:sel]; } if( ev_options & EV_CALLBACK_WAIT ){ d->retval = ret; d->done = YES; condition_signal( &d->cond ); mutex_unlock( &d->lock ); } break; default: printf( "applicationDefined: Unknown event: %d\n", ev->data.compound.subtype ); break; } return self; } /* * The "designated" callback function. */ // option bits for the master callback method... #define WAIT 0x02 #define ARG 0x08 #define HIGH 0x01 #define POST 0x04 #define FAST HIGH #define SAFE (HIGH|POST) #define SLOW POST - (id)callback:target perform:(SEL)aSel with:anObject opt:(int)option { // allocate all variables off stack so it's reentrant and thread-safe callbackMsg msg; struct waitdata d; // if this is called from the main thread, just do a perform: // otherwise 'wait' may deadlock if( cthread_self() == mainThread ){ if( option & ARG ){ return [target perform:aSel with:anObject]; } else { return [target perform:aSel]; } } // setup header msg.h.msg_simple = TRUE; msg.h.msg_size = sizeof(callbackMsg); msg.h.msg_local_port = PORT_NULL; msg.h.msg_remote_port = (option&HIGH) ? fastPort : basePort; msg.h.msg_type = MSG_TYPE_NORMAL; msg.t.msg_type_name = MSG_TYPE_INTEGER_32; msg.t.msg_type_size = 32; msg.t.msg_type_number = CALLBACK_DATA; msg.t.msg_type_inline = TRUE; msg.t.msg_type_longform = FALSE; msg.t.msg_type_deallocate = FALSE; // set up message data msg.h.msg_id = EV_CALLBACK; msg.target = target; msg.sel = aSel; // set options... if( option & POST ){ msg.h.msg_id |= EV_CALLBACK_POST; } if( option & ARG ){ msg.h.msg_id |= EV_CALLBACK_ARG; msg.arg = anObject; } if( option & WAIT ){ msg.h.msg_id |= EV_CALLBACK_WAIT; msg.data = &d; d.done = NO; mutex_init( &d.lock ); condition_init( &d.cond ); mutex_lock( &d.lock ); } msg_send((msg_header_t *)&msg, MSG_OPTION_NONE,0); if( option & WAIT ){ // wait for method to return while( !d.done ){ condition_wait( &d.cond, &d.lock ); } mutex_unlock( &d.lock ); mutex_clear( &d.lock ); condition_clear( &d.cond ); return d.retval; } else { return nil; } } // Exported variations on the theme - (id)callbackAndWaitTarget:target perform:(SEL)aSel { return [self callback:target perform:aSel with:nil opt:FAST|WAIT]; } - (id)callbackAndWaitTarget:target perform:(SEL)aSel with:anObject { return [self callback:target perform:aSel with:anObject opt:FAST|WAIT|ARG]; } - (void)callbackTarget:target perform:(SEL)aSel { [self callback:target perform:aSel with:nil opt:FAST]; } - (void)callbackTarget:target perform:(SEL)aSel with:anObject { [self callback:target perform:aSel with:anObject opt:FAST|ARG]; } - (id)safeCallbackAndWaitTarget:target perform:(SEL)aSel { return [self callback:target perform:aSel with:nil opt:SAFE|WAIT]; } - (id)safeCallbackAndWaitTarget:(id)target perform:(SEL)aSel with:(id)anObject { return [self callback:target perform:aSel with:anObject opt:SAFE|WAIT|ARG]; } - (void)safeCallbackTarget:target perform:(SEL)aSel { [self callback:target perform:aSel with:nil opt:SAFE]; } - (void)safeCallbackTarget:(id)target perform:(SEL)aSel with:(id)anObject { [self callback:target perform:aSel with:anObject opt:SAFE|ARG]; } - (id)slowCallbackAndWaitTarget:target perform:(SEL)aSel { return [self callback:target perform:aSel with:nil opt:SLOW|WAIT]; } - (id)slowCallbackAndWaitTarget:(id)target perform:(SEL)aSel with:(id)anObject { return [self callback:target perform:aSel with:anObject opt:SLOW|WAIT|ARG]; } - (void)slowCallbackTarget:target perform:(SEL)aSel { [self callback:target perform:aSel with:nil opt:SLOW]; } - (void)slowCallbackTarget:(id)target perform:(SEL)aSel with:(id)anObject { [self callback:target perform:aSel with:anObject opt:SLOW|ARG]; } + new { [super new]; // register ports for receiving callback messages port_allocate( task_self(), &fastPort ); DPSAddPort( fastPort, (DPSPortProc)callbackHandler, sizeof(callbackMsg), NULL, FAST_THRESHOLD ); port_allocate( task_self(), &basePort ); DPSAddPort( basePort, (DPSPortProc)callbackHandler, sizeof(callbackMsg), NULL, BASE_THRESHOLD ); // identify the main Thread mainThread = cthread_self(); // set runtime to be thread-safe... objc_setMultithreaded(YES); return NXApp; } - free { DPSRemovePort(fastPort); DPSRemovePort(basePort); port_deallocate( task_self(), fastPort ); port_deallocate( task_self(), basePort ); return [super free]; } @end /***************************************************************** * Support for CJRLocks... */ @implementation ThreadedApp (CJRLock) - (void)threadDidUnlock:sender // construct a EV_UNLOCK Mach message to send to fastPort... { unlockMsg msg; if( !blocked ){ // no point in sending UNLOCK events if no one cares... return; } // setup header msg.h.msg_simple = TRUE; msg.h.msg_size = sizeof(unlockMsg); msg.h.msg_local_port = PORT_NULL; msg.h.msg_remote_port = fastPort; msg.h.msg_type = MSG_TYPE_NORMAL; // type field for zero data... msg.t.msg_type_name = MSG_TYPE_INTEGER_32; msg.t.msg_type_size = 32; msg.t.msg_type_number = UNLOCK_DATA; msg.t.msg_type_inline = TRUE; msg.t.msg_type_longform = FALSE; msg.t.msg_type_deallocate = FALSE; // message type... msg.h.msg_id = EV_UNLOCK; msg.lock = sender; // the lock object that unlocked msg_send((msg_header_t *)&msg, MSG_OPTION_NONE,0); } - (void)lock: (mutex_t)mutex /* * This method is called from CJRLock and CJRConditionLock methods * and should only be invoked from the main thread. * It is possible for this routine to be called recursively in response * to servicing applicationDefined: */ { NXEvent *ev; int saved_block; if( fastPending ){ // this doesn't return [self error: "Cannot use locks from a fast callback method\n"]; } if( !mutex_try_lock(mutex) ){ saved_block = blocked; block_count++; blocked = YES; for(;;){ ev = [self getNextEvent:NX_APPDEFINEDMASK waitFor:NX_FOREVER threshold:FAST_THRESHOLD]; if( ev->data.compound.subtype == EV_UNLOCK ){ if( mutex_try_lock(mutex) ) break; } else { // handle the event... [self applicationDefined:ev ]; } } blocked = saved_block; block_count--; } } - (void) lock:(mutex_t)mutex whenCondition:(condition_t)cond withData:(volatile int *)data is:(int)value /* * This method is called from CJRConditionLock methods * and should only be invoked from the main thread. * It is possible for this routine to be called recursively in response * to servicing applicationDefined: */ { NXEvent *ev; int saved_block; if( fastPending ){ // this doesn't return [self error: "Cannot use locks from a fast callback method\n"]; } if( mutex_try_lock(mutex) ){ if( *data == value ) { return; } mutex_unlock(mutex); } saved_block = blocked; block_count++; blocked = YES; for(;;){ ev = [self getNextEvent:NX_APPDEFINEDMASK waitFor:NX_FOREVER threshold:FAST_THRESHOLD]; if( ev->data.compound.subtype == EV_UNLOCK ){ if( mutex_try_lock(mutex) ){ if( *data == value ) break; mutex_unlock(mutex); } } else { // handle the event... [self applicationDefined:ev ]; } } blocked = saved_block; block_count--; } @end
These are the contents of the former NiCE NeXT User Group NeXTSTEP/OpenStep software archive, currently hosted by Netfuture.ch.