Multithreading In C

What are processes and threads?

A thread serves as the essential unit for executing any process. A collection of processes constitutes a program, with each process composed of threads that serve as the fundamental units. Thus, a thread can be viewed as the foundational component of a process or the basic unit that collectively influences CPU usage.

The following items are included in a thread:

Thread ID:

It is a unique thread identifier that is created when a thread is initialized and remains constant throughout the thread's lifespan.

Program counter:

It is a value that the hardware loads .

A registered set:

It is a collection of common registers .

A stack:

It is a remnant of that specific thread .

In a scenario where two threads operate concurrently within a single process, they jointly access code, data sections, and various operating system resources such as file openings and signal handling. Unlike a heavyweight process that can only manage a single thread, a multi-threaded approach enables the execution of multiple tasks simultaneously. Employing threads enhances system efficiency significantly, making them a valuable asset.

The explanation of the difference between single and multithreading in C is as follows. Initially, a single-threaded process operates where the entire block, comprising code, data, etc., is treated as a single entity with only one thread. This implies that this approach handles only one task at a time. Conversely, in a multithreading process, multiple threads manage various activities such as code execution, stack management, data processing, and file operations. Each thread possesses its stack and registers, enabling the execution of multiple tasks simultaneously, hence referred to as a multithreading process.

Thread comes in two varieties:

Thread at user level:

It operates at the user level, as the name suggests. The kernel does not have permission to access its data.

Thread at the kernel level

The type of thread pertains to the thread's connection to the kernel and the system's operating system.

The process refers to the sequence of actions undertaken to execute a program. Running a program does not initiate immediate execution. Instead, it is divided into several fundamental stages that are systematically executed in sequence to eventually initiate the process.

A procedure that has been divided into smaller stages is known as a "clone or child process", whereas the initial process is denoted as the "parent" process. Each process occupies a specific amount of memory space that is exclusive to that particular process and not shared with any other processes.

A process progresses through several stages before it is executed.

In this situation, a new process is generated .

READY-

When a task is readied and awaiting allocation to a processor, it is in this particular condition.

RUNNING-

When the process is active, it is the state.

WAITING-

When a program reaches this condition, it indicates that there is an impending action to be executed.

TERMINATED-

It is the condition in which the process is being executed.

Why is C multithreaded?

Utilizing the concept of multithreading in C can significantly boost an application's capabilities through parallel processing. For instance, envision having multiple tabs open in a web browser where each tab functions simultaneously, akin to individual Threads. If we take Microsoft Excel as an example, one Thread could be responsible for managing text formatting while another Thread could oversee input handling. Consequently, the multithreading functionality in C streamlines the execution of numerous tasks concurrently. The instantiation of a Thread is notably prompt, facilitating swift task initiation. Moreover, the transition of context between Threads occurs expeditiously. Furthermore, facilitating swift communication between Threads and straightforward Thread termination are additional advantages of C's multithreading feature.

How to Write C Programmes for Multithreading?

While the C language does not inherently support multithreading, it can be achieved depending on the underlying operating system. The threads.h standard library is typically utilized for incorporating multithreading concepts in C; however, no compiler currently offers this capability. To enable multithreading in C, we need to utilize platform-specific solutions like the "POSIX" threads library, which involves including the header file pthread.h. "Pthreads" is an alternative term for this library. Creation of a POSIX thread can be accomplished in the following manners:

Example

#include <pthread.h>
pthread_create (thread, attr, start_routine, arg)

In this scenario, invoking Pthread_create generates a fresh thread to render the thread executable. This function enables you to incorporate multithreading functionality in C repeatedly within your code. The details of the parameters and their explanations mentioned previously are presented below.

thread:

It is a unique identifier that the subprocess yields.

attr:

When we need to define thread characteristics, we utilize this opaque attribute.

start_routine:

When the start_routine is created, the thread will execute a specific routine.

The argument received by the start_routine. If no arguments are provided, NULL will be utilized.

Certain C multithreading examples

Here are a few instances of multithreading challenges in the C programming language.

1. The Reader-Writer Issue

A common challenge in operating systems related to process synchronization is the reader/writer problem. Consider a scenario where we have a database accessible to two distinct user groups: Readers and Writers. Readers are authorized to only view the database content, while Writers have permissions to both read and update the database. To illustrate this concept, let's consider the Indian Railway Catering and Tourism Corporation (IRCTC) as an example. When checking the status of a specific train number, users can simply input the train number to access relevant train details. This action corresponds to a read operation. However, when reserving a ticket, users need to fill out a booking form with personal details such as name and age, which constitutes a write operation. Consequently, modifications are made to the IRCTC database to reflect these updates.

The problem occurs when multiple individuals try to access the IRCTC database at the same time. These individuals may act as either writers or readers. A conflict arises if a reader is currently using the database and a writer tries to access it concurrently to modify the same data. Similarly, a conflict arises when a writer is using the database, and a reader attempts to access the same data. Additionally, a challenge arises when one writer is updating the database while another writer is also trying to update data on the same database. Lastly, a situation arises when two readers seek to retrieve the same information. These challenges stem from the shared usage of database data by both readers and writers.

Semaphore is a technique utilized to address this problem. Let's examine a demonstration of how to apply this solution.

Reader process:

Example

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>

int rc = 0; // Reader count
int data = 0; // Shared data
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_twrt = PTHREAD_COND_INITIALIZER;

void* reader(void* arg) {
 int reader_id = *(int*)arg;

pthread_mutex_lock(&mutex);
rc++;

 if (rc == 1)
pthread_cond_wait(&wrt, &mutex);

pthread_mutex_unlock(&mutex);

 // Reading the shared data
printf("Reader %d reads data: %d\n", reader_id, data);

pthread_mutex_lock(&mutex);
rc--;

 if (rc == 0)
pthread_cond_signal(&wrt);

pthread_mutex_unlock(&mutex);

 return NULL;
}

int main() {
pthread_treaders[5]; // Assuming 5 reader threads

 int reader_ids[5];
 for (int i = 0; i< 5; i++) {
reader_ids[i] = i + 1;
pthread_create(&readers[i], NULL, reader, &reader_ids[i]);
 }

 // Joining reader threads
 for (int i = 0; i< 5; i++) {
pthread_join(readers[i], NULL);
 }

 return 0;
}

Output:

Output

Reader 1 reads data: 0
Reader 2 reads data: 0
Reader 3 reads data: 0
Reader 4 reads data: 0
Reader 5 reads data: 0

Explanation:

In this code snippet, we are working with the variable data that is shared among processes, along with the reader count represented by rc. The condition variable wrt is employed to control the access of the writer process, while the mutex ensures exclusive access to the shared data.

The reader process is symbolized by the reader method. The reader count (rc) is incremented prior to acquiring the mutex lock. It employs pthreadcondwait to pause on the wrt condition variable when it is the initial reader (rc == 1). Consequently, authors will be halted from writing until all readers have finished.

The reader process verifies whether it was the final reader (rc == 0) and decrements the reader count (rc--) after accessing the shared data. If it indeed is the last reader, pthreadcondsignal is used to notify the wrt condition variable, allowing any waiting writer processes to proceed.

By employing the pthreadcreate and pthreadjoin functions, we create and synchronize numerous reader threads within the main function. Each reader thread is allocated a unique identifier to facilitate identification.

Writer process:

Example

wait(wrt);
.
. WRITE INTO THE OBJECT
.
signal(wrt);

In a manner analogous to the reader process, a procedure called the wait operation is executed on "wrt" when a user intends to retrieve the data or object. Subsequently, the object becomes inaccessible to another user. Following the completion of the writing task by the user, a signal operation is then conducted on "wrt".

2. lock and unlock problem:

The concept of a mutex is applied in C programming for multithreading to ensure the prevention of race conditions among threads. When multiple threads simultaneously access the same data, it leads to a situation known as a race condition. To address this issue, the lock and unlock functions of a mutex are employed to safeguard a specific section of code for an individual thread, preventing other threads from executing the same operation concurrently. This safeguarded code segment is commonly referred to as the "critical section" or "critical region." It is essential to lock the shared resources before utilizing them and subsequently unlock them after completing their usage.

Let's explore how the mutex functions for acquiring and releasing locks in multithreading within the C programming language:

Example:

Example

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>

pthread_mutex_tmy_mutex = PTHREAD_MUTEX_INITIALIZER;
int shared_data = 0;

void *thread_function(void *arg) {
pthread_mutex_lock(&my_mutex);

shared_data++; // Modify the shared data
printf("Thread %ld: Shared data modified. New value: %d\n", (long)arg, shared_data);


pthread_mutex_unlock(&my_mutex);

 return NULL;
}

int main() {
pthread_tthreads[5]; // Assuming 5 threads
 for (int i = 0; i< 5; i++) {
 if (pthread_create(&threads[i], NULL, thread_function, (void *)(long)(i + 1)) != 0) {
fprintf(stderr, "Error creating thread %d\n", i + 1);
 return 1;
 }
 }

 for (int i = 0; i< 5; i++) {
 if (pthread_join(threads[i], NULL) != 0) {
fprintf(stderr, "Error joining thread %d\n", i + 1);
 return 1;
 }
 }

 return 0;
}

Output:

Output

Thread 1: Shared data modified. New value: 1
Thread 2: Shared data modified. New value: 2
Thread 3: Shared data modified. New value: 3
Thread 4: Shared data modified. New value: 4
Thread 5: Shared data modified. New value: 5

Explanation:

In the example provided, the process of securing and releasing a specific code section to prevent concurrent access is detailed. An instance of 'pthreadmutext' serves as the initialization method in the aforementioned scenario. Following this, 'pthreadmutexlock' is implemented prior to the commencement of the code block requiring protection. Subsequently, the execution of the protected code concludes. To release the lock on the code, 'pthreadmutexunlock' is employed, thereby reverting the code to an unlocked state.

The Dining Philosopher Problem:

One common challenge related to synchronization is the dining philosopher problem. It involves managing resources for multiple processes without causing deadlock or starvation. This problem serves as a basic illustration of multiple processes competing for resources. To prevent any process from getting blocked or ceasing operation, it is crucial to allocate resources effectively across all processes.

Assume there are five scholars positioned around a circular table. They dine at one moment and contemplate various topics at another. The individuals are equally distributed on the seats encircling the round table. Moreover, a dish of rice and a set of five chopsticks are placed in the center of the table for each scholar. In situations where a scholar is unable to communicate effectively with the neighboring colleagues.

A philosopher sometimes opts for two chopsticks when feeling hungry. These are selected from the nearby neighbors, one on the left and one on the right, as long as they are easily accessible. It is essential for the philosopher to only handle one chopstick at a time and not attempt to take the one being used by the neighbor.

Example:

Let's utilize an example to illustrate the implementation of this concept in the C programming language.

Example

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <semaphore.h>
#include <unistd.h>
	
pthread_tphilosopher[5];
pthread_mutex_tchopstick[5];

void *func(void *arg)
{
 int n = *(int *)arg;
printf("\nPhilosopher %d is thinking.", n);
pthread_mutex_lock(&chopstick[n]);
pthread_mutex_lock(&chopstick[(n + 1) % 5]);

printf("\nPhilosopher %d is eating.", n);

sleep(3);
pthread_mutex_unlock(&chopstick[n]);
pthread_mutex_unlock(&chopstick[(n + 1) % 5]);
printf("\nPhilosopher %d Finished eating ", n);
 return NULL;
}

int main()
{
 int i, k;
 void *message;
 for (i = 0; i< 5; i++)
 {
 k = pthread_mutex_init(&chopstick[i], NULL);
 if (k != 0)
 {
printf("Failed to initialize the mutex\n");
exit(1);
 }
 }
 for (i = 0; i< 5; i++)
 {
 k = pthread_create(&philosopher[i], NULL, func, (void *)&i);
 if (k != 0)
 {
printf("Error in thread creation.\n");
exit(1);
 }
 }
 for (i = 0; i< 5; i++)
 {
 k = pthread_join(philosopher[i], &message);
 if (k != 0)
 {
printf("Failed to join the thread.\n");
exit(1);
 }
 }
 for (i = 0; i< 5; i++)
 {
 k = pthread_mutex_destroy(&chopstick[i]);
 if (k != 0)
 {
printf("Mutex destroyed.\n");
exit(1);
 }
 }

 return 0;
}

Output:

Output

Philosopher 0 is thinking.
Philosopher 1 is thinking.
Philosopher 2 is thinking.
Philosopher 3 is thinking.
Philosopher 4 is thinking.

Philosopher 0 is eating.
Philosopher 1 is eating.
Philosopher 2 is eating.
Philosopher 3 is eating.
Philosopher 4 is eating.

Philosopher 0 Finished eating
Philosopher 1 Finished eating
Philosopher 2 Finished eating
Philosopher 3 Finished eating
Philosopher 4 Finished eating

Explanation:

Chopsticks in this scenario can be symbolized using a semaphore. When chopsticks are placed on the table without any philosopher having selected them, all chopsticks' attributes are initially set to 1. After chopstick[i] is chosen as the initial chopstick, the first wait operation is executed on chopstick[i] and chopstick[(i+1)%5]. This signifies that the philosopher has taken these chopsticks. Subsequently, the philosopher begins the eating process by picking up his chopstick. Once the philosopher completes the meal, a signal operation is performed on chopsticks [i] and [(i+1)%5]. The philosopher then returns to a resting state.

To verify if the subthread has been incorporated into the main thread, we employed the pthreadjoin function. Likewise, we validated the initialization of the mutex lock through the pthreadmutex_init method.

To commence and ascertain the creation of a new thread, we employed the pthreadcreate function. Correspondingly, we dismantled the mutex lock by utilizing the pthreadmutex_destroy function.

The Producer-Consumer Problem:

A common issue with multithreading process synchronization is the producer-consumer problem . Two processes are present in it: the first is the producer's process , and the second is the consumer's process . Furthermore, it is assumed that both operations are occurring concurrently in parallel. Additionally, they are a cooperative process, which implies that they are sharing something with one another. It is important that when the buffer is full , the producer cannot add data. When the buffer is empty, the consumer cannot extract data from the buffer because the common buffer size between the producer and the consumer is fixed . The issue is stated in this way. Hence, to implement the Producer-Consumer problem and solve it, we shall employ the idea of parallel programming.

Example:

Example

#include <stdio.h>	
#include <stdlib.h>	

int mutex = 1, full = 0, empty = 3, x = 0;

int main()	
{
 int n;
 void producer();
 void consumer();
 int wait(int);
 int signal(int);
printf("\n1.producer\n2.consumer\n3.for exit");
 while (1)
 {
printf("\n Please enter your choice:");
scanf("%d", &n);
 switch (n)
 {
 case 1:
 if ((mutex == 1) && (empty != 0))
producer();
 else
printf("Oops!! the buffer is full!!");
 break;
 case 2:
 if ((mutex == 1) && (full != 0))
consumer();
 else
printf("Oops!! the buffer is empty!!");
 break;
 case 3:
exit(0);
 break;
 }
 }

 return 0;
}

int wait(int s)
{
 return (--s);
}

int signal(int s)
{
 return (++s);
}

void producer()
{
 mutex = wait(mutex);
 full = signal(full);
 empty = wait(empty);
 x++;
printf("\nItem produced by the Producer %d", x);
 mutex = signal(mutex);
}

void consumer()
{
 mutex = wait(mutex);
 full = wait(full);
 empty = signal(empty);
printf("\nItem consumed by the Consumer %d", x);
 x--;
 mutex = signal(mutex);
}

Output:

Output

1. producer
2. consumer
3. for exit
Please enter your choice:

Explanation:

We execute two actions. The consumer and producer functions describe the state and actions of the consumer and producer respectively. The producer function establishes the mutex lock and verifies if the buffer is full upon invocation. In case the buffer is full, no production will occur. Otherwise, it will generate the product, then proceed to suspend itself to release the mutex lock. Similar to the producer, the consumer initially initializes the mutex lock, inspects the buffer, consumes the item, and subsequently releases the lock before returning to a dormant state.

A counter (x) will increment as the manufacturer produces each item. Conversely, the consumer will produce a reduced quantity of the same item (x).

Conclusion:

The concept of utilizing two or more threads for program execution is referred to as multithreading in the C programming language. Multithreading enables the concurrent running of multiple tasks. A thread represents the most basic executable unit within a program. The concept involves dividing a task into multiple smaller sub-processes to accomplish it.

The inclusion of the pthread.h header file is essential for enabling multithreading functionality in C as it cannot be achieved directly.

Input Required

This code uses input(). Please provide values below: