15, Linux thread synchronization

15, Linux thread synchronization

1, Thread synchronization concept

Thread synchronization is co synchronization, and each thread runs in a predetermined order

Thread synchronization means that when a thread makes a function call, the call will not return until the result is obtained. At the same time, other threads cannot call this function to ensure data consistency

For example: if a and B withdraw 10000 yuan from the same account at the same time in different banks, but the account balance is exactly 10000 yuan, if the processing mechanism of the banking system is not in place and thread synchronization is not set, it may lead to that both a and B get 10000 yuan and the account balance becomes - 10000 yuan, which is the problem of thread insecurity

Reasons for data confusion:

  • Resource sharing (exclusive resources will not appear), such as: all threads jointly operate global variables, all threads compete for a device, etc
  • Scheduling is random (which means there will be competition for data access)
  • Lack of necessary synchronization mechanism between threads

✅ Among the above three points, the first two points cannot be changed (to improve efficiency, data must be transmitted and resources must be shared), but as long as resources are shared, there will be competition. As long as there is competition, data is easy to be confused. Therefore, we can only start from the third point to make multiple threads mutually exclusive when accessing shared resources

For example, an example is given below to demonstrate data chaos

Rand() returns a range from 0 to RAND_MAX (at least 32767), then rand()% 2 can generate 0 or 1

However, since the number generated by rand() is the same every compilation run, the random number seed should be set: srand(). As long as different numbers are passed in, rand() will generate different random numbers every compilation run. Therefore, time(NULL) is passed in, because the time is different

I want the main thread to print: "Thread Test\n" and the sub thread to print: "Hello World\n", but the printing process is interspersed with random time. Because there is no thread synchronization, the final printing result is chaotic

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <pthread.h>
#include <string.h>

void *tfunc(void *arg)
{
    srand(time(NULL)); // Set random number seed

    while(1) // Print "Hello World\n" and insert random time in the sentence
    {
        printf("Hello ");
        sleep(rand() % 2);
        printf("World\n");
        sleep(rand() % 2);
    }
    
    pthread_exit(NULL);
}

int main(int argc, char *argv[])
{
    int ret;
    pthread_t tid;
    
    srand(time(NULL)); // Set random number seed

    ret = pthread_create(&tid, NULL, tfunc, NULL);
    if(ret != 0)
    {
        fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
        exit(1);
    }

    while(1) // Print "Thread Test\n" and insert random time in the sentence
    {
        printf("Thread ");
        sleep(rand() % 2);
        printf("Test\n");
        sleep(rand() % 2);
    }

    pthread_exit(NULL);
}


In the above example, this is the so-called data confusion, that is, when multiple processes compete for stdout resources

2, Mutex / mutex

1. Basic concepts

Linux provides a mutex lock. Each thread tries to lock before operating on resources. Only after locking successfully can it operate shared resources. At the end of the operation, unlock and give up the lock, that is, when a thread is locking shared resources and other threads want to request a lock, they must block and wait for the thread to give up the lock, and other threads can lock and operate shared resources

At this time, resources are still shared and threads compete (threads compete for mutually exclusive locks, threads that grab the lock can operate shared resources, and other threads block waiting for the transfer of the lock). However, through "lock", the access to resources becomes mutually exclusive, and then time-related errors will not occur again

✅ Note: the mutex itself is not mandatory, that is, if A thread does not set A mutex when operating shared resources, it is not restricted by the mutex. For example, if thread A sets A mutex when accessing shared resources, but thread B does not set A mutex when accessing shared resources, then thread B's access is free and not restricted by the mutex, Only A is restricted; Therefore, when threads want to operate shared resources, they should be locked to restrict each other

2. Correlation function

At this time, these functions may not be found in the man manual in Ubuntu because the man Manual of pthread is not installed. You can execute the following command to install:

sudo apt-get install manpages-posix-dev

Include header file:

#include <pthread.h>

int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);

Initialize a mutex;
In addition, you can also initialize the mutex by static initialization: pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

pthread_mutex_t *restrict mutex outgoing parameter to return the initialized mutex
restrict is a keyword used to modify a pointer to indicate the contents of the memory space pointed to by the pointer. The operation can only be completed by this pointer
pthread_mutex_t type, whose essence is a structure, can be ignored in its implementation details and simply treated as an integer
When mutex is regarded as an integer, its variable has only two values: 1 or 0; Call pthread_ mutex_ After init, the initial value of mutex can be regarded as 1; 1 means lockable and 0 means the lock is occupied
const pthread_mutexattr_t *restrict attr sets the properties of the mutex. NULL is passed by default
The return value returns 0 when successful and errno when failed


int pthread_mutex_lock(pthread_mutex_t *mutex);

When the lock is not occupied, add the lock, which can be understood as mutex -- operation;
If lock is already occupied, block waiting for other threads to give up lock

The return value returns 0 when successful and errno when failed


int pthread_mutex_unlock(pthread_mutex_t *mutex);

Unlocking and releasing the lock can be understood as mutex + + operation; Wake up threads blocked on locks

The return value returns 0 when successful and errno when failed


int pthread_mutex_destroy(pthread_mutex_t *mutex);

Destroy the mutex and the resources it occupies

pthread_mutex_t *mutex mutex to destroy
The return value returns 0 when successful and errno when failed

For example, the example of data confusion at the beginning of the article is improved through mutual exclusion

Because it involves shared resources between two processes, it is necessary to define a mutex as a global variable

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <pthread.h>
#include <string.h>

pthread_mutex_t mutex; // Involving shared resources between two processes, the definition uses global variable locks

void *tfunc(void *arg)
{
    srand(time(NULL)); // Set random number seed

    while(1)
    {
        pthread_mutex_lock(&mutex);   // Lock
        printf("Hello ");             // Print "Hello World\n" and insert random time in the sentence
        sleep(rand() % 2);
        printf("World\n");
        pthread_mutex_unlock(&mutex); // Unlock
        sleep(rand() % 2);
    }
    pthread_mutex_destroy(&mutex);    // Destroy lock
    
    pthread_exit(NULL);
}

int main(int argc, char *argv[])
{
    int ret;
    pthread_t tid;
    
    srand(time(NULL));                      // Set random number seed
    ret = pthread_mutex_init(&mutex, NULL); // Initialize mutex
    if(ret != 0)
    {
        fprintf(stderr, "pthread_mutex_init error: %s\n", strerror(ret));
        exit(1);
    }

    ret = pthread_create(&tid, NULL, tfunc, NULL);
    if(ret != 0)
    {
        fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
        exit(1);
    }

    while(1)
    {
        pthread_mutex_lock(&mutex);   // Lock
        printf("Thread ");            // Print "Thread Test\n" and insert random time in the sentence
        sleep(rand() % 2);
        printf("Test\n");
        pthread_mutex_unlock(&mutex); // Unlock
        sleep(rand() % 2);
    }
    pthread_mutex_destroy(&mutex);    // Destroy lock

    pthread_exit(NULL);
}

Obviously, the printed results have been very standardized

✅ Also note: why is the statement pthread in the above code_ mutex_ unlock(&mutex); In the statement sleep (rand()% 2); Above, not below? This is because the smaller the granularity of the lock, the better; That is, to prevent a thread from robbing the lock immediately after unlocking itself, it should [immediately] unlock and give up the lock after operating the shared resources

3. Non blocking locking

int pthread_mutex_trylock(pthread_mutex_t *mutex);

Try to lock. If it succeeds, mutex --. If it fails, set the error number errno directly, such as EBUSY, without blocking

The return value returns 0 when successful and errno when failed

3, Deadlock

Phenomena caused by improper use of locks: (i.e. thread blocking)

  • Pthread is called repeatedly on a lock_ mutex_ lock()
  • Thread 1 has occupied A lock and requested to obtain B lock, while thread 2 has occupied B lock and requested to obtain A lock. However, since thread 1 and thread 2 will not give up their own lock, the two lines will block each other and will not give up resources

4, Read write lock (rwlock)

1. Basic concepts

The read-write lock is similar to the mutex lock, but it allows higher parallelism. Its characteristics are: exclusive (Contention) at write time, shared at read time, and high priority of write lock

🔺 There are two locking modes for read-write locks: (1) lock for read mode and (2) lock for write mode

The logic of read-write lock is shown in the following figure:

2. Correlation function

int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock, const pthread_rwlockattr_t *restrict attr);

Initialize a read-write lock;
Note that the type of lock is pthread_rwlock_t


int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);

Lock in read mode


int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);

Write lock


int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);

Unlock and release the lock


int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);

Destroy the read-write lock and its occupied resources

The above five functions:
Their usage is similar to that of mutex related functions, so they will not be described in detail here
The return value returns 0 when successful and errno when failed

3. Non blocking locking

int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);

Try to lock in read mode. If it succeeds, lock it. Otherwise, an error number will be returned without blocking


int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);

Try to lock in write mode. If it is successful, lock it. Otherwise, an error number is returned without blocking

The return value returns 0 when successful and errno when failed

5, Condition variable

Condition variable itself is not a lock! But it can also cause thread blocking; Therefore, it is usually used in conjunction with mutexes to provide a meeting place for multiple threads

1. Correlation function

int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr);

Initializes a condition variable object

pthread_cond_t *restrict cond outgoing parameter, which returns the condition variable after initialization
const pthread_condattr_t *restrict attr condition variable attribute, NULL by default


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

The blocking waiting condition variable cond is satisfied;
① When the condition variable cond is not satisfied, release the acquired mutex, which is equivalent to pthread_ mutex_ Unlock (& mutex), block and wait for cond to be satisfied;
② When the condition variable cond is satisfied, lock mutex again and unblock pthread at the same time_ cond_ The wait() function returns 0;

pthread_cond_t *restrict cond condition variable
pthread_mutex_t *restrict mutex mutex


int pthread_cond_timedwait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex, const struct timespec *restrict abstime);

The time limited blocking waiting condition variable cond is satisfied, which is a continuation of the previous function

pthread_cond_t *restrict cond condition variable
pthread_mutex_t *restrict mutex mutex
Construct timespec * restrict abstime to set the waiting time (absolute time) (system time + waiting time)
Its structure is as follows:
It is defined in the header file #include < sys / time h> Medium

struct timespec {
	long    tv_sec;         /* seconds second */
	long    tv_nsec;        /* nanoseconds nanosecond */
};

Absolute time: the number of seconds since 00:00:00 on January 1, 1970. For example, time(NULL) is the absolute time
✅ The correct usage of this function is as follows:

time_t cur = time(NULL);					// Get current time
struct timespec t;							// Define timespec struct variable t
t.tv_sec = cur + 2;							// Timing 2 seconds
pthread_cond_timedwait (&cond, &mutex, &t);	// Transmission parameter

int pthread_cond_signal(pthread_cond_t *cond);

Wake up at least one thread blocked on the condition variable, that is, it will satisfy the condition variable;
Generally, only one thread is awakened, but it is possible to awaken multiple threads


pthread_cond_t *cond condition variable being blocked


int pthread_cond_broadcast(pthread_cond_t *cond);

Wake up all threads blocked on the condition variable, that is, it will satisfy the condition variable

pthread_cond_t *cond condition variable being blocked

int pthread_cond_destroy(pthread_cond_t *cond);

Destroy a condition variable and its resources

The above six functions:
The return value returns 0 when successful and errno when failed

Logic diagram of the above functions:

2. Producer consumer conditional variable model


For example: simulate the producer consumer thread. The producer inserts the linked list node through the header insertion method. Each node is regarded as a shared resource data unit, while the consumer reads the linked list node data and prints it on the screen to free up the node space

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <pthread.h>
#include <string.h>

/* Define and use static methods to initialize condition variables and mutexes */
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

struct common_resource // Shared resource structure
{
    int num;                      // data
    struct common_resource *next; // Next node
};

struct common_resource *head; // Chain header (shared resource)

void *producer(void *arg) // Producer thread
{
    struct common_resource *data;

    while(1)
    {
        data = (struct common_resource *)malloc(sizeof(struct common_resource));
        data->num = rand() % 1000 + 1;
        printf("producer: %d -------------\n", data->num);

        pthread_mutex_lock(&mutex); // Lock and operate shared resources
        data->next = head;
        head = data;
        pthread_mutex_unlock(&mutex); // Unlock the shared resource immediately after the operation

        pthread_cond_signal(&cond); // Wake up the consumer thread blocked on the condition variable
        sleep(rand() % 3);
    }

    pthread_exit(NULL);
}

void *consumer(void *arg) // Consumer thread
{
    struct common_resource *data;

    while(1)
    {
        pthread_mutex_lock(&mutex); // Lock
        if(head == NULL) // If the chain header pointer is empty, it indicates that there is no node, then it will be blocked and wait for the producer thread to wake up
        {
            pthread_cond_wait(&cond, &mutex);
        }
        // Operating shared resources
        data = head;
        head = data->next;
        pthread_mutex_unlock(&mutex); // Unlock the shared resource immediately after the operation
        printf("--------------consumer: %d\n", data->num); // Want to print what you read on the screen

        free(data); // Release the linked list node to prevent memory leakage
        sleep(rand() % 2);
    }

    pthread_exit(NULL);
}

int main(int argc, char *argv[])
{
    pthread_t producer_tid, consumer_tid;
    int ret;

    ret = pthread_create(&producer_tid, NULL, producer, NULL); // Create producer thread
    if(ret != 0)
    {
        fprintf(stderr, "pthread_create producer error: %s\n", strerror(ret));
    }

    ret = pthread_create(&consumer_tid, NULL, consumer, NULL); // Create consumer thread
    if(ret != 0)
    {
        fprintf(stderr, "pthread_create producer error: %s\n", strerror(ret));
    }

    pthread_join(producer_tid, NULL); // Blocking wait
    pthread_join(consumer_tid, NULL); // Blocking wait

    pthread_exit(NULL);
}

Because it is a linked list, the final consumption is also orderly

3. Multiple producer and multiple consumer condition variables

✅ In the case of multiple consumer threads, if you directly modify the code in the above example, you should note that when the program starts, the producer has not started to produce data. At this time, head = NULL, multiple consumer threads may block in pthread at the same time_ cond_ wait(&cond, &mutex); Come on. If the producer produces a data at this time, the pthread_cond_signal() will wake up "at least one" (usually one, but possibly multiple) consumer threads blocked on the condition variable. If multiple consumer threads are awakened, these threads will compete for the mutex lock (those who succeed in the competition will get the mutex lock first, and those who fail in the competition will wait for the lock). The consumer threads that succeed in the competition will read the shared data and free() it, Then unlock and release the lock; However, when the lock is released, another thread blocked on the lock can obtain the lock. At this time, the shared resources will be read. However, since the shared resources have been read by the previous thread, a Segmentation fault (core dumped) error will appear

Therefore, if you modify directly from the code in the above example, you should:

if(head == NULL) // If the chain header pointer is empty, it indicates that there is no node, then it will be blocked and wait for the producer thread to wake up
{
	pthread_cond_wait(&cond, &mutex);
}

Amend to read:

while(head == NULL) // If the chain header pointer is empty, it indicates that there is no node, then it will be blocked and wait for the producer thread to wake up
{
	pthread_cond_wait(&cond, &mutex);
}

In this way, the thread can make multiple judgments to avoid repeated data reading

Code diagram: change the single producer and single consumer in the previous example to multiple producers and multiple consumers

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <pthread.h>
#include <string.h>

/* Set the number of multiple producers and consumers */
#define PRODUCER_COUNT 10
#define CONSUMER_COUNT 5

/* Define and use static methods to initialize condition variables and mutexes */
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

struct common_resource // Shared resource structure
{
    int num;                      // data
    struct common_resource *next; // Next node
};

struct common_resource *head; // Chain header (shared resource)

void *producer(void *arg) // Producer thread
{
    struct common_resource *data;

    while(1)
    {
        data = (struct common_resource *)malloc(sizeof(struct common_resource));
        data->num = rand() % 1000 + 1;
        printf("producer: %d -------------\n", data->num);

        pthread_mutex_lock(&mutex); // Lock and operate shared resources
        data->next = head;
        head = data;
        pthread_mutex_unlock(&mutex); // Unlock the shared resource immediately after the operation

        pthread_cond_signal(&cond); // Wake up the consumer thread blocked on the condition variable
        sleep(rand() % 4);
    }

    pthread_exit(NULL);
}

void *consumer(void *arg) // Consumer thread
{
    struct common_resource *data;

    while(1)
    {
        pthread_mutex_lock(&mutex); // Lock
        while(head == NULL) // If the chain header pointer is empty, it indicates that there is no node, then it will be blocked and wait for the producer thread to wake up
        {
            pthread_cond_wait(&cond, &mutex);
        }
        // Operating shared resources
        data = head;
        head = data->next;
        pthread_mutex_unlock(&mutex); // Unlock the shared resource immediately after the operation
        printf("--------------consumer: %d\n", data->num); // Want to print what you read on the screen

        free(data); // Release the linked list node to prevent memory leakage
        sleep(rand() % 2);
    }

    pthread_exit(NULL);
}

int main(int argc, char *argv[])
{
    pthread_t producer_tid[PRODUCER_COUNT]; // Producer thread pool
    pthread_t consumer_tid[CONSUMER_COUNT]; // Consumer thread pool
    int i, ret;

    for(i=0; i<PRODUCER_COUNT; i++) // Create multiple thread pools
    {
        ret = pthread_create(&producer_tid[i], NULL, producer, NULL);
        if(ret != 0)
        {
            fprintf(stderr, "pthread_create producer error: %s\n", strerror(ret));
        }
    }

    for(i=0; i<CONSUMER_COUNT; i++) // Create multiple thread pools
    {
        ret = pthread_create(&consumer_tid[i], NULL, consumer, NULL); // Create consumer thread
        if(ret != 0)
        {
            fprintf(stderr, "pthread_create consumer error: %s\n", strerror(ret));
        }
    }

    for(i=0; i<PRODUCER_COUNT; i++) // Blocking waiting for multiple producer threads
    {
        pthread_join(producer_tid[i], NULL);
    }

    for(i=0; i<CONSUMER_COUNT; i++) // Blocking waiting for multiple consumer threads
    {
        pthread_join(consumer_tid[i], NULL);
    }

    pthread_exit(NULL);
}

6, Semaphore

It is equivalent to initializing the mutex value of N, that is, it is not only 0 or 1, but ≥ 0

Semaphores are not limited to thread synchronization, but can also be used in process synchronization

1. Correlation function:

Include header file:

#include <semaphore.h>

int sem_init(sem_t *sem, int pshared, unsigned int value);

Initialize a semaphore

sem_t *sem outgoing parameter, return initialized semaphore
int pshared if 0 is passed: used for synchronization between threads; If pass 1: used for inter process synchronization
unsigned int value initial value of the semaphore


int sem_wait(sem_t *sem);

Locking;
① When sem > 0, execute sem --;
② When sem = 0, block and wait;

sem_t *sem semaphore


int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);

Time limited blocking wait is a continuation of the previous function

sem_t *sem semaphore
const struct timespec *abs_timeout to set the waiting time (absolute time) (system time + waiting time)
Its structure is as follows:
It is defined in the header file #include < sys / time h> Medium

struct timespec {
	long    tv_sec;         /* seconds second */
	long    tv_nsec;        /* nanoseconds nanosecond */
};

Absolute time: the number of seconds since 00:00:00 on January 1, 1970. For example, time(NULL) is the absolute time
✅ The correct usage of this function is as follows:

time_t cur = time(NULL);	// Get current time
struct timespec t;			// Define timespec struct variable t
t.tv_sec = cur + 2;			// Timing 2 seconds
sem_timedwait (&sem, &t);	// Transmission parameter


int sem_post(sem_t *sem);

Equivalent to executing sem++


sem_t *sem semaphore


int sem_destroy(sem_t *sem);

Destroy semaphores and their occupied resources

The above six functions:
The return value returns 0 when successful and errno when failed

2. Non blocking locking

int sem_trywait(sem_t *sem);

Try to lock. If it succeeds, sem--. If it fails, directly set the error number errno, which is non blocking

3. Producer consumer semaphore model


✅ Where, the cargo grid in the figure is an array through i = (i + 1)% num; i can be taken from 0 to num circularly to form a logical "ring queue" for the prime group; A direction is specified on the ring queue, the producer produces goods along the direction, the consumer takes out the goods along the direction, and the circular access is realized through the ring queue

For example:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <pthread.h>
#include <string.h>
#include <semaphore.h>

#define NUM 5 / / total number of cargo cells
int grid[NUM]; // Simulate the goods grid, and the goods are integer numbers

sem_t product_number, blank_number; // Two semaphores are defined, representing the quantity of goods and the shelf space

void *producer(void *arg) // Producer thread
{
    int i = 0;

    while(1)
    {
        sem_wait(&blank_number);	 // blank_number--
        grid[i] = rand() % 1000 + 1; // Simulated production goods
        sem_post(&product_number);   // product_number++

        printf("producer: %d -------------\n", grid[i]);

        i = (i + 1) % NUM; // Ring queue
        sleep(rand() % 3);
    }

    pthread_exit(NULL);
}

void *consumer(void *arg) // Consumer thread
{
    int i = 0;

    while(1)
    {
        sem_wait(&product_number); // product_number--
        printf("--------------consumer: %d\n", grid[i]); // Remove the goods and print them on the screen
        sem_post(&blank_number);   // blank_number++

        i = (i + 1) % NUM; // Ring queue
        sleep(rand() % 3);
    }

    pthread_exit(NULL);
}

int main(int argc, char *argv[])
{
    pthread_t producer_tid, consumer_tid; // Define producer thread and consumer thread
    int i, ret;

    sem_init(&product_number, 0, 0); // Initialize semaphore product_number, the initial value is 0
    sem_init(&blank_number, 0, NUM); // Initialize semaphore blank_number, the initial value is NUM

    ret = pthread_create(&producer_tid, NULL, producer, NULL); // Create producer thread
    if(ret != 0)
    {
        fprintf(stderr, "pthread_create producer error: %s\n", strerror(ret));
    }

    ret = pthread_create(&consumer_tid, NULL, consumer, NULL); // Create consumer thread
    if(ret != 0)
    {
        fprintf(stderr, "pthread_create consumer error: %s\n", strerror(ret));
    }

    pthread_join(producer_tid, NULL); // Blocking waiting producer threads
    pthread_join(consumer_tid, NULL); // Blocking waiting consumer threads

    pthread_exit(NULL);
}

Keywords: C Linux Embedded system

Added by dlf on Mon, 20 Dec 2021 19:59:19 +0200