Instead of our classic view of a single point of execution within a computer program (i.e., a single PC where instructions are being fetched from and executed),a multi-threaded program has more than one point of execution (i.e., multiple PCs, each of which is being fetched and executed from). Perhaps another way to think of this is that each thread is very much like a separate process, except for one difference: they share the same address space and thus can access the same data.
In C, threading can be achieved using the POSIX thread (pthread) library, which provides a powerful set of functions for creating and managing threads. Here, I’ll introduce the basic concepts of threading in C with examples.
The following example demonstrates how to create a thread, execute a function in that thread, and then wait for the thread to finish.
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
// Function to be executed by the thread
void* myThreadFunction(void* arg) {
int* myid = (int*) arg;
printf("Thread ID: %d\n", *myid);
return NULL;
}
int main() {
pthread_t thread;
int thread_id = 1;
// Create a new thread
if (pthread_create(&thread, NULL, myThreadFunction, (void*) &thread_id)) {
fprintf(stderr, "Error creating thread\n");
return 1;
}
// Wait for the thread to complete
if (pthread_join(thread, NULL)) {
fprintf(stderr, "Error joining thread\n");
return 2;
}
printf("Thread has finished executing\n");
return 0;
}
- pthread.h: The header file that includes the definitions for the POSIX thread library.
- pthread_create: Creates a new thread. It takes four arguments:
- A pointer to a pthread_t variable where the thread ID will be stored.
- Thread attributes (NULL for default attributes).
- The function to be executed by the thread.
- A single argument to be passed to the function.
- pthread_join: Waits for the thread specified by the thread ID to finish execution.
If you’ve got multiple processors either that’s multiple cores virtual cores in a hypothetically a multi-core processor you want to run your algorithm or on multiple CPU instances so you need to split it up and the operating system needs to find some way of controlling all those different instances actually you can still be beneficial on a single CPU system because it might be times where part of your program is waiting for say network input so the operators will then schedule it in another thread of execution.
So one way you could break your program up into multiple chunks is to actually have multiple processes running in for a while people did this they sort of will use the command a function in UNIX for example called fork you would split the program into 1/2 which would run off as it was the other would have a new process identifier and the operating system would copy that block and create a new list of what memory uses a new list of the files they had open now these are point back to the same files the same blocks memory allocations and things so that if you change one memory location it updated on the other side and things if you changed a file seat to a different part of a file for example it would update in the others but they would be separate processes and they’d have to have all the storage required to do that and that worked but if you’re breaking your program up so you can run your task on multiple CPUs most of them are going to have the same list of files the same memory configuration the everything else that the operating system needs to keep track of the only thing that’s going to be different is where it is in the program what bit of code is running from what part and so the idea that came along was ok let’s have something lighter weight than a process and they called it a thread because you’ve got your thread of execution through the instructions and what we have is that each process will have at least one thread of execution but could have many.
So you have multiple threads of execution in your program so your programs running one thread which it starts off with that will then create another thread which does something and then another thread and so on can do different parts they made last for the whole length of the program they may only last for a short part while that task being completed and so on. You may have multiple ones that get jobs from different things and so on how you structure your program doesn’t matter but from the operating system point of view it’s now scheduling different threads onto the different cause whether that’s one to four however many you’ve got in your system so the idea behind multi-threading is a sort of lightweight way of saying to the operating system these are the different things I want running on my computer whether that’s always part of one program or multiple threads through multiple programs and the operating systems choosing which one of them actually gets CPU times on any of the CPU calls physical or virtual that they have available
Now that it produces if you were actually one program that you’ve split into multiple threads that introduces some interesting issues because you can get to the point where your programs can no longer do the task that you expect them to do, a program which was just one single stream of instructions executing one after the other and so he knew the order they’d execute in two to multiple strengths of instruction that are executing sometimes in parallel sometimes one after the other but you no longer have any control over the order that would happen because the operating system might put two of their threads on at the same time or I might let one thread run and then put the other thread on after the other and then go back and you can all control, where it’s switch from one to the other.
Consider the following code:
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
int counter = 0; // Shared counter
void* incrementCounter(void* arg) {
for (int i = 0; i < 100000; ++i) {
counter++;
}
return NULL;
}
int main() {
pthread_t threads[10];
// Create 10 threads
for (int i = 0; i < 10; i++) {
if (pthread_create(&threads[i], NULL, incrementCounter, NULL)) {
fprintf(stderr, "Error creating thread\n");
return 1;
}
}
// Wait for all threads to complete
for (int i = 0; i < 10; i++) {
if (pthread_join(threads[i], NULL)) {
fprintf(stderr, "Error joining thread\n");
return 2;
}
}
What do you think will happen?
So if we look at what’s happening, it’s loading the value in it’s adding the value onto it and then it’s doing it back but of course, we’ve got multiple systems and so at some point this program was going to start executing the same code so let’s cross this instruction out for now we’ll put it back in a bit later on
When multiple threads attempt to modify a shared resource simultaneously, the outcome depends on the timing of the threads. This can lead to inconsistent and incorrect results. In the above example, the final value of counter is not guaranteed to be 100000 because the increments might overlap.
So what we need to do is find a way to synchronize these two things so that as soon as this is loaded the value in and it’s going to add something on to it it stops. Read the next article on Mutex to find out more!


Leave a comment