The Posix standard defines a number of thread system calls. The posix function to create a new thread within the same process has the following rather ugly function prototype
#include <pthread.h> int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine, void*),void *arg);
This system call has four arguments, the first *thread
is a pointer to the thread id number (the type
pthread_t
is an int). The calling program may
need this value later for thread synchronization.
The data type pthread_attr_t
allows the calling
program to set some of the attibutes of the thread, such as the size of the stack. If this
is set to NULL, the thread has default attributes which
are appropriate for most purposes.
The third argument start_routine
is the name
of a function where the newly created thread will start.
This function must have the following prototype
void *start_routine(void *arg)
(replacing start_routine with the name of the function.)
It must take one argument, a pointer. The
last argument is the pointer to the argument.
As is usually the case, the pthread_create
call
will return a zero if it successfully createed the new thread
and a negative value if it failed, and if it failed, the external
global variable errno will be set to a value which
indicates why it failed.
The threads will be running concurrently; which means that they are all running seemingly at the same time, and there is no assurance about which thread will run first. This can create a race condition; in a race condition, two or more events happen at about the same time, and there is no assurance that one event will happen before the other; the order of the events is indeterminate.
One possibility with threads is that the main calling thread can terminate before all of the threads have terminated. Since all threads are running in the same process space, when the main thread terminates, the entire process dies. In fact, if any thread calls exit(), the entire process immediately terminates. This means that it is possible that some of the threads to die before they have completed their work.
There are two other posix thread system calls that help
to address this problem. To terminate a single thread without
killing the entire process, use the system call
void pthread_exit(void *value_ptr);
The argument is a pointer to a return value, but it can be
set to NULL.
The calling thread can wait for a particular thread to terminate
with the call
int pthread_join(pthread_t thread, void **value_ptr);
The first argument is the thread id that the process is waiting for,
and the second is a pointer to the argument passed back by the
thread. This can be set to NULL. This function will block until
the thread terminates. Note the similarity in function between this
function and the wait function, which waits for a child to
die.
Here is some very simple thread code
/* Alert: link to the pthreads library by appending -lpthread to the compile statement */ #include <pthread.h> #include <stdio.h> #include <sys/types.h> #include <unistd.h> /* for sleep */ #include <stdlib.h> /* for malloc */ #define NUM_THREADS 5 void *sleeping(void *); /* thread routine */ pthread_t tid[NUM_THREADS]; /* array of thread IDs */ int main() { int *sleeptime, i, retval; for ( i = 0; i < NUM_THREADS; i++) { sleeptime = (int *)malloc(sizeof(int)); *sleeptime = (i+1)*2; retval =pthread_create(&tid[i], NULL, sleeping, (void *)sleeptime); if (retval != 0) { perror("Error, could not create thread"); } } for ( i = 0; i < NUM_THREADS; i++) pthread_join(tid[i], NULL); printf("main() reporting that all %d threads have terminated\n", i); return (0); } /* main */ void *sleeping(void *arg) { int sleep_time = *(int *)arg; printf("thread %d sleeping %d seconds ...\n", pthread_self(), sleep_time); sleep(sleep_time); printf("\nthread %d awakening\n", pthread_self()); return (NULL); }Passing arguments to the thread routine requires special attention. The argument is a pointer. It is usually important that the argument to each thread point to different memory, which is why the loop that calls the thread allocates new memory with malloc each time. Also, make sure that the calling function does not change the value of the memory that arg is pointing to after it creates the thread. Because there is a race condition between the calling function and the newly created thread, we do not know whether the thread or the calling function will run first
If you wrote the loop in the obvious (but wrong) way, like this:
for (i=0;i<5;i++) { retval = pthread_create(&threadIds[i], NULL, ThreadRoutine, &i); ...the program would run without errors, but the argument to all five threads would probably be the same, rather than different for each because you are passing the same address each time.
Returning a value from a dying thread using pthread_exit() will test your pointer skills. The argument to this function is a void pointer. A void pointer can point to any data type. The second argument to the function pthread_join is a pointer to a pointer to void, which can also point to any particular data type (presumably the same type as the argument to pthread_exit(). When the call to pthread_join returns, the second argument will point to the argument returned by pthread_exit for that thread.
Here is a short program which demonstrates this. It makes copies of files. The names of the files to be copied are passed in as arguments to main (up to a max of 10). A separate thread is created for each file. The name of the copied file is formed by prepending the string Copy_Of_ to the filename (for example, if the filename was myfile, the copy would be called Copy_Of_myfile). The thread returns the number of bytes copied, and the main thread then displays the total number of bytes copied in all of the files.
/* A program that copies files in parallel using pthreads */ /* Alert: link to the pthreads library by appending -lpthread to the compile statement */ #include <stdio.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <pthread.h> #include <string.h> #include <errno.h> #include <stdlib.h> #include <unistd.h> /* pthread_t copy_tid;*/ extern int errno; #define BUFSIZE 256 void *copy_file(void *arg) { int infile, outfile; int bytes_read = 0; int bytes_written = 0; char buffer[BUFSIZE]; char outfilename[128]; int *ret; ret = (int *)malloc(sizeof(int)); *ret = 0; infile = open(arg,O_RDONLY); if (infile < 0) { fprintf(stderr,"Error opening file %s: ", (char *)arg); fprintf(stderr,"%s\n",strerror(errno)); pthread_exit(ret); } strcpy(outfilename,"Copy_Of_"); strcat(outfilename,arg); outfile = open(outfilename,O_WRONLY | O_CREAT | O_TRUNC, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH); if (outfile < 0) { fprintf(stderr,"Error opening file %s",outfilename); fprintf(stderr,"%s\n",strerror(errno)); pthread_exit(ret); } while (1) { bytes_read = read(infile, buffer, BUFSIZE); if (bytes_read == 0) break; else if (bytes_read < 0) { perror("reading"); pthread_exit(ret); } bytes_written = write(outfile, buffer, bytes_read); if (bytes_written != bytes_read) { perror("writing"); pthread_exit(ret); } *ret += bytes_written; } close(infile); close(outfile); pthread_exit(ret); return ret; /* we never get here, but the compiler likes to see this line */ } int main(int argc, char *argv[]) { pthread_t tid[10]; /* max of 10 possible threads */ int total_bytes_copied = 0; int *bytes_copied_p; int i; int ret; for (i=1;i < argc;i++) { ret = pthread_create(&tid[i], NULL, copy_file, (void *)argv[i]); if (ret != 0) { fprintf(stderr,"Could not create thread %d: %s\n", i, strerror(errno)); fprintf(stderr,"ret is %d\n",ret); } } /* wait for copies to complete */ for (i=1;i < argc;i++) { ret = pthread_join(tid[i],(void **)&(bytes_copied_p)); if (ret != 0) { fprintf(stderr,"No thread %d to join: %s\n", i, strerror(errno)); } else { printf("Thread %d copied %d bytes from %s\n", i, *bytes_copied_p, argv[i]); total_bytes_copied += *bytes_copied_p; } } printf("Total bytes copied = %d\n",total_bytes_copied); return 0; }Pay particular attention to the way that pthread_exit passes a value, in this case an integer, back to the main thread; and note how the main thread uses the second argument of pthread_join to access this value. You may find this syntax
(void **)&(bytes_copied_p)
a little unsettling.
The variable bytes_copied_p
is a pointer to an integer.
By preceding it with an amersand (&) we are passing its address to
the function. This will permit the function to change what it
is pointing to, which is a good thing because when you call the function,
it is not pointing anywhere. The function changes its value so that
it is pointing to the argument of thread_exit. The
(void **) casts this value to a pointer to a
pointer, which keeps the compiler happy.
The typical design of a program that uses threads is to have a main thread that creates some number of child threads and then waits for them to terminate. Both of the examples above used this design. However, any thread can create new threads, and it is not necessary for the parent thread to wait for the children to terminate.
Here is an exercise on threads
The Win32 API to create a new thread is
HANDLE CreateThread( LPSECURITY_ATTRIBUTES lpThreadAttributes, // pointer to security attributes DWORD dwStackSize, // initial thread stack size LPTHREAD_START_ROUTINE lpStartAddress, // pointer to thread function LPVOID lpParameter, // argument for new thread DWORD dwCreationFlags, // creation flags LPDWORD lpThreadId // pointer to receive thread ID );
Although this takes a few more arguments than pthread_create it works in a very similar way. If the first argument lpThreadAttributes is set to NULL, and the second argument dwStackSize is set to zero, appropriate default values will be assigned. The third argument lpStartAddress should be set to the name of the function to be called. The fourth argument lpParameter is a pointer to the argument to be passed to the function. The fifth argument dwCreationFlags should be set to zero. The last argument is a pointer to a DWORD. This will be set by the function.
The function returns a HANDLE value if successul. If it fails, the return value will be NULL.
The called function must have the following signature:
DWORD WINAPI ThreadProc(LPVOID lpParameter);
replacing ThreadProc with the name of the function.
The Win32 equivalent of pthread_join is
DWORD WaitForSingleObject( HANDLE hHandle, // handle to object to wait for DWORD dwMilliseconds // time-out interval in milliseconds );This is a very important function because it is used to wait for all kinds of different events, and we will see it a lot in this course. In this case it is used to wait for a thread to terminate. The first argument hHandle is the handle returned from CreateThread. Like pthread_join, this function blocks until the thread terminates. However, unlike pthread_join this function allows you to specify how long you are willing to wait for the event before becoming unblocked. The second argument is the number of milliseconds to wait. if this value is zero, the function returns immediately, even if the thread has not terminated. Another possible value is the keyword INFINITE, which causes the function to block indefinitely if the thread does not terminate.
Here is why this may be useful. Supposed there is an infinite loop or other code in the thread function which means that the thread will never terminate. If the second argument of WaitForSingleObject was set to INFINITE, this would mean that the main thread would be indefinitely blocked. On the other hand, if the second argument was set to, say, 1000, then it would wake up after a one second interval.
Here is a simple sample program which corresponds to the first sample program using pthreads.
#include <windows.h> #include <stdio.h> DWORD WINAPI ThreadRoutine(LPVOID lpArg) { int a; a = *(int *)lpArg; fprintf(stderr,"My argument is %d\n",a); return NULL; } int main() { int i; int *lpArgPtr; HANDLE hHandles[5]; DWORD ThreadId; for (i=0;i < 5;i++) { lpArgPtr = (int *)malloc(sizeof(int)); *lpArgPtr = i; hHandles[i] = CreateThread(NULL,0,ThreadRoutine,lpArgPtr,0,&ThreadId); if (hHandles[i] == NULL) { fprintf(stderr,"Could not create Thread\n"); exit(0); } else printf("Thread %d was created\n",ThreadId); } for (i=0;i < 5;i++) { WaitForSingleObject(hHandles[i],INFINITE); } return 0; }
To terminate a particular thread without terminating the
entire process, use the API
VOID ExitThread(DWORD ExitCode);
where ExitCode is a value to be returned to another
thread.
A thread can read the ExitCode of another thread which has
terminated with the API
BOOL GetExitCodeThread(HANDLE hThread, LPDWORD lpdwExitCode);
This should only be called if the thread has terminated, so it
is usually used in combination with the API
WaitForSingleObject
Here are links to the on line help pages for these functions
CreateThread
WaitForSingleObject
ExitThread
GetExitCodeThread
Thread Safe Libraries
The C library was written for single threaded functions, and many library functions use global variables to store intermediate results. This means that they are not thread safe. If two or more threads are accessing the same library function at the same time, the function can produce an erroneous answer or a memory exception error.
The documentation for most library functions should specify whether or not the system call is thread safe.
Posix Thread Synchronization
The Posix thread mechanism (pthreads) provides a simple mutex facility as defined in the previous module. The pthread library defines an opaque data type pthread_mutex_t. An instance of a pthread_mutex_t should be defined globally and initialized when it is declared with the macro PTHREAD_MUTEX_INITIALIZER. There are three system calls which perform operations on a mutex, each of which takes a pointer to a pthread_mutex_t as an argument.
/* pthreadmutex.c - a demo of pthread mutual exclusion */ #include <pthread.h> #include <stdlib.h> /* for random() */ #include <stdio.h> /* for printf() */ #include <unistd.h> /* for sleep() */ pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; /* declare the mutex globally */ void CriticalSection(int num) { printf("Thread %d is in its critical section\n", num); sleep(random() % 4); /* sleep for zero to four seconds */ printf("Thread %d is leaving its critical section\n", num); } void NonCriticalSection(int num) { printf("Thread %d is in its noncritical section\n", num); sleep(random() % 4); /* sleep for zero to four seconds */ printf("Thread %d is leaving its noncritical section\n", num); } void* ThreadController(void *arg) { int *num = (int *)malloc(sizeof(int)); int i; *num = *(int *)arg; for (i=0;i < 4;i++) { NonCriticalSection(*num); pthread_mutex_lock(&mutex); CriticalSection(*num); pthread_mutex_unlock(&mutex); } pthread_exit(num); return num; /* we never get here */ } int main() { int i; int retval; int *arg; pthread_t threadid[5]; for (i=0;i < 5;i++) { /* create five threads */ arg = (int *)malloc(sizeof(int)); *arg = i+1; retval = pthread_create(&threadid[i],NULL, ThreadController,arg); if (retval != 0) { fprintf(stderr,"ERROR creating thread"); } } for (i=0;i<5;i++) { arg = (int *) malloc(sizeof(int)); retval = pthread_join(threadid[i],(void **)&arg); if (retval == 0) printf("Thread %d finished with value %d\n", i, *arg); else fprintf(stderr,"ERROR on join"); } return 0; }This program creates five threads, and each thread goes through our loop which alternates between the NonCriticalSection and the CriticalSection. The Critical Section is guarded by a mutex. If you compile and run this program (don't forget to link with the pthread library by appending -lpthread to your compile statement), you should be able to confirm that only one thread at a time is in its critical section.
Semaphores in Unix
The pthread mutex works well for threads, but synchronization of multiple independent processes as created by fork is more complicated. Semaphores were not a part of the original Unix Operating Systems. They were added later, and the code is pretty ugly. This is a part of the Interprocess Communication (IPC) facility which includes mechanisms for shared memory and message passing in addition to semaphores.
The problem that IPC facilities have to solve is that two or more independent processes have to share a common structure, but it should not be available to just any old process. To obtain access to an IPC facility, the process has to know the key. The key is of type key_t but it is really just an integer. One process creates and initializes the semaphore, and other processes can then access it if they know the key.
The code for using IPC Semaphores is complicated, and, frankly, a little bizarre, and so we will not discuss it further. The interested student can read the Unix man pages for semget, semctl and semop.
Win32 Synchronization System Calls
Unlike Unix, where process synchronization seems to have been added as an afterthought, synchronization of both threads and processes is an inherent feature of the Windows operating systems, and Win32 provide a very rich set of synchronization APIs.
If all that you want to do is to increment, decrement or set a value for a variable which is shared between processes or threads, WIN32 provides the follow functions.
LONG InterlockedIncrement(LPLONG lpAddent); LONG InterlockedDecrement(LPLONG lpAddend); LONG InterlockedExchange(LPLONG target, LONG Value);The first two of these take a pointer to an integer as an argument, and increment and decrement the value respectively. The last function sets the value of its first argument to the value of its second argument. These are guaranteed by the operating system to be atomic operations so the problem of an incorrect value resulting from a race condition when two or more processes or threads trying to update the variable at the same time cannot occur.
WIN32 allows independent processes to create and use a mutex in the usual fashion. A mutex has a handle and a name, which is a string. To create a new mutex, use this function
HANDLE CreateMutex( LPSECURITY_ATTRIBUTES lpMutexAttributes, BOOL bInitialOwer, LPCTSTR lpname );The first argument can be set to NULL, the second should be set to FALSE, and the third is the name of the mutex. This function returns a handle to the mutex.
Two or more processes can call CreateMutex to create the same named mutex. The first process actually creates the mutex and subsequent processes open a handle to the existing mutex.
The equivalent of the mutex lock facility is the API
DWORD WaitForSingleObject(HANDLE hHandle, DWORD dwMilliseconds);which we have seen before. The first argument is the handle returned by the call to CreateMutex, the second is how long to wait before giving up. If the second argument is zero, this is equivalent to the pthread function pthread_mutex_trylock, i.e. it returns immediately. The return value will be either WAIT_OBJECT_0 which means that the mutex was successfully locked, or WAIT_TIMEOUT which means that the timeout occured before the mutex became available. If the second argument is set to INFINITE, the function is equivalent to the pthread function pthread_mutex_lock.
The function which is equivalent to pthread_mutex_unlock is
BOOL ReleaseMutex(HANDLE hMutex);
Here are links to the documentation for these functions
CreateMutex
OpenMutex
ReleaseMutex
WaitForSingleObject