One article clearly explains the principles and usage of C language multithreading, signals, mutexes, and condition variables (including cases)

Introduction

Thread (Thread): A thread is a concurrent execution flow within a program, also known as a lightweight process. Unlike processes, threads share the same memory space, so data and communication can be shared more easily. Threads can be created, destroyed, synchronized and communicated through the pthread library.

Signal (Signal): Signal is an inter-process communication mechanism in UNIX and UNIX-like systems, which is used to notify the process that certain events have occurred.

Mutex (Mutex): A mutex is a synchronization primitive used to protect shared resources and avoid data competition problems caused by multiple threads accessing the same resource at the same time. When accessing a shared resource, a thread needs to obtain a mutex first to prevent other threads from accessing the resource at the same time. Once a thread has finished accessing the resource, the mutex must be released so that other threads can continue to access the resource.

When a thread acquires a lock and enters a critical section, other threads are blocked trying to acquire the same lock. This is the core function of the mutex, which is to ensure that only one thread can enter the critical section at the same time, avoiding data competition and unpredictable results.

Other threads are not completely unable to proceed, but will be blocked in the process of trying to acquire the lock, until the thread that acquires the lock releases the lock and leaves the critical section, other threads have the opportunity to acquire the lock and enter the critical section district. During this time, blocked threads can wait, sleep, or perform other tasks to avoid wasting CPU resources.

The operation between locking and releasing the lock must be as fast as possible, usually a simple assignment operation, etc., to avoid blocking other threads all the time.

Condition Variable (Condition Variable): Usually used with a mutex to allow a thread to wait until a certain condition is met to avoid unnecessary busy waiting. When a thread has changed a shared resource and wants to notify other threads, it can be signaled through a condition variable. Other threads can wait on the condition variable until a signal is received.

Assuming that there is a multi-threaded program that needs to read and write a shared resource, then threads, signals, mutexes, and condition variables can be used to achieve synchronization and communication between threads.

For example, a multi-threaded program needs to fetch data from a shared queue for processing, but if the queue is empty, it needs to wait until there is data in the queue before fetching. At this time, a mutex can be used to protect the read and write operations of the queue to prevent data confusion caused by multiple threads accessing the queue at the same time; use a condition variable to implement the thread waiting and wake-up mechanism, when the queue is empty, call the wait of the condition variable The function suspends the current thread until the queue is not empty and wakes up the thread to take out the operation. At the same time, use signals to handle abnormal conditions, such as when the program needs to end, use signals to notify all threads to exit.

In this way, by using threads, signals, mutexes, and condition variables, the synchronization and communication of multi-threaded programs can be realized, and the performance and concurrent processing capabilities of the programs can be improved.

Program 1: Create a thread

int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);

Among them, thread is a pointer to pthread_t type, which is used to store the ID of the new thread; attr is a pointer to pthread_attr_t type, indicating the attribute of the new thread. If NULL is passed, the default attribute is used; start_routine is the new thread to execute Function, its return value and parameters are void* type; arg is the parameter passed to start_routine, if there is no parameter, NULL can be passed.

After calling the pthread_create function, the system will create a new thread and start executing the start_routine function. The ID of the new thread will be stored in the memory pointed to by thread. The pthread_create function returns 0 if the thread was created successfully, otherwise it returns a non-zero error code.

#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
static void *my_thread_func()
{<!-- -->
    while (1)
    {<!-- -->
        sleep(1);
        /* code */
    }
    
}
int main()
{<!-- -->
    pthread_t tid;
    int ret;
    //Create thread
    ret=pthread_create( &tid,NULL,my_thread_func,NULL);
    //If the creation fails, output error
    if(ret)
    {<!-- -->
        printf("error!\
");
        return -1;
    }
    while (1)
    {<!-- -->
        sleep(1);
        /* code */
    }
    return 0;
}

output:

jing@ubuntu:~/Desktop/test$ gcc -o pthread pthread.c -lpthread
jing@ubuntu:~/Desktop/test$ ./pthread &
[2] 14553
jing@ubuntu:~/Desktop/test$ ps
   PID TTY TIME CMD
 14376 pts/1 00:00:00 bash
 14533 pts/1 00:00:00 pthread
 14553 pts/1 00:00:00 pthread
 14556 pts/1 00:00:00 ps
jing@ubuntu:~/Desktop/test$ ps -T
   PID SPID TTY TIME CMD
 14376 14376 pts/1 00:00:00 bash
 14533 14533 pts/1 00:00:00 pthread
 14533 14534 pts/1 00:00:00 pthread
 14553 14553 pts/1 00:00:00 pthread
 14553 14554 pts/1 00:00:00 pthread
 14557 14557 pts/1 00:00:00 ps

Procedure 2 Signal (sem)

The function you want to achieve: After the program starts to execute, the main thread waits for user input, and after the user input, transfers to the sub-thread to perform printout operations

How to do it: using the signal mechanism

#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <semaphore.h>
static char g_buf[1000];
static sem_t g_sem;
static void *my_thread_func()
{<!-- -->
    while (1)
    {<!-- -->
        sem_wait( & amp;g_sem);//wait for the signal, only when the signal is received, the next step will be executed
        printf("recv:%s\
",g_buf);
        
    }
    
}
int main()
{<!-- -->
    pthread_t tid;
    int ret;
    sem_init( & amp;g_sem,0,0);//Signal initialization
    ret=pthread_create( &tid,NULL,my_thread_func,NULL);
    if(ret)
    {<!-- -->
        printf("error!\
");
        return -1;
    }
    while (1)
    {<!-- -->
        
        fgets(g_buf,1000,stdin);
        //send signal
        sem_post( & g_sem);
    }
    return 0;
}

output

jing@ubuntu:~/Desktop/test$ ./pthread2
12
1recv:12

123
1recv:123

123345
1recv:123345

Problems that arise: Assuming that the user is inputting data and the sub-thread is still executing, the output result may be a mixture of the two, because the buff amount can be written by the user or read by the sub-thread, which will cause conflicts

Procedure three mutex (mutex)

Function

pthread_mutex_lock() is used to try to acquire a mutex. If the lock is currently occupied, it will block and wait until the lock is acquired before returning. The prototype of this function is as follows:

int pthread_mutex_lock(pthread_mutex_t *mutex);

pthread_mutex_unlock() is used to release the mutex. If there are currently threads blocked waiting for the lock, one of the threads will be awakened to continue execution.

int pthread_mutex_unlock(pthread_mutex_t *mutex);

Code

#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <semaphore.h>
#include <string.h>
static char g_buf[1000];
static sem_t g_sem;
static pthread_mutex_t g_tMutex=PTHREAD_MUTEX_INITIALIZER;//Create a lock
static void *my_thread_func()
{<!-- -->
    while (1)
    {<!-- -->
        sem_wait( & g_sem);
        pthread_mutex_lock( & g_tMutex);
        printf("recv:%s\
",g_buf);
        pthread_mutex_unlock( & g_tMutex);
        
    }
    
}
int main()
{<!-- -->
    pthread_t tid;
    int ret;
    char buf[1000];
    sem_init( & g_sem,0,0);
    ret=pthread_create( &tid,NULL,my_thread_func,NULL);
    if(ret)
    {<!-- -->
        printf("error!\
");
        return -1;
    }
    while (1)
    {<!-- -->
        fgets(buf,1000,stdin);
        pthread_mutex_lock( & amp;g_tMutex);//lock
        memcpy(g_buf,buf,1000);
        pthread_mutex_unlock( & amp;g_tMutex);//lock
        sem_post( & g_sem);
    }
    return 0;
}

It should be noted that do not directly lock the things entered by fgets, because most of the execution is in fgets, which may cause the lock to be occupied all the time and cannot be acquired by threads

Program 4 condition variable

Condition variables and mutexes are used at the same time to set up a condition and perform operations on a resource only when the condition is met

pthread_cond_wait

pthread_cond_wait is a thread blocking function, which is used to wait for the trigger of the condition variable. It is usually used in conjunction with a mutex to achieve thread synchronization.

int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);

pthread_cond_wait will automatically release the lock on mutex, and put the calling thread in the condition variable waiting queue to wait for the signal of the condition variable. When another thread calls pthread_cond_signal or pthread_cond_broadcast to notify the condition variable, the pthread_cond_wait function returns and reacquires the mutex lock. At this point, the thread can operate on shared resources.

When using condition variables, the general practice is to put the operation of waiting for the condition variable and the operation of modifying the shared resource into a critical section to ensure the consistency and correctness of the data.

pthread_cond_signal

The pthread_cond_signal() function is used to wake up a thread that is waiting for a specific condition variable, and its function prototype is as follows:

int pthread_cond_signal(pthread_cond_t *cond);

This function notifies a thread waiting for the condition variable cond to return from the blocked state in the pthread_cond_wait() function. If multiple threads are waiting for the condition variable, only one of the threads will be awakened, and the awakened thread will re-compete for the mutex corresponding to the condition variable. If the mutex is successfully acquired, the thread will continue to execute. Otherwise, it will enter the blocked state again. The function returns 0 if a thread is successfully woken up, otherwise it returns a non-zero value.

The benefits of using condition variables to control semaphore locks are as follows:

  1. Reduce CPU resource consumption: When no condition is established, the thread will always be in a waiting state without using CPU resources. This can avoid busy waiting (busy waiting), thereby reducing CPU resource consumption.
  2. Avoid deadlock: Using condition variables can handle the mutual exclusion relationship between multiple threads more safely, avoiding the occurrence of deadlock.
  3. Better control over threads: Using condition variables allows finer control over the execution order and synchronization of threads.
  4. Better handling of complex thread interaction relationships: When there are complex interaction relationships between multiple threads, using condition variables can better handle these relationships, making the code more concise, easy to understand and maintain.

Code

#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <semaphore.h>
#include <string.h>
static char g_buf[1000];
static sem_t g_sem;
static pthread_cond_t g_tConVar=PTHREAD_COND_INITIALIZER;//Condition volume
static pthread_mutex_t g_tMutex=PTHREAD_MUTEX_INITIALIZER;
static void *my_thread_func()
{<!-- -->
    while (1)
    {<!-- -->
        sem_wait( & g_sem);
        pthread_mutex_lock( & g_tMutex);
        pthread_cond_wait( & amp;g_tConVar, & amp;g_tMutex);//Only when the condition is met, the signal lock will be acquired
        
        printf("recv:%s\
",g_buf);
        pthread_mutex_unlock( & g_tMutex);
        
    }
    
}
int main()
{<!-- -->
    pthread_t tid;
    int ret;
    char buf[1000];
    sem_init( & g_sem,0,0);
    ret=pthread_create( &tid,NULL,my_thread_func,NULL);
    if(ret)
    {<!-- -->
        printf("error!\
");
        return -1;
    }
    while (1)
    {<!-- -->
        fgets(buf,1000,stdin);
        pthread_mutex_lock( & g_tMutex);
        memcpy(g_buf,buf,1000);
        pthread_cond_signal( & amp;g_tConVar);//Release signal
        pthread_mutex_unlock( & g_tMutex);
        sem_post( & g_sem);
    }
    return 0;
}

pthread_cond_wait( & amp;g_tConVar, & amp;g_tMutex) will first release the g_tMutex thread lock, and then suspend the current thread until another thread sends a condition notification (via pthread_cond_signal or pthread_cond_broadcast function), the suspended thread will wake up and acquire the g_tMutex thread lock again.

Linux command supplement

./pthread & amp;//Background execution program
ps //View thread
ps -T //View process
kill-9 xxxx//kill thread
top //View the program being executed, the latest one is at the top