Concurrency and asynchronous programming in C with the library pthread.
What is Concurrency?
Concurrency is the ability of a computer system to execute multiple sequences of instructions simultaneously.
This does not necessarily mean they are running at the exact same time (as in parallelism) but that the system manages multiple tasks to appear as though they are being executed at the same time.
Concurrency improves the efficiency and responsiveness of programs.
What is a thread?
A thread is the smallest unit of processing that can be scheduled by an operating system.
It is a sequence of instructions within a program that can be managed independently.
Threads share the same process resources, including memory and file descriptors, but they run independently and can be executed simultaneously, allowing for multitasking within a single program.
Using threads, you can perform background operations, handle multiple I/O operations, or parallelize tasks to improve performance.
Pthread
POSIX Threads, or Pthreads, is an execution model that exists independently from a programming language
Let’s dive in!
In C we can use pthread functionality by importing “pthread.h”.
Pthread in C includes some nice functions we can use to create threads, join threads exit threads, and detach threads.
Creating a Thread
First, you need to create a variable of type pthread_t that holds the identifier of a thread.
#include <pthread.h>
int main(void) {
pthread_t threadID;
return 0;
}
Create the thread. pthread_create() takes 4 parameters.
- Reference to the thread identifier variable.
- Custom attributes, if you want to use the default set it to NULL.
- A thread function. Here you specify the instructions related to the thread.
- An argument associated with the thread function. If none set it to NULL.
For now, let’s keep attributes and arguments set to null.
int status;
status = pthread_create(&threadID, NULL, myThreadFunction, NULL);
if (status != 0) {
printf("Error creating thread\n");
exit(-1);
}
The pthread_create function returns an integer. A return value of 0 indicates success; otherwise, an error occurred. Store the value and handle the error if it occurs.
Thread Function
When specifying the function associated with the thread there are some rules to follow. The return type is a void pointer. Void can be used when a function won’t return anything but in C void is also a generic type to tell the compiler that the return type is not specified. The function also receives a void pointer as an argument.
When the function is done executing, use pthread_exit() to exit the thread and optionally return a value. If the function won’t return anything, set it to NULL.
void* threadFunction(void* arg) {
pthread_exit(NULL);
}
Joining a Thread
The main thread can join the created thread, which means the main thread will wait for the thread to finish and optionally retrieve the return value. Use the thread identifier and a double pointer to a variable to store the return value (or NULL if not needed).
pthread_join(threadID, NULL);
Thread Functions with arguments and a return value.
When creating the thread you can pass a pointer of any type.
Create a variable and reference it in the pthread create function.
int argument = 5;
pthread_create(&threadID, NULL, myThreadFunction, &argument);
Observe that the "argument" variable is scoped to the main function and will go away when the main function returns. Later, we will talk about detaching threads and then it can be a good idea to create dynamic memory for the variable. For now, it’s fine though as the main function will join the thread and not finish before the created thread is done.
For the thread function to use the argument passed you first need to cast the argument to the right type.
We also want to return a value and then we need to cast the return value to void pointer.
void* myThreadFunction(void* argument) {
int passedValue = *(int*)argument;
int* returnValue = (int*)malloc(sizeof(int));
*returnValue = passedValue * 2;
pthread_exit((void*)returnValue);
}
Notice how we cast the argument of type void* to int*. We dereference the pointer with * to get the value.
We also store the value in dynamic memory to make it persist in memory after the function execution. Then cast it from int* to void* and return it.
The pthread join function's second argument is a double pointer. The pointer points to a pointer that holds the return value 🤯.
void* valueReturned;
// second argument is a reference to valueReturned.
pthread_join(threadID, &valueReturned);
Now we can print the result to the standard output, first check that it exists, and cast it to int*.
void* valueReturned;
pthread_join(threadID, &valueReturned);
if (valueReturned != NULL) {
int result = *(int*)valueReturned;
printf("%d\n", result);
free(valueReturned); // Don't forget to free the allocated memory
}
Thread Attributes
You’ve got a good understanding of working with threads so far, this leads us to talk about attributes.
The second argument in the pthread create function is an attribute object you can customize or leave as default by setting it to NULL.
This is how you can customize the attributes object.
- Change the thread state to joinable or detached.
- Set a scheduling policy.
- Set scheduling priority.
- Change the size of the stack memory associated with the thread.
Initialization of attribute object.
To initialize the attribute object, create a variable of type pthread_attr_t and pass the reference to the pthread_attr_init function.
pthread_attr_t attr;
pthread_attr_init(&attr);
Joinable vs detached thread.
By default, the attribute is set to joinable, meaning the parent thread waits for the thread to finish and optionally retrieves a return value.
Detached threads on the other hand are independent and the parent thread is not joining the thread. For example, the main thread will not wait for it to finish.
Instead detached threads are fired and forgotten. You cannot receive a return value from detached threads.
We set the state of the attribute to detach state, pass the attribute with the create function, and then destroy the attribute object and forget about it and the system will clean up recourses when the execution is done.
#include <pthread>
void* threadFunction(*void arg) {
pthread_exit(NULL);
}
int main(void) {
pthread_t threadID;
pthread_attr_t attr;
pthread_attr_init(&attr);
Pthread_attr_setdetachstate(&attr,PTHREAD_CREATE_DETACHED);
pthread_create(&threadID, &attr, threadFunction, NULL);
pthread_attr_destroy(&attr);
return 0;
}
Scheduling policy.
To understand scheduling priority you must first know that a computer's CPU is handling the instructions of a program. A CPU can have multiple cores and each core can independently execute the instructions of a program. A thread is a sequence of instructions handled by one of the cores.
There are probably more threads than cores and the operating system is responsible for delegating the threads to the cores when available and otherwise be queued.
But what thread goes first and what thread should wait in the queue? That’s when scheduling policies come into place.
Different scheduling policies are:
- SCHED_OTHER, the default, which lets the operating system handle priority based on factors such as thread behavior and system load.
- SCHED_FIFO, which follows the first-in, first-out principle based on priority.
- SCHED_RR, which stands for round-robin and ensures that threads with the same priority equally share CPU time.
SCHED_FIFO and SCHED_RR are good for real-time applications where performance is critical.
With those two policies, you can pass a value of the priority to the thread attribute.
The range for the priority is decided from the system and you can use the methods sched_get_priority_min and sched_get_priority_max.
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <sched.h>
#include <unistd.h>
void* threadFunction(void* arg) {
printf("Thread is running\n");
sleep(1); // from unistd lib
printf("Thread is finishing\n");
pthread_exit(NULL);
}
int main(void) {
pthread_t thread;
pthread_attr_t attr;
struct sched_param param;
int policy;
// Initialize the attribute object
pthread_attr_init(&attr);
// Set the scheduling policy to SCHED_FIFO
pthread_attr_setschedpolicy(&attr, SCHED_FIFO);
// Set the priority
int maxPriority = sched_get_priority_max(SCHED_FIFO);
int minPriority = sched_get_priority_min(SCHED_FIFO);
param.sched_priority = (maxPriority + minPriority) / 2;
pthread_attr_setschedparam(&attr, ¶m);
// Create the thread with the specified attributes
int status = pthread_create(&thread, &attr, threadFunction, NULL);
if (status != 0) {
printf("Error creating thread\n");
exit(-1);
}
// Destroy the attribute object
pthread_attr_destroy(&attr);
// Wait for the thread to finish
pthread_join(thread, NULL);
return 0;
}
Set the stack size.
The operation system has a default stack size that should be sufficient. But there might be situations where you know there will be a great amount of data stored in local variables or a lot of recursions.
You can then increase the size of the stack to prevent a stack overflow.
int status;
status = pthread_attr_setstacksize(&attr, 1024 * 1024);
if (status != 0) {
fprintf(stderr, "Error setting stack size\n");
exit(EXIT_FAILURE);
}
Happy coding!
Top comments (0)