CS360 Lecture notes -- Thread #1

  • Jim Plank
  • Directory: http://www.cs.utk.edu/~mbeck/classes/cs560/360/notes/Thread1
  • Lecture notes: http://www.cs.utk.edu/~mbeck/classes/cs560/360/notes/Thread1/lecture.html
    Last revised: Tue Jan 17 12:37:57 EST 2006
    Threads programming is a paradigm of programming that is very powerful and natural. In a nutshell, threads let you have multiple processing units, called ``threads'' that cooperate via shared memory.

    Why threads?

    There are many reasons to program with threads. In the context of this class, there are two important ones:
    1. They allow you to deal with asynchronous events synchronously and efficiently.
    2. They allow you to get parallel performance on a shared-memory multiprocessor.
    You'll find threads to be a big help in writing an operating system. Therefore, by learning threads here, you get a leg up on CS460.

    What are threads?

    What are threads? Threads are often called "lightweight processes". Whereas a typical process in Unix consists of CPU state (i.e. registers), memory (code, globals, heap and stack), and OS info (such as open files, a process ID, etc), in a thread system there is a larger entity, called a "task", a "pod", or sometimes a "heavyweight process."

    The task consists of a memory (code, globals, heap), OS info, and threads. Each thread is a unit of execution, which consists of a stack and CPU state (i.e. registers). Multiple threads resemble multiple processes, except that multiple threads within a task use the same code, globals and heap. Thus, while two processes in Unix can only communicate through the operating system (e.g. through files, pipes, or sockets), two threads in a task can communicate through memory.

    When you program with threads, you assume that they execute simultaneously. In other words, it should appear to you as if each thread is executing on its own CPU, and that all the threads share the same memory.

    There are various primitives that a thread system must provide. Let's start with two basic ones. In this initial discussion, I am talking about a generic thread system. We'll talk about specific ones (such as POSIX) later.


    Posix threads

    On most Unix machines, there is a thread system that you can use. It is called ``Posix threads.'' Fortunately, our Hydra/Cetus machines To make use of Posix threads in your program, you need to have the following include directive:
    #include < pthread.h >
    
    And you have to link libpthread.a to your object files. (i.e. if your program is in main.c, you need to do the following to make your thread executable):
    UNIX> cc -c main.c
    UNIX> cc -o main main.o -lpthread
    
    You can use gcc too. There's a lot of junk in the pthread library. You can read about it in the various man pages. Start with ``man pthreads''. The two basic primitives defined above are the following in Posix threads:
         int pthread_create(pthread_t *new_thread_ID,
                            const pthread_attr_t *attr,
                            void * (*start_func)(void *), 
                            void *arg);
    
         int pthread_join(pthread_t target_thread, 
                          void **status);
    
    This isn't too bad, and not too far off from my generic description above. Instead of returning a pointer to a thread control block, pthread_create() has you pass the address of one, and it fills it in. Don't worry about the attr argument yet -- just use NULL. Then func is the function, and arg is the argument to the function, which is a (void *). When pthread_create returns, the TCB is in *new_thread_ID, and the new thread is running func(arg).

    pthread_join() has you specify a thread, and give a pointer to a (void *). When the specified thread exits, the pthread_join() call will return, and *status will be the return or exit value of a thread.

    In all the Posix threads, calls, an integer is returned. If zero, everything went ok. Otherwise, an error has occurred. As with system calls, it is always good to check the return values of these calls to see if there has been an error. In my code here in the lecture notes, I'll omit error checking, but it is in the files, and you should do it.

    How does a thread exit? By calling return or pthread_exit().

    Ok, so check out the following program (in hw.c):

    #include < pthread.h >
    #include < stdio.h >
    
    void *printme()
    {
      printf("Hello world\n");
      return NULL;
    }
    
    main()
    {
      pthread_t tcb;
      void *status;
    
      if (pthread_create(&tcb, NULL, printme, NULL) != 0) {
        perror("pthread_create");
        exit(1);
      }
      if (pthread_join(tcb, &status) != 0) { perror("pthread_join"); exit(1); }
    }
    
    Try copying hw.c to your home area, compiling it, and running it. It should print out ``Hello world''.

    Forking multiple threads

    Now, look at print4.c. This forks off 4 threads that print out ``Hi. I'm thread n'', where n goes from zero to 3. This should give you a good idea of how the pthread library works. Feel free to play with this library to get a feeling for how a thread system works. Since Unix is not multithreaded, and since your machines are not multiprocessors, the threads don't get you any extra performance. It just lets you play with threads.

    Here's the output of print4.c:

    Trying to join with tid 0
    Hi.  I'm thread 0
    Hi.  I'm thread 1
    Hi.  I'm thread 2
    Hi.  I'm thread 3
    Joined with tid 0
    Trying to join with tid 1
    Joined with tid 1
    Trying to join with tid 2
    Joined with tid 2
    Trying to join with tid 3
    Joined with tid 3
    
    So what happened is the following. The main() program got control after forking off the four threads. It called pthread_join for thread zero and blocked. Then thread zero got control, printed its line, and exited. Next came threads one, two and three. When they finished, the main() thread got control again and since thread zero was done, its pthread_join() call returned. Then it made the pthread_join() calls for threads one, two and three, all of which returned since these threads were done. When main() returns, all the threads are done, and the program exits.

    exit() vs pthread_exit()

    In pthreads there are two things you should know about thread/program termination. The first is that pthread_exit() makes a thread exit, but keeps the task alive, while exit() terminates the task. If all threads (and the main() program should be considered a thread) have terminated, then the task terminates. So, look at p4a.c.

    Here, all threads, including the main() program exit with pthread_exit(). You'll see that the output is the same as print4.

    Now, look at p4b.c. Here, we put a pthread_exit() call in main() before making the join calls. The output is:

    Hi.  I'm thread 0
    Hi.  I'm thread 1
    Hi.  I'm thread 2
    Hi.  I'm thread 3
    
    You'll note that none of the "Joining" lines were printed out because the main thread had exited. However, the other threads ran just fine, and the program terminated when all the threads had exited.

    The second thing you need to know is that when a forked thread returns from its initial calling procedure (e.g. printme in print4.c, then that is the same as calling pthread_exit(). However, if the main() thread returns, then that is the same as calling exit(), and the task dies. That's why there is no output in p4c.c. Threads 0 through 3 have been forked when the main thread exits, but they haven't run yet. When the main thread returns, the task is terminated, and thus the threads do not run.

    Finally, look at p4d.c. Here, the threads call exit() instead of pthread_exit(). You'll note that the output is usually:

    Trying to join with tid 0
    Hi.  I'm thread 0
    
    This is because the task is terminated by thread 0's exit() call. However, you may occassionly see different output, such as:
    Trying to join with tid 0
    Hi.  I'm thread 1
    
    or even:
    Trying to join with tid 0
    Hi.  I'm thread 0
    Hi.  I'm thread 2
    
    Which shows that POSIX thread scheduling is non-deterministic, and can differ between identical runs of the same program. In this case, preemption may occur either before or after the call to exit() in the first thread that runs.

    Preemption versus non-preemption

    Now, take a look at iloop.c. Here, four threads are forked off, and then the main() thread goes into an infinite loop. When you execute it, what do you think you'll see? I can think of two answers. One is that you'll see nothing -- the main() thread spins forever, and the other threads don't run. The second answer is that the main() thread will run, but the other threads will also get the CPU at some point and run to completion.

    The underlying issue here is called preemption. If your thread system is preemptive, then although the main thread gets most of the CPU, the thread system interrupts it at certain points (i.e. it preempts the main() thread), and runs the other threads.

    POSIX thread systems under Solaris used to be non-preemptive. In recent versions of Solaris, and under LINUX, they are not. So, in our labs (which are LINUX boxes), iloop runs as follows:

    UNIX> iloop
    Hi.  I'm thread 0
    Hi.  I'm thread 1
    Hi.  I'm thread 2
    Hi.  I'm thread 3
    
    However, on a Solaris 5.9, which also has a preemptive POSIX thread system, the output is:
    UNIX> iloop
    Hi.  I'm thread 3
    Hi.  I'm thread 2
    Hi.  I'm thread 1
    Hi.  I'm thread 0
    
    Which shows that the order of scheduling is not the same among different preemptive implementations of the POSIX threads systems.

    There are some machines that have multiple CPU's attached to a single memory. These systems are by nature preemptive, since different threads will actually execute on different CPU's. However, whether or not a thread system is preemptive is an attribute that you must discern when you are programming for a thread system.

    A non-preemptive thread system on a system with a single CPU (called a "uniprocessor") may seem useless, but in actuality it is extremely useful.