/*
 * tclThreadJoin.c --
 *
 *	This file implements a platform independent emulation layer for the
 *	handling of joinable threads. The Windows platform uses this code to
 *	provide the functionality of joining threads.  This code is currently
 *	not necessary on Unix.
 *
 * Copyright © 2000 Scriptics Corporation
 *
 * See the file "license.terms" for information on usage and redistribution of
 * this file, and for a DISCLAIMER OF ALL WARRANTIES.
 */

#include "tclInt.h"

#ifdef _WIN32

/*
 * 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(
    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. */
{
    JoinableThread *threadPtr;

    /*
     * 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.
     */

    Tcl_MutexLock(&joinMutex);

    threadPtr = firstThreadPtr;
    while (threadPtr!=NULL && threadPtr->id!=id) {
	threadPtr = threadPtr->nextThreadPtr;
    }

    if (threadPtr == 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 = firstThreadPtr;

	while (prevThreadPtr->nextThreadPtr != threadPtr) {
	    prevThreadPtr = prevThreadPtr->nextThreadPtr;
	}
	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);
    Tcl_Free(threadPtr);

    return TCL_OK;
}

/*
 *----------------------------------------------------------------------
 *
 * TclRememberJoinableThread --
 *
 *	This procedure remembers a thread as joinable. Only a call to
 *	TclJoinThread will remove the structure 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(
    Tcl_ThreadId id)		/* The thread to remember as joinable */
{
    JoinableThread *threadPtr;

    threadPtr = (JoinableThread *)Tcl_Alloc(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(
    Tcl_ThreadId id,		/* Id of the thread signaling its exit. */
    int result)			/* The result from the thread. */
{
    JoinableThread *threadPtr;

    Tcl_MutexLock(&joinMutex);

    threadPtr = firstThreadPtr;
    while ((threadPtr != NULL) && (threadPtr->id != id)) {
	threadPtr = threadPtr->nextThreadPtr;
    }

    if (threadPtr == 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);
}
#else
TCL_MAC_EMPTY_FILE(generic_tclThreadJoin_c)
#endif /* _WIN32 */

/*
 * Local Variables:
 * mode: c
 * c-basic-offset: 4
 * fill-column: 78
 * End:
 */
