Teaching:
FeedbackThis is an old revision of the document!
A process can run multiple threads – multiple flows of control.
In general, the threads share all resources (such as memory or open files).
Each thread uses a separate stack, and obviously has its own state of the CPU
registers. A limited set of properties might also be specific to a thread
(e.g., signal mask, CPU affinity).
Since 2012 the C and C++ languages include API for concurrency, including threads. However, it is very generic, so that it can be implemented in all operating systems.
POSIX | C | C++ | |
---|---|---|---|
create a new thread | pthread_create | thrd_create | std::thread constructor |
join a thread | pthread_join | thrd_join | std::thread::join |
detach a thread | pthread_detach | thrd_detach | std::thread::detach |
exit from a thread | pthread_exit | thrd_exit | — |
send signal to a specific thread | pthread_kill | — | — |
A process always starts with a single thread called the main thread.
When the main thread returns from main
function, or when any thread
calls exit
function, all threads are forcefully terminated.
The POSIX standard defines an API for threads called
pthreads.
While each operating system has its own implementation of threads, virtually
any Unix-like system implements the POSIX Threads API.
Linux implementation of threads is called
NPTL.
Apart from the standard pthread functions, it offers several Linux-specific
functions (which names end with _np
to indicate non-portability).
To compile any program that uses pthreads, one had to add -pthread
option
upon invoking compiler and linker.
This changed since version 2.34 of glibc, that moved POSIX thread routines to
standard C library (libc).
To start a new thread, one must point to the function that will be executed
by the thread.
The function must take a pointer to arbitrary data as an argument and return an
pointer to arbitrary data1):
void * start_routine (void * argument){ … return some_pointer; }
In C such function is of type void *(*)(void *)
, and a variable called
func
of this type should be declared as void *(*func)(void *)
.
A new thread will get an identifier of type pthread_t
that can be used later
on to refer to the thread.
The function starting a new thread has the following syntax:
Needs header:pthread.h
int pthread_create(pthread_t *restrict threadIdentifier,
const pthread_attr_t *restrict attr,
void *(*start_routine)(void *),
void *restrict arg);
It will write the thread identifier to the variable pointed by threadIdentifier
,
start a new thread and run start_routine(arg)
in it. If the attr
argument is not NULL, thread attributes will be set before starting the thread.
An example of creating a thread and passing it some data
#include <pthread.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> struct myStruct { int i; char s[16]; }; // function to be run by a thread; it must accept and return a 'void *' void *func(void *rawArg) { struct myStruct *arg = rawArg; printf("%d %s\n", arg->i, arg->s); free(arg); return NULL; } int main() { pthread_t ti; // pthread_t is a type that stores thread identifiers ('long unsigned int' in Linux) // to pass several data to a thread, a structure is created on heap struct myStruct *t1_arg = malloc(sizeof(struct myStruct)); t1_arg->i = 42; strcpy(t1_arg->s, "foo baz bar"); // the following line creates a new thread and runs func(t1_arg) in the thread. pthread_create(&ti, NULL, func, t1_arg); // terminating the main thread (or any other) with pthread_exit does not terminate the process pthread_exit(NULL); }
Just like a child process is remembered by the operating system until the parent process reaps it, a thread that has terminated is remembered by the operating system until another thread joins it. Alternatively, a thread can be explicitly detached so that it is immediately reaped upon termination and cannot be joined anymore.
By default, a thread starts in joinable state. One can create a set of attributes
(using pthread_attr_init
and pthread_attr_setdetachstate
) that tells the
OS to create a thread in the detached state.
To join a thread and collect its result one has to call the function:
Needs header: pthread.h
int pthread_join(pthread_t thread, void **value_ptr);
Calling the pthread_join
waits until the thread thread
terminates
and writes its return value to the void *
pointer pointed by value_ptr
.
The value_ptr
can be NULL, and then the return value is discarded.
An example code that creates 3 threads and joins them, collecting their result.
#include <pthread.h> #include <stdint.h> #include <stdio.h> #include <stdlib.h> #include <time.h> // this just does computation, there's nothing about threads here void monteCarloPi(uint64_t *hit, uint64_t *miss) { struct timespec ts; clock_gettime(CLOCK_REALTIME, &ts); srand(ts.tv_nsec); for (int i = 0; i < 1e6; ++i) { double x = rand() / (double)RAND_MAX; double y = rand() / (double)RAND_MAX; if ((x*x + y*y) < 1) (*hit)++; else (*miss)++; } } struct theResult { uint64_t hit; uint64_t miss; }; void *runPi(void *arg) { // preparing the return value - a structure 'theResult' struct theResult *ret = malloc(sizeof(struct theResult)); // filling it with data ret->hit = ret->miss = 0; monteCarloPi(&ret->hit, &ret->miss); // returning pointer to memory allocated on heap return ret; } int main() { pthread_t ti[3]; uint64_t hit = 0, miss = 0; for (int i = 0; i < 3; ++i) pthread_create(ti + i, NULL, runPi, NULL); monteCarloPi(&hit, &miss); for (int i = 0; i < 3; ++i) { // create a variable that will store the location of the result struct theResult *result; // wait until thread ti[i] exits, and write its return value to result pthread_join(ti[i], (void **)&result); // use the result hit += result->hit; miss += result->miss; // free the memory free(result); } printf("%f\n", 4. * hit / (hit + miss)); return 0; }
To detach a thread one has to call the function:
Needs header: pthread.h
int pthread_detach(pthread_t thread);
The pthread_detach
function returns without waiting. The value returned by
the detached thread will be discarded.
A thread can detach itself (i.e. call pthread_detach(pthread_self());
), but
typically the thread that spawned it calls the detach.
An example code that creates a thread and detaches it.
#ifdef __GNUC__ // whenever GNU Compiler or compatible is detected, #define _GNU_SOURCE // enable GNU Compiler extensions. #endif #include <pthread.h> #include <signal.h> #include <stdio.h> #include <string.h> #include <time.h> #include <unistd.h> void *buzzer(void *arg) { #ifdef _GNU_SOURCE // when GNU Compiler extensions are available, pthread_setname_np(pthread_self(), "buzzer"); // assign a name for the thread #endif int i = 0; while (1) { usleep(1e5); fprintf(stderr, "%06d BZZZZZTT\n", ++i); } } void usr1(int num) { signal(SIGUSR1, SIG_DFL); char name[33] = "USR1 received by "; #ifdef _GNU_SOURCE pthread_getname_np(pthread_self(), name + 17, 16); #endif int len = strlen(name); name[len] = '\n'; write(STDERR_FILENO, name, len + 1); pthread_exit(NULL); // terminate this thread } int main() { // create a thread pthread_t ti; pthread_create(&ti, NULL, buzzer, NULL); // and tell the operating system that the thread will never be joined pthread_detach(ti); sleep(1); signal(SIGUSR1, usr1); // notice that threads share signal handlers pthread_kill(ti, SIGUSR1); // sends a signal to a specified thread sleep(1); return 0; }
Exercise 1
In the main thread create a new thread, join it and then return from main.
In the newly created thread write Hello World
to the standard output.
Exercise 2
Remove the code you wrote for the previous exercise the pthread_join
and
re-run the code. What has changed in the behaviour of the program?
Exercise 3
In the main thread create 10 new threads, passing to each of them an ordinal
number. Then, join each thread and exit.
In the newly created threads write the number to the standard output.
Exercise 4 Add to the program from the previous exercise a global variable. In each thread read the variable and increment it. Output the obtained value together with the ordinal number, and return it from the thread entry routine. In the main thread, collect the returned numbers and display them.
In C, data items can have different storage duration and different linkage.
Until C11, there existed only static, automatic or allocated storage durations.
Roughly:
• static = global variables and static variables defined inside functions,
• automatic = items on stack, automatically allocated inside functions ('local variables'),
• allocated = items on heap, explicitly allocated and freed.
None of these cover a case when one wants a static ("global") variable with
a separate value for each thread.
Such storage duration is called thread local
and a coupe of ways to create thread-local items are presented in this section.
One can create keys (that may be understood as identifiers of variables) and
then associate, for each thread separately, a value for a key.
To this end the following functions can be used:
Need header:pthread.h
int pthread_key_create(pthread_key_t *key, void (*destructor)(void*))
int pthread_setspecific(pthread_key_t key, const void *value)
void *pthread_getspecific(pthread_key_t key)
Each key is initially associated with a NULL
for each thread.
An example code that creates a key, stores and retrieves the value from two threads.
#include <pthread.h> #include <stdio.h> #include <stdlib.h> #include <string.h> pthread_key_t mySpecificVal; void printMine() { printf("I have: %s\n", (char *)pthread_getspecific(mySpecificVal)); } void *routine(void *arg) { char *text = malloc(4); strcpy(text, "baz"); pthread_setspecific(mySpecificVal, text); printMine(); return NULL; } int main() { char *text = malloc(4); strcpy(text, "foo"); pthread_key_create(&mySpecificVal, free); pthread_setspecific(mySpecificVal, text); pthread_t ti; pthread_create(&ti, NULL, routine, NULL); pthread_join(ti, NULL); printMine(); return 0; }
Starting with C11, a new storage duration _Thread_local
or
thread_local
2) is defined.
Variables defined with this storage duration have unique value for each thread.
Notice that the thread_local
variables can have initial values (unlike in
POSIX), but there is no way to provide a custom destructor (unlike in POSIX).