Notes for Advanced Linux Programming - 4. Threads
4. Threads
- To use the POSIX standard thread API (pthreads), link libpthread.so to your program.
4.1. Thread Creation
- Each thread in a process is identified by a thread ID, pthread_t.
- The pthread_self function returns the thread ID of the current thread.
- This thread IDs can be compared with the pthread_equal function.
if (!pthread_equal (pthread_self (), other_thread))
pthread_join (other_thread, NULL);
- each thread executes a thread function:
void * function(void *)
- The pthread_create function creates a new thread.
- A pointer to a pthread_t variable: store the thread ID of the new thread.
- A pointer to a thread attribute object. You can pass NULL to use the default attributes.
- A pointer to the thread function. void* (*) (void*)
- A thread argument value of type void*.
- Compile and link this program:
% cc -o thread-create thread-create.c –lpthread
- A thread exits in two ways.
- return from the thread function. The function return value is the thread return value.
- explicitly call pthread_exit. The argument to pthread_exit is the thread’s return value.
- Create a Thread
#include <pthread.h>
#include <stdio.h>
/* Prints x’s to stderr. The parameter is unused. Does not return. */
void* print_xs (void* unused)
{
while (1)
fputc (‘x’, stderr);
return NULL;
}
/* The main program. */
int main ()
{
pthread_t thread_id;
/* Create a new thread. The new thread will run the print_xs function. */
pthread_create (&thread_id, NULL, &print_xs, NULL);
/* Print o’s continuously to stderr. */
while (1)
fputc (‘o’, stderr);
return 0;
}
4.1.1. Passing Data to Threads
Listing 4.2 (thread-create2) Create Two Threads
#include <pthread.h>
#include <stdio.h>
/* Parameters to print_function. */
struct char_print_parms
{
/* The character to print. */
char character;
/* The number of times to print it. */
int count;
};
/* Prints a number of characters to stderr, as given by PARAMETERS,
which is a pointer to a struct char_print_parms. */
void* char_print (void* parameters)
{
/* Cast the cookie pointer to the right type. */
struct char_print_parms* p = (struct char_print_parms*) parameters;
int i;
for (i = 0; i < p->count; ++i)
fputc (p->character, stderr);
return NULL;
}
/* The main program. */
int main ()
{
pthread_t thread1_id;
pthread_t thread2_id;
struct char_print_parms thread1_args;
struct char_print_parms thread2_args;
/* Create a new thread to print 30,000 ’x’s. */
thread1_args.character = ’x’;
thread1_args.count = 30000;
pthread_create (&thread1_id, NULL, &char_print, &thread1_args);
/* Create a new thread to print 20,000 o’s. */
thread2_args.character = ’o’;
thread2_args.count = 20000;
pthread_create (&thread2_id, NULL, &char_print, &thread2_args);
return 0;
}
- But there is a bug
- Main thread creates the thread parameters as local variables and passes them to the threads.
- If the main thread exits before the threads, the parameters will be deallocated.
- But the threads are still accessing it.
- Use join the wait for these threads to finish first.
4.1.2. Joining Threads
pthread_join takes two arguments:
the thread ID of the thread to wait for
A pointer to a void* variable that will receive the finished thread’s return value.
Revised Main Function for thread-create2.c
int main ()
{
pthread_t thread1_id;
pthread_t thread2_id;
struct char_print_parms thread1_args;
struct char_print_parms thread2_args;
/* Create a new thread to print 30,000 x’s. */
thread1_args.character = ’x’;
thread1_args.count = 30000;
pthread_create (&thread1_id, NULL, &char_print, &thread1_args);
/* Create a new thread to print 20,000 o’s. */
thread2_args.character = ’o’;
thread2_args.count = 20000;
pthread_create (&thread2_id, NULL, &char_print, &thread2_args);
/* Make sure the first thread has finished. */
pthread_join (thread1_id, NULL);
/* Make sure the second thread has finished. */
pthread_join (thread2_id, NULL);
/* Now we can safely return. */
return 0;
}
4.1.3. Thread Return Values
- The second argument of pthread_join is the thread’s return value
- Compute Prime Numbers in a Thread
#include <pthread.h>
#include <stdio.h>
/* Compute successive prime numbers (very inefficiently). Return the
Nth prime number, where N is the value pointed to by *ARG. */
void* compute_prime (void* arg)
{
int candidate = 2;
int n = *((int*) arg);
while (1) {
int factor;
int is_prime = 1;
/* Test primality by successive division. */
for (factor = 2; factor < candidate; ++factor)
if (candidate % factor == 0) {
is_prime = 0;
break;
}
/* Is this the prime number we’re looking for? */
if (is_prime) {
if (--n == 0)
/* Return the desired prime number as the thread return value. */
return (void*) candidate;
}
++candidate;
}
return NULL;
}
int main ()
{
pthread_t thread;
int which_prime = 5000;
int prime;
/* Start the computing thread, up to the 5,000th prime number. */
pthread_create (&thread, NULL, &compute_prime, &which_prime);
/* Do some other work here... */
/* Wait for the prime number thread to complete, and get the result. */
pthread_join (thread, (void*) &prime);
/* Print the largest prime it computed. */
printf(“The %dth prime number is %d.\n”, which_prime, prime);
return 0;
}
4.1.4. Thread Attributes
- To specify customized thread attributes:
- Create a pthread_attr_t object.
- Call pthread_attr_init to initialize the attributes with default values.
- Modify the attribute object to contain the desired attribute values.
- Pass a pointer to the attribute object when calling pthread_create.
- Call pthread_attr_destroy to release the attribute object. The pthread_attr_t variable itself is not deallocated and can be reinitialized with pthread_attr_init.
- The detach state attribute.
- A thread may be created as a joinable thread (the default) or as a detached thread.
- A joinable thread is not automatically cleaned up it terminates until another thread calls pthread_join to obtain its return value.
- A detached thread is cleaned up automatically when it terminates. Other thread can not pthread_join it to obtain its return value.
- Use pthread_attr_setdetachstate to set the detach state in a thread attribute object.
- Skeleton Program That Creates a Detached Thread
#include <pthread.h>
void* thread_function (void* thread_arg)
{
/* Do work here... */
}
int main ()
{
pthread_attr_t attr;
pthread_t thread;
pthread_attr_init (&attr);
pthread_attr_setdetachstate (&attr, PTHREAD_CREATE_DETACHED);
pthread_create (&thread, &attr, &thread_function, NULL);
pthread_attr_destroy (&attr);
/* Do work here... */
/* No need to join the second thread. */
return 0;
}
4.2 Thread Cancellation
- A thread can cancel another thread by calling phtread_cancel.
- You should join the cancelled thread to free it resources.
- The return value of a canceled thread is the special value given by PTHREAD_CANCELED
- A thread can be in one of three states with regard to thread cancellation.
- Asynchronously cancelable: The thread may be canceled at any point in its execution.
- Synchronously cancelable: cancellation requests are queued until the thread reaches specific points in its execution.
- Uncancelable: Attempts to cancel the thread are quietly ignored.
- By default, a thread is synchronously cancelable
- Call pthread_setcanceltype to set the cancellation type:
- pthread_setcanceltype (PTHREAD_CANCEL_ASYNCHRONOUS, NULL);
- pthread_setcanceltype (PTHREAD_CANCEL_DEFERRED, NULL);
- pthread_setcanceltype (PTHREAD_CANCEL_DISABLE, NULL);
4.3 Thread-Specific Data
- Each thread-specific data item has a key.
- Each thread uses this key to access its own copy of the corresponding data item.
- To create a new key and a new data item for each thread, call pthread_key_create.
- The first argument is pthread_key_t
- The second argument is a cleanup function which will be called when the thread exits.
- Each thread can set its thread-specific value corresponding to that key by calling pthread_setspecific.
- Call pthread_getspecific to retrieve a thread-specific data item.
- Per-Thread Log Files Implemented with Thread-Specific Data
#include <malloc.h>
#include <pthread.h>
#include <stdio.h>
/* The key used to associate a log file pointer with each thread. */
static pthread_key_t thread_log_key;
/* Write MESSAGE to the log file for the current thread. */
void write_to_thread_log (const char* message)
{
FILE* thread_log = (FILE*) pthread_getspecific (thread_log_key);
fprintf (thread_log, “%s\n”, message);
}
/* Close the log file pointer THREAD_LOG. */
void close_thread_log (void* thread_log)
{
fclose ((FILE*) thread_log);
}
void* thread_function (void* args)
{
char thread_log_filename[20];
FILE* thread_log;
/* Generate the filename for this thread’s log file. */
sprintf (thread_log_filename, “thread%d.log”, (int) pthread_self ());
/* Open the log file. */
thread_log = fopen (thread_log_filename, “w”);
/* Store the file pointer in thread-specific data under thread_log_key. */
pthread_setspecific (thread_log_key, thread_log);
write_to_thread_log (“Thread starting.”);
/* Do work here... */
return NULL;
}
int main ()
{
int i;
pthread_t threads[5];
/* Create a key to associate thread log file pointers in thread-specific data. Use close_thread_log to clean up the file
pointers. */
pthread_key_create (&thread_log_key, close_thread_log);
/* Create threads to do the work. */
for (i = 0; i < 5; ++i)
pthread_create (&(threads[i]), NULL, thread_function, NULL);
/* Wait for all threads to finish. */
for (i = 0; i < 5; ++i)
pthread_join (threads[i], NULL);
return 0;
}
4.3.1. Thread Clean-up
- Linux provides cleanup handlers which is a function and should be called when a thread exits.
- To register a cleanup handler, call pthread_cleanup_push.
- pthread_cleanup_pop unregisters the cleanup handler.
- pthread_cleanup_pop(0): just unregister the handler.
- pthread_cleanup_pop(1): not only unregister the handler, but also call it.
- Program Fragment Demonstrating a Thread Cleanup Handler
#include <malloc.h>
#include <pthread.h>
/* Allocate a temporary buffer. */
void* allocate_buffer (size_t size)
{
return malloc (size);
}
/* Deallocate a temporary buffer. */
void deallocate_buffer (void* buffer)
{
free (buffer);
}
void do_some_work ()
{
/* Allocate a temporary buffer. */
void* temp_buffer = allocate_buffer (1024);
/* Register a cleanup handler for this buffer, to deallocate it in case the thread exits or is cancelled. */
pthread_cleanup_push (deallocate_buffer, temp_buffer);
/* Do some work here that might call pthread_exit or might be cancelled... */
/* Unregister the cleanup handler. Because we pass a nonzero value, this actually performs the cleanup by calling
deallocate_buffer. */
pthread_cleanup_pop (1);
}
- Thread Cleanup in C++
- Use C++ destructors.
- When the objects go out of scope because of an exception, C++ makes sure that destructors are called for those automatic variables.
- If a thread calls pthread_exit, C++ doesn’t guarantee that destructors are called for all variables.
- We can throw out an exception on the top level to let all variables out of scope and then in the exception handler, call pthread_exit.
- Implementing Safe Thread Exit with C++ Exceptions
#include <pthread.h>
class ThreadExitException
{
public:
/* Create an exception-signaling thread exit with RETURN_VALUE. */
ThreadExitException (void* return_value) : thread_return_value_ (return_value) {}
/* Actually exit the thread, using the return value provided in the constructor. */
void* DoThreadExit ()
{
pthread_exit (thread_return_value_);
}
private:
/* The return value that will be used when exiting the thread. */
void* thread_return_value_;
};
void do_some_work ()
{
while (1) {
/* Do some useful things here... */
if (should_exit_thread_immediately ())
throw ThreadExitException (/* thread’s return value = */ NULL);
}
}
void* thread_function (void*)
{
try {
do_some_work ();
}
catch (ThreadExitException ex) {
/* Some function indicated that we should exit the thread. */
ex.DoThreadExit ();
}
return NULL;
}
////////////////////////////////////////////////////////////////////
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
class ThreadExitException
{
public:
ThreadExitException(void * return_value): thread_return_value(return_value){}
void * DoThreadExit()
{
pthread_exit(thread_return_value);
}
private:
void * thread_return_value;
};
class example
{
public:
example(int size)
{
printf("allocating...");
memory = malloc(size);
}
~example()
{
printf("deallocating...");
free(memory);
}
private:
void *memory;
};
void do_some_work()
{
printf("do some work...");
example e(100);
throw ThreadExitException(NULL);
}
void * thread_function(void *)
{
try
{
do_some_work();
}
catch(ThreadExitException ex)
{
ex.DoThreadExit();
}
return NULL;
}
int main()
{
pthread_t thread_id;
pthread_create(&thread_id, NULL, &thread_function, NULL);
pthread_join(thread_id, NULL);
}
4.4 Synchronization and Critical Sections
4.4.1. Mutexes
- Mutex is short for MUTual EXclusion locks.
- A mutex is a special lock that only one thread may lock at a time.
- To create a mutex
- Call pthread_mutex_init
- The first argument is pthread_mutex_t
- The second argument is a mutex attribute object, NULL for default attributes.
pthread_mutex_t mutex;
pthread_mutex_init (&mutex, NULL);
- pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
- call pthread_mutex_lock to lock a mutex.
- call pthread_mutex_unlock to unlocks a mutex
- Job Queue Thread Function, Protected by a Mutex
#include <stdio.h>
#include <stdlib.h>
#include <malloc.h>
#include <pthread.h>
struct job
{
struct job* next;
int value;
};
struct job* job_queue;
void process_job(struct job*);
pthread_mutex_t job_queue_mutex = PTHREAD_MUTEX_INITIALIZER;
void * process_queue_function (void * arg)
{
while(1)
{
struct job* next_job;
pthread_mutex_lock(&job_queue_mutex);
if(job_queue == NULL)
next_job = NULL;
else
{
printf("begin removing a job...\n");
next_job = job_queue;
job_queue = job_queue->next;
printf("after removing a job...\n");
}
pthread_mutex_unlock(&job_queue_mutex);
if(next_job == NULL)
{
sleep(5);
continue;
}
process_job(next_job);
free(next_job);
}
return NULL;
}
void process_job(struct job* p)
{
printf("The value is : %d.\n", p->value);
}
void enqueue_job(struct job* new_job)
{
pthread_mutex_lock(&job_queue_mutex);
printf("begin inserting a job...\n");
new_job->next = job_queue;
job_queue = new_job;
printf("after inserting a job...\n");
pthread_mutex_unlock(&job_queue_mutex);
}
void * insert_queue_function(void * arg)
{
int i = 0;
while(i < 20)
{
sleep(1);
printf("put the value: %d.\n", i);
struct job* new_job = (struct job*)malloc(sizeof(struct job));
new_job->next = NULL;
new_job->value = i;
enqueue_job(new_job);
i++;
}
}
int main()
{
pthread_t insert_thread, process_thread;
pthread_create(&insert_thread, NULL, &insert_queue_function, NULL);
pthread_create(&process_thread, NULL, &process_queue_function, NULL);
pthread_join(insert_thread, NULL);
pthread_join(process_thread, NULL);
}
- Three kinds of mutexes exist:
- fast mutex (the default kind): locking this kind of mutex twice will cause a dead lock.
- recursive mutex: this kind of mutex is allowed to be locked many times, you can unlock the mutex the same times to release the mutex.
- error-checking mutex: the second lock to the mutex will return the failure code EDEADLK.
- Use mutex attribute object to create different kinds of mutexes.
pthread_mutexattr_t attr;
pthread_mutex_t mutex;
pthread_mutexattr_init (&attr);
pthread_mutexattr_setkind_np (&attr, PTHREAD_MUTEX_ERRORCHECK_NP);
pthread_mutexattr_setkind_np (&attr, PTHREAD_MUTEX_RECURSIVE_NP);
pthread_mutex_init (&mutex, &attr);
pthread_mutexattr_destroy (&attr);
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
pthread_mutex_t mutex;
void * thread_function(void * arg)
{
printf("try the mutex...\n");
pthread_mutex_lock(&mutex);
printf("get the mutex...\n");
pthread_mutex_unlock(&mutex);
printf("release the mutex...\n");
}
int main()
{
pthread_mutexattr_t attr;
pthread_mutex_t mutex;
pthread_t thread;
pthread_mutexattr_init(&attr);
pthread_mutexattr_setkind_np(&attr, PTHREAD_MUTEX_RECURSIVE_NP);
pthread_mutex_init(&mutex, &attr);
pthread_mutexattr_destroy(&attr);
pthread_mutex_lock(&mutex);
printf("lock the first time...\n");
pthread_mutex_lock(&mutex);
printf("lock the second time...\n");
pthread_create(&thread, NULL, &thread_function, NULL);
pthread_mutex_unlock(&mutex);
printf("unlock the first time...\n");
sleep(5);
pthread_mutex_unlock(&mutex);
printf("unlock the second time...\n");
}
- pthread_mutex_trylock can test whether a mutex is locked without actually blocking on it.
- If you call pthread_mutex_trylock on an unlocked mutex, you will lock the mutex and return zero.
- If the mutex is already locked, pthread_mutex_trylock will not block and return immediately with the error code EBUSY.
4.4.2. Semaphores
- A semaphore is a counter that can be used to synchronize multiple threads.
- GNU/Linux provides two kinds of semaphore implementations.
- Here is the POSIX standard semaphore implementation for communicating among threads.
- The other implementation is used for communication among processes.
- Each semaphore has a counter value, which is a non-negative integer.
- A semaphore supports two basic operations:
- A wait operation decrements the value of the semaphore by 1.
- If the value is already zero, the operation blocks until the value of the semaphore becomes positive. When the semaphore’s value becomes positive, it is decremented by 1 and the wait operation returns.
- A post operation increments the value of the semaphore by 1.
- If the semaphore was previously zero and other threads are blocked in a wait operation on that semaphore, one of those threads is unblocked and its wait operation completes
- A semaphore is represented by a sem_t variable.
- Initialize it using the sem_init function
- The first parameter is a pointer to the sem_t variable.
- The second parameter should be zero
- The third parameter is the semaphore’s initial value.
- Deallocate it with sem_destroy.
- To wait on a semaphore, use sem_wait.
- To post to a semaphore, use sem_post.
- The nonblocking wait function is sem_trywait
- To retrieve the current value of a semaphore, use sem_getvalue
#include <stdio.h>
#include <stdlib.h>
#include <malloc.h>
#include <pthread.h>
#include <semaphore.h>
struct job
{
struct job* next;
int value;
};
struct job* job_queue;
void process_job(struct job*);
pthread_mutex_t job_queue_mutex = PTHREAD_MUTEX_INITIALIZER;
sem_t job_queue_count;
void initialize_job_queue()
{
job_queue = NULL;
sem_init(&job_queue_count, 0, 0);
}
void * process_queue_function (void * arg)
{
while(1)
{
struct job* next_job;
sem_wait(&job_queue_count);
pthread_mutex_lock(&job_queue_mutex);
printf("begin removing a job...\n");
next_job = job_queue;
job_queue = job_queue->next;
printf("after removing a job...\n");
pthread_mutex_unlock(&job_queue_mutex);
process_job(next_job);
free(next_job);
}
return NULL;
}
void process_job(struct job* p)
{
printf("The value is : %d.\n", p->value);
}
void enqueue_job(struct job* new_job)
{
pthread_mutex_lock(&job_queue_mutex);
printf("begin inserting a job...\n");
new_job->next = job_queue;
job_queue = new_job;
sem_post(&job_queue_count);
printf("after inserting a job...\n");
pthread_mutex_unlock(&job_queue_mutex);
}
void * insert_queue_function(void * arg)
{
int i = 0;
while(i < 20)
{
sleep(1);
printf("put the value: %d.\n", i);
struct job* new_job = (struct job*)malloc(sizeof(struct job));
new_job->next = NULL;
new_job->value = i;
enqueue_job(new_job);
i++;
}
}
int main()
{
pthread_t insert_thread, process_thread;
pthread_create(&insert_thread, NULL, &insert_queue_function, NULL);
pthread_create(&process_thread, NULL, &process_queue_function, NULL);
pthread_join(insert_thread, NULL);
pthread_join(process_thread, NULL);
}
4.4.3. Condition Variables
- If thread A waits on a condition variable, it is blocked until some other thread, thread B, signals the same condition variable.
- Thread A must wait on the condition variable before thread B signals it.
- If thread B signals the condition variable before thread A waits on it, the signal is lost.
- Each condition variable must be used in conjunction with a mutex to prevent the race condition between checking the flag value and signaling or waiting on the condition variable.
- A condition variable is represented by an instance of pthread_cond_t.
- pthread_cond_init initializes a condition variable.
- pthread_cond_signal signals a condition variable.A single thread that is blocked on the condition variable will be unblocked.
- pthread_cond_broadcast unblocks all threads that are blocked on the condition variable.
- pthread_cond_wait blocks the calling thread until the condition variable is signaled.
- The argument is a pointer to the pthread_cond_t instance.The second
- argument is a pointer to the pthread_mutex_t mutex instance.
- When pthread_cond_wait is called, the mutex must already be locked by the calling thread. That function atomically unlocks the mutex and blocks on the condition variable.When the condition variable is signaled and the calling thread unblocks, pthread_cond_wait automatically reacquires a lock on the mutex.
- Control a Thread Using a Condition Variable
#include <pthread.h>
int thread_flag;
pthread_cond_t thread_flag_cv;
pthread_mutex_t thread_flag_mutex;
void initialize_flag ()
{
/* Initialize the mutex and condition variable. */
pthread_mutex_init (&thread_flag_mutex, NULL);
pthread_cond_init (&thread_flag_cv, NULL);
/* Initialize the flag value. */
thread_flag = 0;
}
/* Calls do_work repeatedly while the thread flag is set; blocks if the flag is clear. */
void* thread_function (void* thread_arg)
{
/* Loop infinitely. */
while (1) {
/* Lock the mutex before accessing the flag value. */
pthread_mutex_lock (&thread_flag_mutex);
while (!thread_flag)
/* The flag is clear. Wait for a signal on the condition variable, indicating that the flag value has changed. When the signal arrives and this thread unblocks, loop and check the flag again. */
pthread_cond_wait (&thread_flag_cv, &thread_flag_mutex);
/* When we’ve gotten here, we know the flag must be set. Unlock the mutex. */
pthread_mutex_unlock (&thread_flag_mutex);
/* Do some work. */
do_work ();
}
return NULL;
}
/* Sets the value of the thread flag to FLAG_VALUE. */
void set_thread_flag (int flag_value)
{
/* Lock the mutex before accessing the flag value. */
pthread_mutex_lock (&thread_flag_mutex);
/* Set the flag value, and then signal in case thread_function is blocked, waiting for the flag to become set. However, thread_function can’t actually check the flag until the mutex is unlocked. */
thread_flag = flag_value;
pthread_cond_signal (&thread_flag_cv);
/* Unlock the mutex. */
pthread_mutex_unlock (&thread_flag_mutex);
}
- A condition variable may also be used without a condition, simply as a mechanism for blocking a thread until another thread “wakes it up.”
4.5 GNU/Linux Thread Implementation
- On GNU/Linux, threads are implemented as processes.
(thread-pid) Print Process IDs for Threads (The test result of fedora is not like this.)
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
void* thread_function (void* arg)
{
fprintf (stderr, “child thread pid is %d\n”, (int) getpid ());
/* Spin forever. */
while (1);
return NULL;
}
int main ()
{
pthread_t thread;
fprintf (stderr, “main thread pid is %d\n”, (int) getpid ());
pthread_create (&thread, NULL, &thread_function, NULL);
/* Spin forever. */
while (1);
return 0;
}
- Compile and run
% cc thread-pid.c -o thread-pid -lpthread
% ./thread-pid &
[1] 14608
main thread pid is 14608
child thread pid is 14610
% ps x
PID TTY STAT TIME COMMAND
14042 pts/9 S 0:00 bash
14608 pts/9 R 0:01 ./thread-pid
14609 pts/9 S 0:00 ./thread-pid
14610 pts/9 R 0:01 ./thread-pid
14611 pts/9 R 0:00 ps x
% kill 14608
[1]+ Terminated ./thread-pid
- There are three processes running the thread-pid program.
- The first with pid 14608 is the main thread in the program
- The third with pid 14610 is the thread we created to execute thread_function.
- The second thread with pid 14609 is the “manager thread” which is part of the internal implementation of GNU/Linux threads.
- The manager thread is created the first time a program calls pthread_create to create a new thread.
- Signals sent from outside the program are sent to the process to the main thread of the program.
- Within a multithreaded program, it is possible for one thread to send a signal to another thread with the pthread_kill function.