Using addresses of stack variables with NSPR threads on win16

This is a cautionary note that may be old information for some of you. However, since it affects the portability of code, it was deemed prudent to include a short memo describing the issue.

Problem statement

WIN-16 (aka, Windows 3.1, et al), is unique in that the architecture depends on the Operating Environment (i.e., Windows) knows the address of the stack, and that there is only one such address. Consequently, implementing threads, with their implication of a unique stack for each thread, requires that the NSPR copy the stacks during thread context switches.

The actual copying of the stack is not such a hardship as one would imagine. With the speed of today's processors (even those running WIN-16), the copying of 10 - 50 kilobytes of data between two locations in memory is barely measurable 1. What is a hardship is that addresses of dynamic variables, those allocated on the call stack, in a function's local frame, are not valid across thread boundaries.

The simplest demonstration of the problem is as follows:

typedef struct SharedData
{
    PRLock *ml;
    PRCondVar *cv;
    PRInt32 counter;
} SharedData;

static void ForkedThread(void *arg)
{
    SharedData *shared = (SharedData*)arg;
    while (--shared->counter > 0)
        PR_WaitCondVar(shared->cv, PR_INTERVAL_NO_TIMEOUT);
    return;
}  /* ForkedThread */

PRIntn main(PRIntn argc, char **argv)
{
    PRThread *thread;
    SharedData shared;
    shared.ml = PR_NewLock();
    shared.cv = PR_NewCondVar(shared.ml);
    shared.counter = 10;
    thread = PR_CreateThread(
        PR_USER_THREAD, ForkedThread, &shared, PR_PRIORITY_NORMAL,
        PR_LOCAL_THREAD, PR_JOINABLE_THREAD, 0);

    do
    {
        PR_Sleep(PR_SecondsToInterval(1));
        PR_Lock(shared.ml);
        if (0 == shared.counter) break;
        PR_NotifyCondVar(shared.cv);
        PR_Unlock(shared.ml);
    } while (PR_TRUE);

    rv = PR_JoinThread(thread);
    return (PR_SUCCESS == rv) ? 0 : 1;
}

This is a completely correct (albeit trivial) program that will run predictably on all NSPR platforms other than WIN-16. On WIN-16, the thread's attempt to address the <tt>SharedData</tt> through the pointer shared will provide interesting (though always incorrect) results. It's difficult to tell exaclty where the updated <tt>counter</tt> is being stored. The only portable manner to write this program requires that the shared structure be allocated from the heap. Well, that isn't so hard to remember, is it?

What is probably more likely to cause problems is passing addresses of automatically allocated variables to a function that wends its way though arbitrary amounts of convoluted logic, and finds its way into an object that is shared. One should be particularly cautious of arrays since they are passed by reference by default.

1 It is possible that our ability to measure the costs is not up to the task.

Original Document Information

This note is about writing win16-portable code that uses NSPR threads, probably not interesting to today's developers