/* Professor Liang's Quick Guide to Unix Programming in C Part 2: Introduction to Threads and Processes You've now learnt that the '&' symbol can be used from the command line to start processes concurrently. This document will show you how to create concurrent processes and threads from within a program. In most modern operating systems there exists two classes of processes: "processes" and "threads". A process is a running program. A thread, however, is only part of a running program. Most of the programs you wrote in csc15 and 16 resulted only in processes with a single thread. In such cases there's no distinction between process and thread. However, it is also possible to create a process with multiple threads. A conventional program consists of multiple functions (and classes and objects, etc ...). A thread is like a function, except that once called, it begins to run *concurrently* with the rest of your program! In other words, consider the program void f() { printf("A"); printf("B"); } int main() { f(); printf("C"); printf("D"); exit(0); } As a conventional program, this will of course print "ABCD". But if the function call to f results in a separate "thread of execution", then the body of f will be executed concurrently with the body of main, and you may see "ACBD", or "ACDB", or one of several other possibilities depending on where the operating system decides to task-switch. If the function call to f spins off a separate thread, the above program will have two threads (one for main and one for f). When a new process starts, it gets its own "memory space" - that is, all data needed by the process are its own, and not shared with other processes (except through special mechanisms). In contrast, two threads that are part of the same process can share data in the same way that two functions that are part of the same program can share data. In this way, a thread is sometimes referred to as a "lightweight process". Modern operating system schedulers in fact operate at the level of threads, and not entire processes. A thread is a finer unit of execution than a process, just as a function is a finer unit of code than an entire program. There are system calls one can invoke to create both new threads and new processes. I will first explain threads, because it's easier for them to share data and therefore demonstrate the need for synchronization mechanisms. POSIX Threads The Portable Operating System Interface standard or "POSIX" defines a set of system calls a program can use to create multiple threads. To create a seperate thread of execution, you have to first define a special "thread function" whose body the thread will execute. This function must take a parameter of type (void *) and return something of the same type. void* in C is used to mean "pointer to anything" (it's like the "Object" superclass in Java). Thus we can pass any kind of information we want to a thread function, and it can return any combination of values (though usually it doesn't return values). A single thread function can spawn several threads by using the "pthread_create" system call. Each thread has a thread id of type "pthread_t." The exact mechanism for creating a thread function and initiating a separate thread is best explained through the following simple example: #include #include void * f(void * x) // a thread function { printf("A"); printf("B"); } int main() { pthread_t threadID; pthread_create(&threadID, NULL, f, NULL); printf("C"); printf("D"); pthread_join(threadID,NULL); exit(0); } "pthread_create" is like a special way to call functions: instead of waiting for the function to return before continuing, it immediately continues to the next instruction as the function called is started by another thread of execution. The first argument to pthread_create is a pointer to a variable identifying the thread, which will be instantiated by pthread_create (that's why it has to be a pointer). The second argument is usually NULL. The third argument is the name of the thread function (you can pass functions to other functions in C). The last argument is a pointer to the data structure that will be passed to f. In this example f doesn't use its parameter, so the last argument is NULL. Once the separate thread is created we must assume that the body of main will be executed concurrently with the separate thread. However, it may be the case that main will exit before its "offspring thread" finishes executing. In such a case the separate thread will be killed, since the "main" thread of the process have already terminated. To make the main thread wait for the separate thread to terminte before exiting, we use the "pthread_join" call. This system call causes main to suspend until the thread identified by its first argument has returned. The second argument to pthread_join is a pointer to a data structure that will "catch" the void* value returned by the thread. In this example, the thread doesn't return anything, so this argument is NULL. Consult the appropriate man pages for these system calls for more information. Quiz: what would be the effect of calling pthread_join right after pthread_create? You must compile this program with "gcc -lpthread" to make sure that the posix thread library is loaded. If you run the program you will probably always see "CDAB". That's because the main thread continues to run as the the separate thread is being created. However, if you put the printfs inside long loops, you will see that the threads will task-switch at unpredicatable points. The second example creates two threads. It also passes a pointer to a shared data structure to the threads as they're created. Pay attention to the need for type casting from (void *) to the appropriate types: */ #include #include struct sharedinfo // structure containing shared data for threads { int x; // some data int y; // some more data }; void * f(void * theinfo) { int i; struct sharedinfo * info; info = (struct sharedinfo *) theinfo; // type cast from void* to threadinfo* for(i=0;i<1000;i++) { info->x += 1; // add one to shared variable printf("I am thread F, and I'm changing x to %d\n",info->x); } } //f void * g(void * theinfo) { int i; struct sharedinfo * info; info = (struct sharedinfo *) theinfo; // type cast from void* to sharedinfo* for(i=0;i<1000;i++) { info->x += 1; // add one to shared variable printf("I am thread G, and I'm changing x to %d\n",info->x); } } //g int main() { pthread_t tid1, tid2; struct sharedinfo myinfo; myinfo.x = 0; myinfo.y = 1; pthread_create(&tid1, NULL, f, &myinfo); pthread_create(&tid2, NULL, g, &myinfo); pthread_join(tid1,NULL); pthread_join(tid2,NULL); exit(0); } /* It is also possible to spawn the two threads from a single thread function (just as a conventional function can be called multiple times). Note that the "i" variable of the two threads are not shared simply because they're local variables. However, the variable x inside the struct sharedinfo is shared (through the pointer to the struct passed through pthread_create). Both threads will therefore try to modify the x variable concurrently. This will create a need for a "critical section" and appropriate synchronization mechanisms. The pthread library provides a very basic synchronization mechanism called mutex (the type name is pthread_mutex_t). A mutex is a flag that can be "tested and set" in one locked step. Think of it as a kind of boolean variable. If a process wants to enter a critical section, it checks the value of the mutex variable. If it's true, then in *one locked step* it sets it to false and enters its critical section. If the mutex variable is false, the thread is suspended. Specifically, the thread is put into a queue of threads that are waiting for the variable to become true. When a process leaves its critical section, it sets the mutex variable to true (it "unlocks" it). At this time, one of the processes on the queue will "acquire" the mutex and begin to run in its critical section. One important point to be emphasized is that the mutex itself must be a piece of *shared* data between the threads. Do the following on your own to use a mutex variable to prevent the two threads from changing the shared variable x simultaneously: a. in the declaration of struct sharedinfo, add a new variable: pthread_mutex_t mutex; b. in main, before the calls to create the two threads, initialize the mutex with: pthread_mutex_init(&myinfo.mutex,NULL); c. in both thread functions, surrond the critical sections with: pthread_mutex_lock(&info->mutex); // critical section that changes x goes here pthread_mutex_unlock(&info->mutex); pthread_mutex_lock will in one locked step test the value of the mutex variable, block if necessary, and if the mutex is available, acquire it and enter its critical section. The corresponding pthread_mutex_unlock releases the mutex. Note carefully the use of pointers. The value passed to all the pthread_mutex functions MUST be a pointer to a mutex variable. In particular, note that in the thread functions it's not enough that "info" is already a pointer. "info->mutex" accesses the mutex itself, but to convert it into a pointer, we have to put the additional '&' in front of the expression. With the mutexes we can also implement more sophisticated synchronization mechanisms such as semaphores and monitors. But that's a story for another day. Fork To complete this document I will briefly introduce how to spawn a new process from a program. In Unix, all processes are created by a parent process, except for the initial scheduler process. Each process has a process id of type "pid_t". A process creates another process using the "fork" system call. fork() creates an identical copy of the process that called it, and immediately begins to run the "child process" concurrently. The operating system makes a copy of the process control block. Among the items copied to the child process is the program counter (pointer to the next instruction in the program's code to execute). This means that the child will start executing at the point of the fork. Since fork creates an entirely new process, no variable between the processes can be shared without special (and messy) mechanisms - even if the variables are global. The only thing that distinguishes a child process from the parent (the process that called fork) is the "process id" that is returned by the call to fork(). Zero is always returned to the child process whereas the parent process will get the process id of the child process as assigned by the operating system. Here's an example to clarify all this: #include #include #include #include int x; // global variable, but will it be shared? (no) int main() { pid_t prid; // process id (returned by fork) x = 3; prid = fork(); // fork child process // prid == 0 if you're child process, otherwise prid== pid of child if (prid == 0) { x++; printf("I'm the child, x==%d\n",x); } else { x++; printf("I'm parent, x==%d, and my child has process id %d\n",x,prid); } exit(0); } When you run the program you will see that both parent and the forked child process will print 4 as the value of x. That's because, in contrast to threads, each process has its own copy of x, even though x is "global". The two processes only share the program's source code. The child process will execute the first branch of the if statement and the parent process will execute the else branch. It is also possible to have the child process run another program entirely using the "execv" system call. One process can wait for another to finish using the "waitpid" system call. I will not go into detail concerning these utilities here, but you can consult the man pages. The problem with spawning multiple processes as opposed to threads is that it is much more difficult for processes to shared information. Threads are much easier for the purpose of understanding the necessary mechanisms for synchronizing concurrent programming in general. We will therefore use threads as opposed to processes to understand the concepts of chapter 7 of the dinosaur book. In the future, I hope to cover some interprocess communication (IPC) in Unix. Communication between processes is done through sockets, much as sockets are used in network programs. In fact, in Unix the same basic mechanism for implementing client-server applications over the Internet is also used for processes on the same system to communicate and share information. */