ftp.nice.ch/pub/next/developer/objc/threads/ThreadedApp.1.0.s.tar.gz#/ThreadedApp-1.0/ThreadedApp.m

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.