Open-Source Internship opportunity by OpenGenus for programmers. Apply now.
A thread is the basic unit of processor utilization that consists of a program counter, stack and registers. In this article we discuss how they are created, how data is passed between threads, waiting in threads and attributes.
Table of contents.
- Introduction.
- Creating threads.
- Passing data.
- Waiting in threads.
- Attributes.
- Summary.
- References.
Prerequisites.
- Linux processes: listing, creation, scheduling
- Linux processes: signals, termination, zombies, cleanup
- Linux threads: cancellation, data and cleanup
Introduction.
Threads allow a program to do more than one thing at a time(multitask). Threads exist within processes.
All threads have a unique identifier, thread ID.
In Linux threads run concurrently and are scheduled by the kernel asynchronously and hence the kernel interrupts a threads to allow another to execute from time to time.
When a program is invoked, a new process is created and the process will have a single thread. This thread can in turn create other multiple threads, they all run the same program but may be executing different parts of the program.
The program itself and its threads will share system resources such as memory space.
Linux implements POSIX thread API - pthreads whereby all thread functions and data types are within the <pthread.h> header file.
Creating threads.
The type pthread_t is used reference thread IDs in a C/C++ program. When a thread is created a thread function is executed, these functions take a single parameter-thread argument of type void* and return a void* type.
Thread arguments can be used to pass data to a new thread or we can use the returned value to pass data from the current thread to its creator.
The pthread_create creates a new thread it takes, a pointer to pthread_t variable which stores new thread ID, a pointer to a thread attribute object which controls details of thread interaction with the program, a pointer to the thread function and a thread argument value of void* type.
Creating a thread - createThread.c
#include<pthread.h>
#include<stdio.h>
// Print x to stderr. Parameter unused doesn't return.
void* printXs(void* unused){
while(1)
fputc("x", stderr);
return NULL;
}
//main program
int main(){
pthread_t threadID;
// Create new thread that runs printXs
pthread_create(&threadID, NULL, &printXs, NULL);
// Print o’s continuously to stderr.
while(1)
fputc("o", stderr);
return 0;
}
Compilation, linking and execution
cc -o createdThread createThread.c -lpthread
#execute
./createdThread
From the output we see two threads x's and o's scheduled alternately.
A thread can terminate by either it returning from the thread function or explicitly calling pthread_exit which can be called within the thread function or another function directly or indirectly.
Passing data.
We can pass data to threads by using thread arguments since their types is void*.
Although it is possible not a lot of data can be passed using this method therefore to pass a lot of data we instead pass a pointer to a structure which will hold the data we intend to pass. With this method we define structures for each thread function.
As we shall see in the example below, by use of thread arguments, we can reuse a thread function for many threads, i.e all treads execute the same code but on different data.
passingData.c
#include<pthread.h>
#include<stdio.h>
// print function parameters
// characters to print, number of times to print it
struct printParams{
char character;
int count;
};
// printing characters
void* printChars(void* parameters){
// cast cookie pointer to right type
struct printParams* pp = (struct printParams*) parameters;
for(int i = 0; i < pp->count; ++i)
fputc(pp->character, stderr);
return NULL;
}
int main(){
pthread_t thread1ID;
pthread_t thread2ID;
struct printParams thread1Args;
struct printParams thread2Args;
// new thread to print 1000 x's
thread1Args.character = 'x';
thread1Args.count = 1000;
pthread_create(&thread1ID, NULL, &printChars, &thread1Args);
// new thread to print 2000 o's
thread2Args.character = 'o';
thread2Args.count = 2000;
pthread_create(&thread2ID, NULL, &printChars, &thread2Args);
return 0;
}
From the above program, the main thread(main function) creates the thread parameter structures(*thread1Args, thread2Args) as local variable then passes their pointers to threads created by it, however this is a problem(bug) in that scheduling of the three threads can happen such that main completes execution before the two are done and when this happens, memory containing thread parameters structures gets deallocated. Do you see the problem?
Waiting in threads.
A solution would be to make main wait until the two threads are done however wait functions are for processes, in threads the pthread_join function is used for such cases. It takes the thread ID of the thread to wait for and a pointer to void* variable that receives the thread's return value after it completes.
A revised main function - passingData.c
// Making sure the 1st and 2nd thread finish
pthread_join (thread1ID, NULL);
pthread_join (thread2ID, NULL)
The above code is added just before returning.
From that we learn that data passed to a thread by reference should not be deallocated until we know it is of no use. This is also applicable to local variables and heap-allocated variables.
Return values.
We passed NULL as the second parameter to pthread_join however if it is not null, the thread's return value is placed in the location pointed to by the argument.
This value is of type void* therefore assuming we want to pass back an int we can cast it to void* then cast it back to the required type after calling pthread_join function.
Attributes.
With attributes we can fine tune a thread's behavior.
We can create and customize thread attribute objects to specify other attribute values.
Steps for thread attributes customization
- First create pthread_attr_t object
- Call pthread_attr_init passing a pointer to the created object so as to initialize attributes to their default values.
- Modify object with desired values.
- Pass a pointer to the object when calling pthread_create.
- To release object call pthread_attr_destroy.
A single thread attribute object can be used to start several threads after which we can release it.
A thread can be joinable or detached, the former is not cleaned up automatically when it terminates(zombie process), the latter is cleaned up automatically when it terminates.
A detach state attribute is commonly of interest in application programming tasks.
We use pthread)attr_setdetachstate is used to set the detach state in a thread, it takes a pointer to the thread attribute object and the desired detach state as arguments.
An example
#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;
}
Note that even when a thread is created in a joinable state it can be turned to a detached state by calling pthread_detach and once detached it can't be made joinable.
Summary.
Threads allow a program to do multiple tasks at a time.
The Linux kernel schedules threads asynchronously, interrupting a thread from time to time so as to allow another to execute.
A process creates threads and threads can also create other threads all which run a single program each executing a different part of the program.
References.
- Advanced Programming in the UNIX Environment 3rd Edition W. Richard Stevens
- Advanced Linux Programming Mark Mitchell, Jeffrey Oldham,and Alex Samuel.