Homework 2 Solutions

22C:116, Fall 1998

Douglas W. Jones
  1. Why, in the example thread manager, was it necessary to have two distinct functions, thread_manager_init and thread_manager_start?
    Between a call to thread_manager_init() and thread_manager_start() there may be any number of calls to thread_launch(). If the functions of the init and start routines were combined into a single startup routine, this would also have to perform the function of thread_launch(). This can be done easily if we only allow the launch of a single thread; consider
    	void  thread_startup( int size, void (* proc)(int), int param )
    	{
    		thread_manager_init();
    		thread_launch( size, proc, param );
    		thread_manager_start();
    	}
    
    Changing this to allow launching an arbitrary number of threads would be messy! In theory, it can be done, either by passing an array of thread description triples, each containing a size, a pointer to a procedure and a parameter, or by using the obscure C macros va_start(), va_arg() and va_end() (all declared in <stdarg.h>). Each of these approaches to launching variable numbers of threads at the start of a multithreaded application leads to user-level code that is less readable than the code that results from the separation of the startup functions into thread_manager_init() and thread_manager_start().

  2. Inspect the data structure thread and the code for thread_relinquish, and compare these to the suggested process table fields given in Figure 2-4 of the text and the context switching operation given in Figure 2-5 of the text. Explain the major differences!
    The biggest difference is that the data structure for thread is designed for embedding in a linked list of threads, while the process table fields in Figure 2-4 are intended for use in an array of records.

    The second big difference is that the fields in Figure 2-4 include many fields that are not part of our thread implementation. Among these, the memory management fields are there because, in most systems, each process may have a different memory address space, while all of our threads share an address space, and all of the ID fields are present to allow access rights checking functions that are irrelevant to the thread package.

    Tannenbaum includes a field labeled process state that is distinct from the process registers; this probably is intended to record whether the process is ready, running, waiting or in some other state. In the thread implementation, this state information is not explicitly recorded, but rather, is determined by the data structure into which the tread record is linked.

  3. Propose a set of data structures that would allow implementation of semaphores under the example thread manager.
    The following solution is borrowed from problem 1 of homework 3.
    	struct semaphore {
    	    int count;
    	    struct thread_queue queue;
    	};
    

  4. Consider the following little UNIX program using parallel processes:
    main()
    {
       int i;
       i = 0;
       if (fork()) { /* parent */
           int j;
           for (j = 0;j < 1000; j++) { i++; }
           wait();
       } else { /* child */
           int j;
           for (j = 0;j < 1000; j++) { i--; }
           exit();
       }
       printf( "%d\n", i );
    }
    
    What output did this program produce? Explain why the output is not zero!
    The output of the program is 1000; this is nonzero because the parent and child processes in the above program do not share memory. Instead, the child gets a copy of the parent's memory at the time of the fork() call, and this copy (in which i is decremented) is discarded when the child exits. Thus, the value of i printed by the parent is determined only by the increment operations done by the parent!

    Note that some students added printf statements at the end of each branch of the if statement, and concluded, incorrectly, that the child always ran first, before the parent, because the output of the two printfs was always printed in that order. It is important to remember that the C standard library routine printf() computes its output into a buffer, and that this buffer is flushed to the actual output device either when the buffer is full, when the process requests input or when the process terminates. In the example, the order of process termination determines the order in which the processes flush their buffers. To force immediate output of text to standard output (descriptor number 1), consider using:

    write( 1, "text\n", 5 );