CS 321 Spring 2012  >  Lecture Notes for Monday, February 6, 2012

CS 321 Spring 2012
Lecture Notes for Monday, February 6, 2012

Threads (cont’d) [2.2]

A Thread Model

Threads can be implemented using the standard kernel process mechanism. It is only necessary to give processes access to the same memory. However, this makes creating, terminating, and switching threads, as expensive as processes, which defeats an important reason for using threads.

Therefore, threads are often implemented using a lighter-weight mechanism. We allow a kernel process to “own” multiple threads. Each thread needs to keep track of the following.

All other process-related information: address space, open files, accounting information, running userid, parent/child information, etc., is the same for all threads owned by a process, and so can be tracked by the process.

Since threads need to keep track of so little information—essentially a few registers and a stack—threads and be created, terminated, and switched quickly.

POSIX Threads: Basics

POSIX & pThreads

POSIX (Portable Operating System Interface, with an “X” on the end for mysterious reasons), is a set of standards for the API of an Operating System. Most major operating systems today are at least partially POSIX-compliant.

POSIX includes a threads standard, pThreads, which allows us to write multi-threaded code that will run under all major OSs. Most Unix-derived OSs include pThreads. It is available for Windows, and may or may not already be a part of any given Windows development environment.

Compilation

Include the file <pthread.h>. This is a C-language header file. It works in C++, but some of the conveniences we expect from a C++ package (constructors, member functions) are absent.

If you are doing command-line compilation in some *ix OS, then add “-lpthread” to your command line, to tell the linker where the library is. For example, you might type the following.

g++ thread1.cpp -o thread1 -lpthread

Similarly, when compiling a program that uses pThreads under NetRun, type “-lpthread” into the “Link with:” box (under Options).

Creating a New Thread

When we create a new thread, we give it a function to run. This function takes a (void *) parameter, and returns (void *). This means we can pass and return pointers to anything; but we need to make sure we handle types right, as the compiler will not check them for us.

So, for example, a function for a thread to run, might look something like this.

void * runThread(void * x)
{
    int param = *(int *)(x);  // Actual parameter is (int *)
    // DO THREAD STUFFS HERE
    return 0;  // Null pointer - no return value
}

Create a new thread by calling pthread_create. This takes four parameters.

(pthread_t *)
Declare a variable of type pthread_t and pass its address as the first paramter. This variable (which might be an item in an array) is used to refer to the thread created, much like a PID is used to refer to a process.
(pthread_attr_t *)
Used to set attributes of the thread. We will not use this now. Pass a null pointer (0).
Pointer to a function for the thread to run
Write the function as above, and pass its name as the parameter.
(void *)
The parameter to the above function.

Function pthread_create returns an int, which is zero if the thread was successfully created, and non-zero otherwise.

Here is a sample thread creation call.

int param = 42;
pthread_t pt;
int err = pthread_create(&pt, 0, runThread, &param);
if (err)
{
    // THREAD CREATION UNSUCCESSFUL
}

Note that the above parameter and pthread_t values need to exist as long as they are needed; you might want to make them globals. Further, you probably want an array (std::vector?) of pthread_t and parameter items, one for each thread you create.

Terminating Threads

In pThreads, processes own the threads they create, and killing the process, terminates all the threads. So avoid doing an exit (or coming to the end of function main), unless you are ready for the threads to end.

A thread ends when the run function returns, or when pthread_exit is called. The latter function takes a (void *) parameter, which becomes the return value of the run function.

See thread1.cpp (NetRun link) for a basic demo of pThreads.

Nondeterminism & Race Conditions

Program thread1.cpp does not produce the same output each time it is run. What the program does, depends on many little decisions made by the process/thread scheduler, which in turn depend on all kinds of factors unrelated to this program. This is an example of nondeterminism: a nondeterministic program has results that do not depend only on its input.

Nondeterminism is a common property of multithreaded programs. This fact has led some people to say that, if you create multiple threads, then you are no longer programming a computer, as a computer is a deterministic device.

Sometimes we do not care much whether the output of a multithreaded program can vary; sometimes we care a great deal. When the results of a program change in significant/surprising/bad ways due to scheduler decisions, we have a race condition. Race conditions can have important implications for debugging; imagine a bug that only shows up on some machines.

Threads will be continued next time.


CS 321 Spring 2012: Lecture Notes for Monday, February 6, 2012 / Updated: 10 Feb 2012 / Glenn G. Chappell / ggchappell@alaska.edu