home *** CD-ROM | disk | FTP | other *** search
/ OS/2 Shareware BBS: 14 Text / 14-Text.zip / EN0706.ZIP / ENV.706
Text File  |  1988-04-23  |  26KB  |  388 lines

  1. OS/2 Threads and High-Level Languages
  2.  
  3. Using OS/2's multiple threads in C programs presents potential, but soluble problems; when it comes to the C Library functions, however, the old spectre of non-reentrancy haunts us again.
  4.  
  5.  
  6. Sometimes the documentation that accompanies the OS/2 Software Development Kit can be a little scary.  Here's a warning appended to the description of the DosCreateThread function:
  7.  
  8. High-level languages, run-time libraries, and stack checking may severely limit or eliminate the ability     to call DosCreateThread directly from a high-level language program.
  9.  
  10.     It's a good thing I didn't mention this in the last issue, or we might never have attempted to call DosCreateThread from a C program!
  11.     As you'll discover in this column, there is good reason for the warning.  But I remain convinced that multiple threads can be created in C programs provided the programmer is aware and alert to the potential problems.  It also helps if the programmer has a good background in assembly language and is  familiar with the assembly language code that makes up the C library functions.  
  12.  
  13. SAME CODE, OTHER THREADS  In last issue's column we looked at two programs that created a second thread of execution.  Let's take another step forward.
  14.     The QUADRANT program shown in Figure 1 creates four additional threads of execution.  Each of these additional threads use the same thread function, which is called ThreadFunction.
  15.     QUADRANT uses the four threads to continuously draw and erase rectangular "spirals" in the four quadrants of the screen.  Each of these four threads uses a different character--the numbers 0, 1, 2, and 3--for drawing its own spiral.
  16.     The main thread (thread 1) calls DosCreateThread four times to create the four threads.  Each of the four threads gets its own stack.  The main thread then calls KbdCharIn to go into hibernation until a key is pressed.  Pressing any key terminates the program.
  17.     Each of the four threads has its own 80286 microprocessor state.  This means that when OS/2 switches among these four threads, it must save the contents of the 80286 registers (including the instruction pointer and stack pointer) of the thread it's switching away from, and restore the contents of the microprocessor registers of the thread it's switching to.  In this respect the threads can run independently.
  18.     But these four threads also access data stored in variables defined within ThreadFunction and Display.  The question we must answer is:  How can the threads avoid clobbering each other when they reach for the same data?
  19.  
  20. STATIC AND AUTOMATIC DATA      You'll notice that some of the variables within ThreadFunction are defined as static and some are not.  The distinction is very important.
  21.     When you first start learning C, you learn that variables defined as static within a function retain data between function calls.  That is, if a function sets a static variable to 55 during one function call, the static variable will still be 55 the next time the function is called.
  22.     A variable that is not defined as static is called an "automatic" variable.  You can explicitly define an variable as automatic by giving it an auto or register type.  Automatic variables defined within a function lose their values when a function is exited.
  23.     But these definitions are not quite adequate when dealing with functions used by multiple threads in an OS/2 program.  A more helpful definition is this:  Static variables are stored in the program's data area while automatic variables are stored on the stack or in registers.
  24.     For example, if you look at a dissassembled C program, you'll find that the compiler creates a prologue and epilogue for each function like this:
  25.  
  26.  
  27. PUSH BP
  28. MOV  BP, SP
  29. SUB  SP, xx
  30.  
  31. [body of function]     
  32.  
  33. MOV  SP, BP
  34. POP  BP
  35. RET
  36.  
  37. (The prologue may be a little different if stack checking is enabled.  When compiling for 80286 code, the first three statements can be replaced with a ENTER instruction and the two statements preceding RET can be replaced with LEAVE.)
  38.     The value of xx in the prologue code is set equal to the size in bytes of the storage needed for the function's automatic variables.  The automatic variables can then be referenced by negative offsets to the BP register relative to the stack segment, thus:
  39.  
  40. SS:[BP - offset]
  41.  
  42. Parameters passed to the function are positive offsets to BP.  By contrast, variables defined as static within a function have an absolute address relative to DS (the data segment).  
  43.     You'll recall that each thread in a process has its own 80286 microprocessor state, its own 80287 math coprocessor state, and its own stack.  For multiple threads in a process that use the same function, this has an important implication:  All variables defined within the thread function as static are shared among the threads.  However, each thread has its own private copy of the automatic variables.  This is true not only for the thread function, but for all functions called by the thread function, such as the Display function in QUADRANT.
  44.     In ThreadFunction, the variable ThreadNumber is defined as static.  Each of the four threads address the same value in QUADRANT's data area when the threads access ThreadNumber.  The MyThreadNum variable is not defined as static, which means that it's an automatic variable and is stored on the stack.  In effect, each thread maintains its own MyThreadNum variable on its own stack.
  45.     ThreadFunction contains the two statements
  46.      
  47. MyThreadNum = ThreadNumber ;
  48. ThreadNumber += 1 ;
  49.  
  50. Because ThreadNumber is initialized to zero, the first thread that executes these two statements sets its own MyThreadNum to zero and increments ThreadNumber.  The second thread then sets its own MyThreadNum to one and increments ThreadNumber.  In this way, each thread establishes for itself its own identify.  The threads use the MyThreadNum variable to display the number to the screen.
  51.     The distinction between static and automatic variables is also observed when ThreadNumber obtains the dimensions of the screen by calling VioGetMode.  The VioGetMode function is only called once by the thread that executes the code the first time.  The ModeData structure passed to VioGetMode is a static variable so that the second, third, and fourth additional threads can use the values set in the structure.
  52.     The very simple rule is:  For threads, static variables are shared; automatic variables are private.
  53.  
  54. CRITICAL SECTION FUNCTIONS      Once you begin using static variables within a thread function, you have to start thinking about what happens if OS/2 switches between threads at the wrong time.
  55.     Consider again, for example, those two statements from QUADRANT's thread function:
  56.      
  57. MyThreadNum = ThreadNumber ;
  58. ThreadNumber += 1 ;
  59.  
  60. What happens if the first created thread executes the assignment statement but before it has a chance to increment ThreadNumber, OS/2 decides it's time to let the second created thread run for a while.  Now the second created thread executes the assignment statement and--Yikes!  The ThreadNumber variable is still equal to 0!
  61.     A series of statements like these are often called a "critical section."  It is important for these statements to be executed without interruption from other threads in the same process.
  62.     To halt all other threads in a process during a critical section, you sandwich the critical section code between calls to DosEnterCritSec and DosExitCritSec.  This is shown in QUADRANT.C.  DosEnterCritSec essentially says "Everybody stop.  I have something important to do."  DosExitCritSec says "O.K., everybody back to work."
  63.     In last issue's column I discussed DosSuspendThread and DosResumeThread.  These two functions affect only one specific thread.  The DosEnterCritSec and DosExitCritSec functions affect all the threads in the same process except for the thread calling the functions.  The functions do not affect threads in other processes.  The thread calling DosEnterCritSec does not ensure for itself uninterrupted processing because it can be interrupted if OS/2 switches to a thread in another process.  But it cannot be interrupted by a thread in the same process.
  64.  
  65. THE THREAD STACK  In the two programs in last issue's column, the stack used for the additional threads was an automatic array defined in main.  The address of the end of this array (the top of the stack) is passed to the DosCreateThread function.
  66.     QUADRANT needs four stack arrays.  Rather than take them from the program's main stack (which, by default, is set to about 2K and is not quite large enough for four threads' stacks), I decided to define them as static arrays.
  67.     This decision has some side effects.  Normally, the function prologues in program compiled with the Microsoft C compiler contain a call to a function that checks for stack overflow.  Because the stack of the thread function is outside the program's normal stack, this stack overflow check would cause the program to abort with an error message.  Stack checking must therefore be inhibited in the thread function and every function called from the thread function.
  68.     In QUADRANT this is accomplished with a pragma statement:
  69.  
  70. #pragma stack_check-
  71.  
  72. You can also compile with the -Gs compiler flag to disable stack checking for the entire module.
  73.     So, we've seen how the thread's stack can be a chunk of memory from either the process's main stack or from the process's static data area.  Either is O.K. because the stack and the static data area occupy the same physical segment.  Even though the compiler generates code that accesses stack data relative to the SS segment register and accesses static data relative to the DS segment register, the two registers have the same value and reference the same segment.  More briefly, in C syntax:
  74.  
  75. DS == SS
  76.  
  77.     It is also possible for a program to allocate a chunk of memory for a thread stack by calling DosAllocSeg.  At this point, however, everything gets a little hairy.  That's because within the thread function--and in every function called from the thread function--the thread's data segment and stack segment are different.  In other words:
  78.  
  79. DS != SS
  80.  
  81.     This little quirk does not prevent you from programming in C.  (In fact, the problem of different stack and data segments also shows up in the programming of OS/2 dynamic link libraries.)  But when writing code for multiple thread programs, it certainly adds another layer of complexity.  I'll discuss the implications in a later column on dynamic link libraries.
  82.     Conclusion:  Avoid using DosAllocSeg to get a block of memory for a thread's stack if you're programming in a high-level language.
  83.  
  84. USING C LIBRARY FUNCTIONS  Earlier, we looked at two lines of code in QUADRANT that we identified as a critical section.  This raises a disturbing question:  Could there be similar code hidden away in the middle of normal C library functions?  Unfortunately, the answer is yes.
  85.     The problem involves reentrancy.  DOS programmers who have ventured into the programming of RAM-resident utilities are well aware of what reentrancy problems can entail.  DOS itself is a nonreentrant operating system.  This means that while one program is in the middle of making a DOS function call, another program cannot make a DOS function call without taking special precautions.  The nonreentrancy of DOS has plagued the writers of RAM-resident programs for years.  Most of the infamous "undocumented" features of DOS involve ways in which DOS programs like PRINT get around the reentrancy problem.
  86.     OS/2 doesn't have this problem.  You do not have to worry about calling OS/2 functions--even the same OS/2 function--from multiple threads.
  87.     What you do have to worry about is calling normal C library functions.  Many C library functions are not reentrant.  This means that if one thread is in the middle of a library function call, another thread cannot call that same function.  
  88.     When you question the Microsoft people in charge of the programming products about the problem with C library function reentrancy, they groan and admit that it's something that has to be fixed.  As we'll see, however, doing so may prove something of a challenge.
  89.     The problem affects only those library functions that use static variables.  If the function uses only stack variables everything is fine: each thread has its own stack.  For simple C functions, you can usually determine if the function uses static data just be thinking it through.  For example, the strlen function determines the length of a zero-terminated string.  That's a fairly simple operation that can be done entirely with registers.  Does strlen use static data?  Probably not, and in fact, it doesn't.  Calling strlen from multiple threads is perfectly safe.
  90.  
  91. REENTRANCY AND RAND       Of those library functions that are not reentrant, rand is a good simple example.  Normally, successive calls to the rand function generate a pseudo-random sequence based on a seed.  The initial value of the seed is 1 but it can be changed with a call to srand.
  92.     If you were to duplicate the rand function by writing it in C, it would look something like this:
  93.  
  94. unsigned long seed = 1 ;
  95. rand ()
  96.     {
  97.     seed=(0x343FDL * seed) + 0x269EC3L ;
  98.     return(int) (0x7FFF & (seed >>16)) ;
  99.     }
  100.  
  101. The seed is a static variable that changes with each call to rand.  Thus, the same seed value is shared among all threads that call the rand function.  That in itself is not a problem.
  102.     The problem occurs during the calculation.  The function loads the value of seed from memory, does a 32-bit multiply, a 32-bit add, and stores the result back in the variable seed.  If this calculation were interrupted between the load and the store by a call from another thread to rand, the two function calls could return the same "random" number.  
  103.     Storing the new 32-bit value back in memory requires two MOV instructions.  If OS/2 switched to another thread between these two MOV instructions, and the second thread called rand, then that rand call could use a seed that was half old and half updated.  The results would be perhaps a little more random than one would prefer!
  104.  
  105. CRITICAL SECTION SOLUTION      Let's fix the rand function without rewriting it.  One solution is to use the DosEnterCritSec and DosExitCritSec function I discussed earlier.  You make up your own function called (for example) SafeRand.  It looks like this:
  106.  
  107. SafeRand ()
  108.      {
  109.      int ReturnValue ;
  110.      DOSENTERCRITSEC () ;
  111.      ReturnValue = rand () ;
  112.      DOSEXITCRITSEC () ;
  113.      return ReturnValue ;
  114.      }
  115.  
  116. Now instead of calling rand in your program you call SafeRand.  The two Critical Section functions allow only one thread to call rand at any time.
  117.  
  118. THE SEMAPHORE SOLUTION  As a general solution to the reentrancy problem, the Critical Section functions are probably overkill.  DosEnterCritSec suspends all other threads in the process.  You don't need to go that far--you only need to prevent multiple threads from calling rand at the same time.
  119.     You can do this more efficiently using a semaphore.
  120.     Semaphores are likely be the topic of their own Environments column, so I only want to touch on them briefly at this point.  There are two basic types of semaphores.  System semaphores have file-like names and can be shared among processes.  RAM semaphores are private to a process but can be shared among threads.  We'll create a RAM semaphore to solve the rand reentrancy problem.
  121.     Semaphores have two general applications:  signaling and resource control.  In both uses, semaphores often serve as temporary roadblocks for a thread of execution.  When a thread comes up against a semaphore that is "set" or "owned" by another thread (or process), the thread is blocked or suspended.  When the other thread (or another process) "clears" the semaphore, then the blocked thread proceeds.
  122.     In a signaling application, the fact that the thread was able to proceed past the semaphore is, in effect, a form of inter-thread or inter-process communication.  In a resource control application (which is what we'll be using), an unblocked semaphore lets a thread use a particular resource, such as the rand function.
  123.     If OS/2 did not support semaphores, you might try mimicing the operation of them like this:
  124.  
  125. SafeRand ()
  126.      {
  127.      static int Flag = 0 ;
  128.      int        ReturnValue ;
  129.      DOSENTERCRITSEC () ;
  130.      while (Flag != 0) ;
  131.      Flag = 1 ;
  132.      DOSEXITCRITSEC () ;
  133.      ReturnValue = rand () ;
  134.      Flag = 0 ;
  135.      return ReturnValue ;
  136.      }
  137.  
  138. The first thread that calls SafeRand sets the Flag variable to 1.  Any other thread calling SafeRand will be blocked by the while statement until the first thread sets the Flag variable to 0.  But we've written some bad code here--that while statement is eating up time slices waiting for a previous thread to reset the flag.  Moreover, we haven't been able to get rid of the Critical Section functions.
  139.     With semaphores, you don't have to worry about such stuff.  Here's the semaphore version of SafeRand:
  140.      
  141. SafeRand ()
  142.     {
  143.     static long Semaphore = 0 ;
  144.     int         ReturnValue ;
  145.     DOSSEMREQUEST ((unsigned long)
  146.          (long far *) &Semaphore, -1L) ;
  147.     ReturnValue = rand () ;
  148.     DOSSEMCLEAR ((unsigned long)
  149.          (long far *) &Semaphore) ;
  150.     return ReturnValue ;
  151.     }
  152.  
  153. The DosSemRequest and DosSemClear functions are two of eight semaphore functions supported by OS/2.  The excessive casting in the DosSemRequest and DosSemClear functions is necessary because of the inadequate header files provided with the initial OS/2 Software Development Kit.  The function calls should really look like this:
  154.  
  155. DOSSEMREQUEST (&Semaphore, -1) ;
  156. and
  157. DOSSEMCLEAR (&Semaphore) ;
  158.  
  159.     The Semaphore variable is initially set to 0, which means that it is "unowned."  When a thread executes DosSegRequest, the semaphore is set to an "owned" state.  Now if another thread attempts to call DosSemRequest the thread will be blocked until the semaphore becomes unowned again.  During the time the thread is blocked, it doesn't use up any time slices.  The thread becomes unblocked when the first thread is finished with the rand function and calls DosSemClear.
  160.     This SafeRand function is used in the ALPHSOUP program, shown in Figure 2.  ALPHSOUP creates 26 threads, each of which displays a letter (A through Z) on the display and moves it around randomly.  The letters take little random walks around the screen or--if you prefer--resemble a bowl of alphabet soup during an earthquake.
  161.  
  162. OTHER C PROBLEM FUNCTIONS  If you'd like to ponder the difficulties of reentrant C library functions on your own, talk at look at strtok.  This function has to save a pointer that it uses on successive calls to the function.  Think about two different threads attempting to divide two different strings into delimited "tokens" using this function.  Think about a mess.
  163.     How would such a function be written so it could work with multiple threads?  
  164.     I think it's obvious that the strtok function itself has to determine which thread is calling the function and store separate static data for each thread.  The strtok function can obtain the thread ID from the DosGetPid function; it would then have to allocate memory to store the static variable and some look-up tables to match thread IDs to static data associated with the thread.
  165.     The same technique could also be adapted for rand so that the function maintained separate seeds for each thread.  If two threads start with the same seed, they get the same pseudo-random sequence back from the function.  Certainly that wouldn't be appropriate for the ALPHSOUP program, though it might be good for other applications.
  166.     So now that we're talking about two multiple thread versions of the rand functions, you can begin to see why the people involved with compiler development at Microsoft start to groan when the subject of library function reentrancy comes up.  It's certainly not quite as simple as it first seems.
  167.  
  168.  
  169.  
  170.  
  171. /*--------------------------------------------------------------------------
  172.    QUADRANT.C--OS/2 Program that Runs 4 Threads Using One Thread Function
  173.                  (C) 1988, Ziff Communications Company
  174.                  PC Magazine * Programmed by Charles Petzold, 11/87
  175.   --------------------------------------------------------------------------*/
  176.  
  177. #include <doscalls.h>
  178. #include <subcalls.h>
  179.  
  180. #define min(a,b) ((a) < (b) ? (a) : (b)) 
  181.  
  182. void far ThreadFunction (void) ;
  183.  
  184. main ()
  185.      {
  186.      static unsigned char ThreadStack [4][1024] ;
  187.      unsigned int         i, ThreadID [4] ;
  188.      struct KeyData       kd ;
  189.  
  190.      for (i = 0 ; i < 4 ; i++)
  191.           if (DOSCREATETHREAD (ThreadFunction, &ThreadID [i],
  192.                                ThreadStack [i] + 1024))
  193.                {
  194.                puts ("QUADRANT: Could not create thread") ;
  195.                return 1 ;
  196.                }
  197.  
  198.      KBDCHARIN (&kd, 0, 0) ;
  199.  
  200.      return 0 ;
  201.      }
  202.  
  203. #pragma check_stack-
  204.  
  205. void far ThreadFunction ()
  206.      {
  207.      static struct ModeData md ;
  208.      static int             ThreadNumber = 0, NumRep ;
  209.      unsigned int           MinRow, MaxRow, MinCol, MaxCol ;
  210.      unsigned int           MyThreadNum, Cycle, Rep, Row, Col ;
  211.  
  212.      DOSENTERCRITSEC () ;
  213.  
  214.      if (ThreadNumber == 0)
  215.           {
  216.           md.length = sizeof (md) ;
  217.           VIOGETMODE (&md, 0) ;
  218.  
  219.           NumRep = (min (md.col, md.row) / 2 + 1) / 2 ;
  220.           }
  221.  
  222.      MyThreadNum = ThreadNumber ;
  223.  
  224.      ThreadNumber += 1 ;
  225.  
  226.      DOSEXITCRITSEC () ;
  227.  
  228.      MinRow = MyThreadNum > 1 ? md.row / 2 : 0 ;
  229.      MaxRow = MinRow + md.row / 2 ;
  230.      MinCol = MyThreadNum % 2 ? md.col / 2 : 0 ;
  231.      MaxCol = MinCol + md.col / 2 ; 
  232.  
  233.      while (1)
  234.           for (Cycle = 0 ; Cycle < 2 ; Cycle++)
  235.                for (Rep = 0 ; Rep < NumRep ; Rep++)
  236.                     {
  237.                     Row = MinRow + Rep ;
  238.  
  239.                     for (Col = MinCol+Rep ; Col < MaxCol-Rep-1 ; Col++)
  240.                          Display (Cycle, Row, Col, MyThreadNum) ;
  241.                     for (Row = MinRow+Rep ; Row < MaxRow-Rep-1 ; Row++)
  242.                          Display (Cycle, Row, Col, MyThreadNum) ;
  243.  
  244.                     for (Col = MaxCol-Rep-1 ; Col > MinCol+Rep ; Col--)
  245.                          Display (Cycle, Row, Col, MyThreadNum)  ;
  246.  
  247.                     for (Row = MaxRow-Rep-1 ; Row > MinRow+Rep ; Row--)
  248.                          Display (Cycle, Row, Col, MyThreadNum) ;
  249.                     }
  250.      }
  251.  
  252. Display (Cycle, Row, Col, Num)
  253.      int Cycle, Row, Col, Num ;
  254.      {
  255.      char String [2] ;
  256.  
  257.      String [0] = (char) (Cycle == 0 ? Num + '0' : ' ') ;
  258.      String [1] = '\x07' ;
  259.  
  260.      VIOWRTCELLSTR (String, 2, Row, Col, 0) ;
  261.  
  262.      DOSSLEEP (0L) ;
  263.      }
  264.  
  265.  
  266.  
  267. Caption:
  268. Figure 1:  QUADRANT.C creates four additional threads of execution based on the same thread function.
  269.  
  270.  
  271. /*---------------------------------------------------------------------------
  272.    ALPHSOUP.C--OS/2 Program that Runs 26 Threads Using One Thread Function
  273.                  (C) 1988, Ziff Communications Company
  274.                  PC Magazine * Programmed by Charles Petzold, 11/87
  275.   ---------------------------------------------------------------------------*/
  276.  
  277. #include <doscalls.h>
  278. #include <subcalls.h>
  279.  
  280. void far ThreadFunction (void) ;
  281.  
  282. main ()
  283.      {
  284.      static unsigned char ThreadStack [26][1024] ;
  285.      unsigned int         i, ThreadID [26] ;
  286.      struct KeyData       kd ;
  287.  
  288.      for (i = 0 ; i < 26 ; i++)
  289.           if (DOSCREATETHREAD (ThreadFunction, &ThreadID [i],
  290.                                ThreadStack [i] + 1024))
  291.                {
  292.                puts ("RANDQUAD: Could not create thread") ;
  293.                return 1 ;
  294.                }
  295.  
  296.      KBDCHARIN (&kd, 0, 0) ;
  297.  
  298.      return 0 ;
  299.      }
  300.  
  301. #pragma check_stack-
  302.  
  303. void far ThreadFunction ()
  304.      {
  305.      static struct ModeData md ;
  306.      static char            ClearCell [2] = " \x07" ;
  307.      static int             ThreadNumber = 0 ;
  308.      unsigned int           MinRow, MaxRow, MinCol, MaxCol ;
  309.      int                    MyThreadNum, Row, Col ;
  310.  
  311.      DOSENTERCRITSEC () ;
  312.  
  313.      if (ThreadNumber == 0)
  314.           {
  315.           VIOSCROLLUP (0, 0, 0xFFFF, 0xFFFF, 0xFFFF, ClearCell, 0L) ;
  316.  
  317.           md.length = sizeof (md) ;
  318.           VIOGETMODE (&md, 0) ;
  319.           }
  320.  
  321.      MyThreadNum = ThreadNumber ;
  322.  
  323.      ThreadNumber += 1 ;
  324.  
  325.      DOSEXITCRITSEC () ;
  326.  
  327.      Row = SafeRand () % md.row ;
  328.      Col = SafeRand () % md.col ;
  329.  
  330.      while (1)
  331.           {
  332.           Row = (Row + SafeRand () % 3 - 1 + md.row) % md.row ;
  333.           Col = (Col + SafeRand () % 3 - 1 + md.col) % md.col ;
  334.  
  335.           Display (0, Row, Col, MyThreadNum) ;
  336.  
  337.           DOSSLEEP (0L) ;
  338.  
  339.           Display (1, Row, Col, MyThreadNum) ;
  340.           }
  341.      }
  342.  
  343. SafeRand ()
  344.      {
  345.      static long Semaphore = 0 ;
  346.      int         ReturnValue ;
  347.  
  348.      DOSSEMREQUEST ((unsigned long) (long far *) &Semaphore, -1L) ;
  349.  
  350.      ReturnValue = rand () ;
  351.  
  352.      DOSSEMCLEAR ((unsigned long) (long far *) &Semaphore) ;
  353.  
  354.      return ReturnValue ;
  355.      }
  356.  
  357. Display (Cycle, Row, Col, Num)
  358.      int Cycle, Row, Col, Num ;
  359.      {
  360.      char String [2] ;
  361.  
  362.      String [0] = (char) (Cycle == 0 ? Num + 'A' : ' ') ;
  363.      String [1] = '\x07' ;
  364.  
  365.      if (Num == 0)
  366.           String [1] = '\x1B' ;
  367.  
  368.      VIOWRTCELLSTR (String, 2, Row, Col, 0) ;
  369.      }
  370.  
  371.  
  372.  
  373. Caption:
  374. Figure 2:  The ALPHSOUP program creates 26 additional threads of execution and demonstrates how semaphores prevent library function reentrancy problems.
  375.  
  376.  
  377. PUSH BP
  378. MOV  BP, SP
  379. SUB  SP, xx
  380.  
  381. [body of function]     
  382.  
  383. MOV  SP, BP
  384. POP  BP
  385. RET
  386.  
  387.  
  388.