In programming, concurrency through threads can greatly improve application efficiency. Threads enable tasks to execute simultaneously, whether it’s dividing a task among threads for faster processing or serving multiple clients in a client-server client-server architecture. This article delves into the basics of creating simple threads in C and leveraging their capabilities to build concurrent applications."
Include the header file pthread.h
.
#include <pthread.h>
Each thread has an object of type pthread_t
associated with it that tells its ID. The same pthread_t
object cannot be used by multiple threads simultaneously. For multiple threads, an array can be created where each element is an ID for a separate thread:
pthread_t id[2];
A thread is created and starts using the function pthread_create()
. It takes four parameters:
Name | Type | Description |
---|---|---|
ID | pthread_t * |
Reference (or pointer) to the ID of the thread. |
Attributes | pthread_attr_t * |
Used to set the attributes of a thread(e.g., the stack size, scheduling policy, etc.) Passing NULL suffices for most applications. |
Starting routine | void * |
The name of the function that the thread starts to execute. If the function’s return type is void * , then its name is simply written; otherwise, it has to be type-cast to void * . |
Arguments | void * |
This is the argument that the starting routine takes. If it takes multiple arguments, a struct is used. |
The return type of a starting routine and its argument is usually set to
void *
.
pthread_create(&id[0], NULL, printNumber, &arg);
pthread_exit()
is used to exit a thread. This function is usually written at the end of the starting routine. If a value is returned by a thread upon ending, its reference is passed as an argument. Since a thread’s local variables are destroyed when they exit, only references to global or dynamic variables are returned.
// Global variable:
int i = 1;
// Starting routine:
void* foo(void* p){
int i = *(int*) p;
printf("Received value: %i", i);
// Return reference to global variable:
pthread_exit(&i);
}
A parent thread is made to wait for a child thread using pthread_join()
. The two parameters of this function are:
Name | Type | Description |
---|---|---|
Thread ID | pthread_t |
The ID of the thread that the parent thread waits for. |
Reference to return value | void ** |
The value returned by the exiting thread is caught by this pointer. |
int* ptr;
pthread_join(id, &ptr);
Let’s understand it further with the help of the following visuals:
Now, let's explore the coding example in the next section.
Click the "Run" button to execute the following coding widget.
#include <stdio.h>#include <string.h>#include <pthread.h>// Global variable:int i = 2;void* foo(void* p){// Print value received as argument:printf("Value recevied as argument in starting routine: ");printf("%i\n", * (int*)p);// Return reference to global variable:pthread_exit(&i);}int main(void){// Declare variable for thread's ID:pthread_t id;int j = 1;pthread_create(&id, NULL, foo, &j);int* ptr;// Wait for foo() and retrieve value in ptr;pthread_join(id, (void**)&ptr);printf("Value recevied by parent from child: ");printf("%i\n", *ptr);}
Lines 1-3: We defined all the necessary header files for standard input-output operations, string manipulation, and pthread
library for threading functionalities.
Lines 8-15: We defined a thread function, which takes the p
a void
pointer as its parameter, which is being printed. It then exits the thread and returns a reference to the global variable i
using pthread_exit
.
Lines 17-30: We defined the main
function in which we manage the thread's execution. We declare a variable id
to store the thread ID and initialize a local variable j
to 1
. Using pthread_create
, we create a new thread with foo
as the function and j
's address as the argument. We then wait for the thread to finish using pthread_join
and print the value returned by the child thread.
When working with threads in C, there are several common issues that programmers may encounter. These include:
Race Conditions: Concurrent access to shared resources without proper synchronization can result in race conditions, leading to unpredictable behavior.
Deadlocks: Improper handling of resource acquisition and release can lead to deadlocks, where threads are stuck indefinitely waiting for each other.
Resource Management: Managing shared resources such as memory or file handles among multiple threads requires careful coordination to avoid conflicts and corruption.
Synchronization: Coordinating the execution of multiple threads to ensure correct program behavior often involves the use of synchronization primitives like mutexes, semaphores, and condition variables.
Performance Overhead: Creating and managing threads can introduce overhead in terms of memory and CPU usage, impacting the overall performance of the application if not managed efficiently.
Debugging Challenges: Debugging multi-threaded programs can be challenging due to the non-deterministic nature of thread execution and timing-dependent bugs.
Platform Dependency: Thread behavior may vary across different operating systems and platforms, requiring platform-specific considerations for thread programming.
Note: If you want to explore further please give a review to this Educative's Blog: Multithreading and concurrency fundamentals
Here are some real-world examples where threads are commonly used in C programming:
Web Servers: Web servers handle multiple client requests concurrently. Each incoming connection can be managed by a separate thread, allowing the server to serve multiple clients simultaneously. Threads handle tasks such as parsing HTTP requests, processing database queries, and generating dynamic content.
Multimedia Applications: Multimedia applications such as video players and audio processing software often use threads for concurrent tasks. For example, a video player may use one thread for decoding video frames, another for audio playback, and another for user interface updates. Multithreading ensures smooth playback and responsiveness even when performing computationally intensive tasks.
Data Processing and Analysis: Applications that involve large-scale data processing, such as data mining, scientific computing, and simulations, benefit from multithreading to improve performance. Threads can divide the workload into smaller tasks that can be executed concurrently on multiple CPU cores, speeding up computation.
In conclusion, mastering the creation of simple threads in C unlocks the power of concurrency, boosting program efficiency and enabling parallel task execution. With this foundational knowledge, developers can delve into advanced parallel programming techniques to tackle complex computational tasks effectively.
Free Resources