/* 
 * tclThreadJoin.c --
 *
 *	This file implements a platform independent emulation layer for
 *	the handling of joinable threads. The Mac and Windows platforms
 *	use this code to provide the functionality of joining threads.
 *	This code is currently not necessary on Unix.
 *
 * Copyright (c) 2000 by Scriptics Corporation
 *
 * See the file "license.terms" for information on usage and redistribution
 * of this file, and for a DISCLAIMER OF ALL WARRANTIES.
 *
 * RCS: @(#) $Id: tclThreadJoin.c,v 1.4 2002/04/24 20:35:40 hobbs Exp $
 */

#include "tclInt.h"

#if defined(WIN32) || defined(MAC_TCL)

/* The information about each joinable thread is remembered in a
 * structure as defined below.
 */

typedef struct JoinableThread {
  Tcl_ThreadId  id;                     /* The id of the joinable thread */
  int           result;                 /* A place for the result after the
					 * demise of the thread */
  int           done;                   /* Boolean flag. Initialized to 0
					 * and set to 1 after the exit of
					 * the thread. This allows a thread
					 * requesting a join to detect when
					 * waiting is not necessary. */
  int           waitedUpon;             /* Boolean flag. Initialized to 0
					 * and set to 1 by the thread waiting
					 * for this one via Tcl_JoinThread.
					 * Used to lock any other thread
					 * trying to wait on this one.
					 */
  Tcl_Mutex     threadMutex;            /* The mutex used to serialize access
					 * to this structure. */
  Tcl_Condition cond;                   /* This is the condition a thread has
					 * to wait upon to get notified of the
					 * end of the described thread. It is
					 * signaled indirectly by
					 * Tcl_ExitThread. */
  struct JoinableThread* nextThreadPtr; /* Reference to the next thread in the
					 * list of joinable threads */
} JoinableThread;

/* The following variable is used to maintain the global list of all
 * joinable threads. Usage by a thread is allowed only if the
 * thread acquired the 'joinMutex'.
 */

TCL_DECLARE_MUTEX(joinMutex)

static JoinableThread* firstThreadPtr;



/*
 *----------------------------------------------------------------------
 *
 * TclJoinThread --
 *
 *	This procedure waits for the exit of the thread with the specified
 *	id and returns its result.
 *
 * Results:
 *	A standard tcl result signaling the overall success/failure of the
 *	operation and an integer result delivered by the thread which was
 *	waited upon.
 *
 * Side effects:
 *	Deallocates the memory allocated by TclRememberJoinableThread.
 *	Removes the data associated to the thread waited upon from the
 *	list of joinable threads.
 *
 *----------------------------------------------------------------------
 */

int
TclJoinThread(id, result)
    Tcl_ThreadId id;     /* The id of the thread to wait upon. */
    int*         result; /* Reference to a location for the result
			  * of the thread we are waiting upon. */
{
    /* Steps done here:
     * i.    Acquire the joinMutex and search for the thread.
     * ii.   Error out if it could not be found.
     * iii.  If found, switch from exclusive access to the list to exclusive
     *       access to the thread structure.
     * iv.   Error out if some other is already waiting.
     * v.    Skip the waiting part of the thread is already done.
     * vi.   Wait for the thread to exit, mark it as waited upon too.
     * vii.  Get the result form the structure, 
     * viii. switch to exclusive access of the list,
     * ix.   remove the structure from the list,
     * x.    then switch back to exclusive access to the structure
     * xi.   and delete it.
     */

    JoinableThread* threadPtr;

    Tcl_MutexLock (&joinMutex);

    for (threadPtr = firstThreadPtr;
	 (threadPtr != (JoinableThread*) NULL) && (threadPtr->id != id);
	 threadPtr = threadPtr->nextThreadPtr)
        /* empty body */
      ;

    if (threadPtr == (JoinableThread*) NULL) {
        /* Thread not found. Either not joinable, or already waited
	 * upon and exited. Whatever, an error is in order.
	 */

      Tcl_MutexUnlock (&joinMutex);
      return TCL_ERROR;
    }

    /* [1] If we don't lock the structure before giving up exclusive access
     * to the list some other thread just completing its wait on the same
     * thread can delete the structure from under us, leaving us with a
     * dangling pointer.
     */

    Tcl_MutexLock   (&threadPtr->threadMutex);
    Tcl_MutexUnlock (&joinMutex);

    /* [2] Now that we have the structure mutex any other thread that just
     * tries to delete structure will wait at location [3] until we are
     * done with the structure. And in that case we are done with it
     * rather quickly as 'waitedUpon' will be set and we will have to
     * error out.
     */

    if (threadPtr->waitedUpon) {
        Tcl_MutexUnlock (&threadPtr->threadMutex);
	return TCL_ERROR;
    }

    /* We are waiting now, let other threads recognize this
     */

    threadPtr->waitedUpon = 1;

    while (!threadPtr->done) {
      Tcl_ConditionWait (&threadPtr->cond, &threadPtr->threadMutex, NULL);
    }

    /* We have to release the structure before trying to access the list
     * again or we can run into deadlock with a thread at [1] (see above)
     * because of us holding the structure and the other holding the list.
     * There is no problem with dangling pointers here as 'waitedUpon == 1'
     * is still valid and any other thread will error out and not come to
     * this place. IOW, the fact that we are here also means that no other
     * thread came here before us and is able to delete the structure.
     */

    Tcl_MutexUnlock (&threadPtr->threadMutex);
    Tcl_MutexLock   (&joinMutex);

    /* We have to search the list again as its structure may (may, almost
     * certainly) have changed while we were waiting. Especially now is the
     * time to compute the predecessor in the list. Any earlier result can
     * be dangling by now.
     */

    if (firstThreadPtr == threadPtr) {
        firstThreadPtr = threadPtr->nextThreadPtr;
    } else {
        JoinableThread* prevThreadPtr;

	for (prevThreadPtr = firstThreadPtr;
	     prevThreadPtr->nextThreadPtr != threadPtr;
	     prevThreadPtr = prevThreadPtr->nextThreadPtr)
	    /* empty body */
	  ;

	prevThreadPtr->nextThreadPtr = threadPtr->nextThreadPtr;
    }

    Tcl_MutexUnlock (&joinMutex);

    /* [3] Now that the structure is not part of the list anymore no other
     * thread can acquire its mutex from now on. But it is possible that
     * another thread is still holding the mutex though, see location [2].
     * So we have to acquire the mutex one more time to wait for that thread
     * to finish. We can (and have to) release the mutex immediately.
     */

    Tcl_MutexLock   (&threadPtr->threadMutex);
    Tcl_MutexUnlock (&threadPtr->threadMutex);

    /* Copy the result to us, finalize the synchronisation objects, then
     * free the structure and return.
     */

    *result = threadPtr->result;

    Tcl_ConditionFinalize (&threadPtr->cond);
    Tcl_MutexFinalize (&threadPtr->threadMutex);
    ckfree ((VOID*) threadPtr);

    return TCL_OK;
}

/*
 *----------------------------------------------------------------------
 *
 * TclRememberJoinableThread --
 *
 *	This procedure remebers a thread as joinable. Only a call to
 *	TclJoinThread will remove the structre created (and initialized)
 *	here. IOW, not waiting upon a joinable thread will cause memory
 *	leaks.
 *
 * Results:
 *	None.
 *
 * Side effects:
 *	Allocates memory, adds it to the global list of all joinable
 *	threads.
 *
 *----------------------------------------------------------------------
 */

VOID
TclRememberJoinableThread(id)
    Tcl_ThreadId id; /* The thread to remember as joinable */
{
    JoinableThread* threadPtr;

    threadPtr = (JoinableThread*) ckalloc (sizeof (JoinableThread));
    threadPtr->id          = id;
    threadPtr->done        = 0;
    threadPtr->waitedUpon  = 0;
    threadPtr->threadMutex = (Tcl_Mutex) NULL;
    threadPtr->cond        = (Tcl_Condition) NULL;

    Tcl_MutexLock (&joinMutex);

    threadPtr->nextThreadPtr = firstThreadPtr;
    firstThreadPtr           = threadPtr;

    Tcl_MutexUnlock (&joinMutex);
}

/*
 *----------------------------------------------------------------------
 *
 * TclSignalExitThread --
 *
 *	This procedure signals that the specified thread is done with
 *	its work. If the thread is joinable this signal is propagated
 *	to the thread waiting upon it.
 *
 * Results:
 *	None.
 *
 * Side effects:
 *	Modifies the associated structure to hold the result.
 *
 *----------------------------------------------------------------------
 */

VOID
TclSignalExitThread(id,result)
    Tcl_ThreadId id;     /* Id of the thread signaling its exit */
    int          result; /* The result from the thread */
{
    JoinableThread* threadPtr;

    Tcl_MutexLock (&joinMutex);

    for (threadPtr = firstThreadPtr;
	 (threadPtr != (JoinableThread*) NULL) && (threadPtr->id != id);
	 threadPtr = threadPtr->nextThreadPtr)
        /* empty body */
      ;

    if (threadPtr == (JoinableThread*) NULL) {
        /* Thread not found. Not joinable. No problem, nothing to do.
	 */

        Tcl_MutexUnlock (&joinMutex);
	return;
    }

    /* Switch over the exclusive access from the list to the structure,
     * then store the result, set the flag and notify the waiting thread,
     * provided that it exists. The order of lock/unlock ensures that a
     * thread entering 'TclJoinThread' will not interfere with us.
     */

    Tcl_MutexLock   (&threadPtr->threadMutex);
    Tcl_MutexUnlock (&joinMutex);

    threadPtr->done   = 1;
    threadPtr->result = result;

    if (threadPtr->waitedUpon) {
      Tcl_ConditionNotify (&threadPtr->cond);
    }

    Tcl_MutexUnlock (&threadPtr->threadMutex);
}

#endif /* WIN32 || MAC_TCL */
