Linux thread synchronization

Posted by br on Sat, 08 Jan 2022 12:20:23 +0100

Linux thread synchronization

1. Mutual exclusion

Ensure that only one thread accesses data at a time.

pthread_mutex_t mut;
//Two initialization methods
mut = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_init(&mut, NULL);
1. Initialization
int pthread_mutex_init (pthread_mutex_t *__mutex,
                              const pthread_mutexattr_t *__mutexattr);
2. Destruction
 int pthread_mutex_destroy (pthread_mutex_t *__mutex)
3. Lock / unlock
extern int pthread_mutex_trylock (pthread_mutex_t *__mutex);

/* Lock a mutex.  */
extern int pthread_mutex_lock (pthread_mutex_t *__mutex);

#ifdef __USE_XOPEN2K
/* Wait until lock becomes available, or specified time passes. */
extern int pthread_mutex_timedlock (pthread_mutex_t *__restrict __mutex,
                                    const struct timespec *__restrict);
#endif

/* Unlock a mutex.  */
extern int pthread_mutex_unlock (pthread_mutex_t *__mutex);
4. Output abcd in sequence

Four locks (a lock, b lock, c lock and D lock) are defined respectively. Initially, all four locks are locked. After the main thread creates a sub thread, unlock lock a, and then unlock lock B and lock lock lock a after thread a outputs. Similarly, unlock lock a and lock d after D outputs, so as to output in a circular and orderly manner.

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

#define N 4
static pthread_mutex_t muts[N];
static int flag = 1;

static int get_next(int i){
    if(i < N - 1)
        return i + 1;
    else
        return 0;
}

static void *pth_fun(void *arg)
{
    char ch = 'a';
    int i = (int)arg;
    char n = ch + i;
    while (flag)
    {
        pthread_mutex_lock(muts + i);
        putchar(n);
        pthread_mutex_unlock(muts + get_next(i));
    }
    pthread_exit((void *)0);
}

int main(int argc, char const *argv[])
{
    int err, i;
    pthread_t tids[N];
    for (i = 0; i < N; i++)
    {
        pthread_mutex_init(muts + i, NULL);
        pthread_mutex_lock(muts + i);
        err = pthread_create(tids + i, NULL, pth_fun, (void *)i);
        if (err)
        {
            fprintf(stderr, "pthread_creare() %s\n", strerror(err));
            exit(err);
        }
    }
    pthread_mutex_unlock(muts);
    sleep(3);
    flag = 0;
    for (i = 0; i < N; i++)
    {
        pthread_join(tids[i], NULL);
        pthread_mutex_destroy(muts + i);
    }
    return 0;
}
5. Use of structure with reference count
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <string.h>

//Structure with number of references
struct foo
{
    int count;
    int id;
    pthread_mutex_t mut;
};

#define N 4
static pthread_t tids[N];
static struct foo *foop;//Global pointer, which can be accessed by each thread

//Allocate memory for data
static struct foo *foo_alloc(int id)
{
    struct foo *fp;
    fp = malloc(sizeof(*fp));
    if (fp == NULL)
    {
        perror("malloc()");
        exit(-1);
    }
    fp->count = 0;// The initial number of references is 0
    fp->id = id;
    pthread_mutex_init(&fp->mut, NULL);
    return fp;
}

//Number of references per reference + 1
static void foo_hold(struct foo *fp)
{
    pthread_mutex_lock(&fp->mut);
    fp->count++;
    pthread_mutex_unlock(&fp->mut);
}

//Number of references on release - 1
static void foo_rele(struct foo *fp)
{
    pthread_mutex_lock(&fp->mut);//Before the count operation, lock it to prevent other threads from modifying the current thread
    if (--fp->count == 0)//When the number of references is 0, the lock is destroyed to free memory
    {
        printf("aaaa\n");
        pthread_mutex_unlock(&fp->mut);
        pthread_mutex_destroy(&fp->mut);
        free(fp);
        fp = NUll;
    }
    else
    {
        pthread_mutex_unlock(&fp->mut);
    }
}

//Thread start function
static void *thr_fun(void *arg)
{
    int i = (int)arg;
    if(foop == NUll)
        pthread_exit((void *)0);
    foo_hold(foop);//quote
    printf("th%d %d,%d\n", i, foop->id, foop->count);
    sleep(2);//The thread sleeps for 2 seconds before releasing
    printf("th%d %d,%d\n", i, foop->id, foop->count);
    foo_rele(foop);//release
    pthread_exit((void *)0);
}

int main(int argc, char const *argv[])
{
    int i, err;
    foop = foo_alloc(10);//Data set to 10
    for (i = 0; i < N; i++)
    {
        //Construction thread
        err = pthread_create(tids + i, NULL, thr_fun, (void *)i);
        if (err)
        {
            fprintf(stderr, "pthread_create() %s\n", strerror(err));
            exit(err);
        }
    }
    for (i = 0; i < N; i++)
    {
        //Reclaim thread resources
        pthread_join(tids[i], NULL);
    }
    return 0;
}
Execution results:
th0 10,1
th1 10,2
th2 10,3
th3 10,4
th2 10,3
th0 10,2
th1 10,1
aaaa
th3 0,0
    It can be seen from the execution results that when a thread references data, the number of references increases in order. When releasing data, although the running order and creation of the thread are inconsistent, the number of references decreases in order,

2. Read write lock

There are three states of read-write lock: lock in read mode, lock in write mode and no lock. Only one thread occupies the read-write lock in write mode at a time, but multiple threads can occupy the read-write lock in read mode at the same time.

/* Initialize read-write lock RWLOCK using attributes ATTR, or use
   the default values if later is NULL.  */
extern int pthread_rwlock_init (pthread_rwlock_t *__restrict __rwlock,
                                const pthread_rwlockattr_t *__restrict);
/* Destroy read-write lock RWLOCK.  */
extern int pthread_rwlock_destroy (pthread_rwlock_t *__rwlock);
/* Acquire read lock for RWLOCK.  */
extern int pthread_rwlock_rdlock (pthread_rwlock_t *__rwlock);
/* Acquire write lock for RWLOCK.  */
extern int pthread_rwlock_wrlock (pthread_rwlock_t *__rwlock);
/* Try to acquire write lock for RWLOCK.  */
extern int pthread_rwlock_trywrlock (pthread_rwlock_t *__rwlock);
/* Unlock RWLOCK.  */
extern int pthread_rwlock_unlock (pthread_rwlock_t *__rwlock);
1. Use of bidirectional linked list under multithreading.

Only one thread can modify the data in the linked list at the same time, but multiple threads can read the data in the linked list at the same time.

The following example simulates a situation of producers and consumers. As a producer, the main thread assigns tasks to different sub threads (writes data to the linked list) according to different thread IDs. Multiple sub threads find their own tasks from the linked list (finds data from the linked list) and consume the tasks (deletes data from the linked list) according to their own thread IDS.

be careful:

  1. After the main thread allocates tasks, it waits for a certain time to cancel the sub thread.

    Call pthread_ When cancel(), the system will not close the canceled thread immediately, but will end the thread at the next system call of the canceled thread. If there is no system call, you can execute pthread in the cancelled thread_ Testancel () checks whether the thread is cancelled. If it is cancelled, the thread will end immediately.

  2. The value returned by thread cancellation is - 1.

  3. System call: it is generally a call that will access hardware resources and execute in the core state of mind.

    Such as file reading and writing.

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

struct job_st
{
    struct job_st *prev;
    struct job_st *next;
    pthread_t tid;
    int id;
};

struct queue_st
{
    struct job_st *head;
    struct job_st *tail;
    pthread_rwlock_t q_lock;
};

//Initialize linked list
static struct queue_st *queue_init()
{
    struct queue_st *ptr;
    int err;
    if ((ptr = malloc(sizeof(*ptr))) == NULL)
    {
        return NULL;
    }
    ptr->head = NULL;
    ptr->tail = NULL;
    err = pthread_rwlock_init(&ptr->q_lock, NULL);
    if (err)
    {
        perror("lock init()");
        free(ptr);
        return NULL;
    }
    return ptr;
}
//Forward insertion
static void queue_insert(struct queue_st *qp, struct job_st *jp)
{
    pthread_rwlock_wrlock(&qp->q_lock);//To modify the linked list, add a write lock
    jp->next = qp->head;//Point the successor of the current node to the head node of the linked list.
    jp->prev = NULL;//The current node is the head node and has no precursor node.
    if (qp->head != NULL)//If the head node of the previous linked list is not empty, set its predecessor node as the current node,
    {
        qp->head->prev = jp;
    }
    else//Otherwise, there is no node in the original linked list, and the tail node of the linked list is also set as the current node
    {
        qp->tail = jp;
        //In this case, there is only one node in the linked list, jp, jp - > next = null, jp - > prev = null
    }
    qp->head = jp;//The chain header node points to the current node
    pthread_rwlock_unlock(&qp->q_lock);
}

//Back insertion
static void queue_append(struct queue_st *qp, struct job_st *jp)
{
    pthread_rwlock_wrlock(&qp->q_lock);//To modify the linked list, add a write lock
    jp->prev = qp->tail;//The precursor of the current node points to the tail node of the linked list.
    jp->next = NULL;
    if (qp->tail != NULL)
    {
        qp->tail->next = jp;//After the tail node, it points to the current node
    }
    else
    {
        qp->head = jp;
    }
    qp->tail = jp;//The tail node points to the current node.
    pthread_rwlock_unlock(&qp->q_lock);
}

static void queue_remove(struct queue_st *qp, struct job_st *jp)
{
    if(jp == NULL){
        return;
    }
    pthread_rwlock_wrlock(&qp->q_lock);//To modify the linked list, add a write lock
    struct job_st *p;
    if (jp == qp->head)//When the current node is the head node
    {
        qp->head = jp->next;
        if (qp->tail == jp)//The current node is also the tail node
        {
            qp->tail = NULL;
        }
        else
        {
            qp->head->prev = jp->prev;
        }
    }
    else if (jp == qp->tail)//When the current node is the tail node
    {
        qp->tail = jp->prev;
        qp->tail->next = jp->next;
    }
    else
    {
        jp->next->prev = jp->prev;
        jp->prev->next = jp->next;
    }
    pthread_rwlock_unlock(&qp->q_lock);
    free(jp);
}

static struct job_st *queue_find(struct queue_st *qp, pthread_t tid)
{
    pthread_rwlock_rdlock(&qp->q_lock);//Find data, do not modify the linked list, add read lock
    struct job_st *jp = NULL;
    for (jp = qp->head; jp != NULL; jp = jp->next)
    {
        if (pthread_equal(tid, jp->tid))
        {
            break;
        }
    }
    pthread_rwlock_unlock(&qp->q_lock);
    return jp;
}

static void queue_destory(struct queue_st *qp)
{
    struct job_st *jp = qp->head, *tmp;
    pthread_rwlock_wrlock(&qp->q_lock);
    while (jp != NULL)
    {
        tmp = jp;
        jp = jp->next;
        free(jp);
    }
    pthread_rwlock_unlock(&qp->q_lock);
    pthread_rwlock_destroy(&qp->q_lock);
    free(qp);
}

static struct queue_st *main_qp;
static void clean_fun(void *arg){
    printf("th%d: cleanup\n", (int)arg);
}

//Consumer thread
static void *thr_fun(void *arg)
{
    int th = (int)arg;
    struct job_st *tmp, *jp;
    pthread_cleanup_push(clean_fun, (void *)th);
    while (1)
    {
        //Check whether the thread is cancelled. If not, the child thread will not be cancelled because there is no system call in the following code
        pthread_testcancel();
        jp = queue_find(main_qp, pthread_self());//Find tasks to consume
        if (jp != NULL)
        {
            tmp = jp;
            printf("th%d: %d\n", th,jp->id);
            jp = tmp->next;
            queue_remove(main_qp, tmp);//Consumption task
        }else{
            sched_yield();//If not found, give up the CPU
        }
    }
    pthread_cleanup_pop(0);
    pthread_exit((void *)0);
}

//Consumer thread (this is a function (routine), but the consumer thread executes the function. Don't confuse it)
int main(int argc, char const *argv[])
{
    int err, i;
    struct job_st *jp;
    pthread_t tids[5];
    main_qp = queue_init();
    for (i = 0; i < 5; i++)
    {
        err = pthread_create(tids + i, NULL, thr_fun, (void *)i);//Create consumer thread
        if (err)
        {
            fprintf(stderr, "thread created %s\n", strerror(err));
            exit(err);
        }
    }
    for (i = 0; i < 10; i++)
    {
        jp = malloc(sizeof(*jp));
        jp->id = i;
        jp->tid = tids[i%5];
        queue_insert(main_qp, jp);//Insert data into linked list (generation task)
    }
    sleep(5);//Cancel the consumer thread in five seconds
    for (i = 0; i < 5; i++)
        pthread_cancel(tids[i]);
    queue_destory(main_qp);
    main_qp = NULL;
    for (i = 0; i < 5; i++)
    {
        err = pthread_join(tids[i], NULL);//Reclaim child thread resources
        if (err)
        {
            fprintf(stderr, "thread join %s\n", strerror(err));
            exit(err);
        }
    }
    return 0;
}
Execution results: 
th3: 3
th3: 8
th1: 6
th1: 1
th2: 7
th2: 2
th0: 5
th0: 0
th4: 9
th4: 4
th3: cleanup
th1: cleanup
th2: cleanup
th4: cleanup
th0: cleanup
    You can see that the corresponding thread consumes its own thread id Matching tasks.

3. Conditional variables

pthread_cond_t cond;
cond = PTHRAD_COND_INITIALIZER;
pthread_cond_init(&cond, NULL);
1. Initialization
int pthread_cond_init (pthread_cond_t *__cond,
                              const pthread_condattr_t *__cond_attr);
2. Destruction
/* Destroy condition variable COND.  */
int pthread_cond_destroy (pthread_cond_t *__cond);
3. Block threads and wait for condition variables
/* Wait for condition variable COND to be signaled or broadcast.
   MUTEX is assumed to be locked before.

   This function is a cancellation point and therefore not marked with
   __THROW.  */
int pthread_cond_wait (pthread_cond_t *__cond,
                              pthread_mutex_t *__mutex);

/* Wait for condition variable COND to be signaled or broadcast until
   ABSTIME.  MUTEX is assumed to be locked before.  ABSTIME is an
   absolute time specification; zero is the beginning of the epoch
   (00:00:00 GMT, January 1, 1970).

   This function is a cancellation point and therefore not marked with
   __THROW.  */
int pthread_cond_timedwait (pthread_cond_t *__cond,pthread_mutex_t *__mutex,
                                   const struct timespec *__abstime);
4. Wake up the blocked thread
//Send a notification to a thread that is not blocked. The one is uncertain, and the other threads continue to block
/* Wake up one thread waiting for condition variable COND.  */
int pthread_cond_signal (pthread_cond_t *__cond);

//Send notifications to all blocked threads, preempt the notified threads to run, and the other threads continue to block
/* Wake up all threads waiting for condition variables COND.  */
int pthread_cond_broadcast (pthread_cond_t *__cond);

In the above two examples of mutex, when the conditions for thread processing are not met, the program is always waiting in a loop, which makes the CPU busy and wastes CPU resources. We can use conditional variables to improve this situation, so that when the thread does not meet the conditions, it is in a blocking state and does not consume CPU. Here are two improved examples.

5. Output abcd in sequence
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>

#define N 4
static pthread_t tids[N];

static pthread_mutex_t lock = PTHREAD_COND_INITIALIZER;
static pthread_cond_t cond = PTHREAD_COND_INITIALIZER;//Introducing conditional variables
static int pos = 0;

static int get_next(int i)
{
    if (i < N - 1)
        return i + 1;
    else
        return 0;
}

static void *pth_fun(void *arg)
{
    int n = (int)arg;
    char buf[] = {'a' + n};
    while (1)
    {
        pthread_mutex_lock(&lock);
        while (!pthread_equal(tids[pos], pthread_self()))//Whether the conditions are met
        {
            pthread_cond_wait(&cond, &lock);//Unsatisfied, blocked, waiting for notification
            /*amount to
            pthread_mutex_unlock(&lock);
            while(&cond);//However, this is not an endless loop, busy, etc., but a blocked thread
            pthread_mutex_lock(&lock);
            */
        }
        //putchar(ch + n);
        write(STDOUT_FILENO, buf, 1);
        pos = get_next(pos);//modify condition
        pthread_mutex_unlock(&lock);
        pthread_cond_broadcast(&cond);//The blocked thread and the thread holding cond will be notified
    }
    pthread_exit((void *)0);
}

int main(int argc, char const *argv[])
{
    int err, i;
    pthread_mutex_lock(&lock);
    for (i = 0; i < N; i++)
    {
        err = pthread_create(tids + i, NULL, pth_fun, (void *)i);
        if (err)
        {
            fprintf(stderr, "pthread_creare() %s\n", strerror(err));
            exit(err);
        }
    }
    pthread_mutex_unlock(&lock);
    pthread_cond_broadcast(&cond);
    alarm(3);//After three seconds, terminate the process
    for (i = 0; i < N; i++)
    {
        pthread_join(tids[i], NULL);
    }
    return 0;
}
6. Use of bidirectional linked list under multithreading
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>

struct job_st
{
    struct job_st *prev;
    struct job_st *next;
    pthread_t tid;
    int id;
};

struct queue_st
{
    struct job_st *head;
    struct job_st *tail;
    pthread_rwlock_t q_lock;
};

//Initialize linked list
static struct queue_st *queue_init()
{
    struct queue_st *ptr;
    int err;
    if ((ptr = malloc(sizeof(*ptr))) == NULL)
    {
        return NULL;
    }
    ptr->head = NULL;
    ptr->tail = NULL;
    err = pthread_rwlock_init(&ptr->q_lock, NULL);
    if (err)
    {
        perror("lock init()");
        free(ptr);
        return NULL;
    }
    return ptr;
}
//Forward insertion
static void queue_insert(struct queue_st *qp, struct job_st *jp)
{
    pthread_rwlock_wrlock(&qp->q_lock); //To modify the linked list, add a write lock
    jp->next = qp->head;                //Point the successor of the current node to the head node of the linked list.
    jp->prev = NULL;                    //The current node is the head node and has no precursor node.
    if (qp->head != NULL)               //If the head node of the previous linked list is not empty, set its predecessor node as the current node,
    {
        qp->head->prev = jp;
    }
    else //Otherwise, there is no node in the original linked list, and the tail node of the linked list is also set as the current node
    {
        qp->tail = jp;
        //In this case, there is only one node in the linked list, jp, jp - > next = null, jp - > prev = null
    }
    qp->head = jp; //The chain header node points to the current node
    pthread_rwlock_unlock(&qp->q_lock);
}

//Back insertion
static void queue_append(struct queue_st *qp, struct job_st *jp)
{
    pthread_rwlock_wrlock(&qp->q_lock); //To modify the linked list, add a write lock
    jp->prev = qp->tail;                //The precursor of the current node points to the tail node of the linked list.
    jp->next = NULL;
    if (qp->tail != NULL)
    {
        qp->tail->next = jp; //After the tail node, it points to the current node
    }
    else
    {
        qp->head = jp;
    }
    qp->tail = jp; //The tail node points to the current node.
    pthread_rwlock_unlock(&qp->q_lock);
}

static void queue_remove(struct queue_st *qp, struct job_st *jp)
{
    if (jp == NULL)
    {
        return;
    }
    pthread_rwlock_wrlock(&qp->q_lock); //To modify the linked list, add a write lock
    struct job_st *p;
    if (jp == qp->head) //When the current node is the head node
    {
        qp->head = jp->next;
        if (qp->tail == jp) //The current node is also the tail node
        {
            qp->tail = NULL;
        }
        else
        {
            qp->head->prev = jp->prev;
        }
    }
    else if (jp == qp->tail) //When the current node is the tail node,
    {
        qp->tail = jp->prev;
        qp->tail->next = jp->next;
    }
    else
    {
        jp->next->prev = jp->prev;
        jp->prev->next = jp->next;
    }
    pthread_rwlock_unlock(&qp->q_lock);
    free(jp);
}

static struct job_st *queue_find(struct queue_st *qp, pthread_t tid)
{
    pthread_rwlock_rdlock(&qp->q_lock); //Find data, do not modify the linked list, add read lock
    struct job_st *jp = NULL;
    for (jp = qp->head; jp != NULL; jp = jp->next)
    {
        if (pthread_equal(tid, jp->tid))
        {
            break;
        }
    }
    pthread_rwlock_unlock(&qp->q_lock);
    return jp;
}

static void queue_destory(struct queue_st *qp)
{
    struct job_st *jp = qp->head, *tmp;
    pthread_rwlock_wrlock(&qp->q_lock);
    while (jp != NULL)
    {
        tmp = jp;
        jp = jp->next;
        free(jp);
    }
    pthread_rwlock_unlock(&qp->q_lock);
    pthread_rwlock_destroy(&qp->q_lock);
    free(qp);
}

static pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
static pthread_mutex_t mut = PTHREAD_MUTEX_INITIALIZER;

struct queue_st *main_qp = NULL;
static void clean_fun(void *arg)
{
    printf("th%d: cleanup\n", (int)arg);
}

static void *thr_fun(void *arg)
{
    int th = (int)arg;
    struct job_st *jp;
    pthread_cleanup_push(clean_fun, (void *)th);
    pid_t pid;
    while (main_qp)
    {
        pthread_mutex_lock(&mut);
        while (main_qp && (jp = queue_find(main_qp, pthread_self())) == NULL)//judge
        {
            pthread_cond_wait(&cond, &mut);//Continue blocking if not satisfied
        }
        pthread_mutex_unlock(&mut);//Unlock first, after notification
        pthread_cond_broadcast(&cond);//Send a notification to all threads. The preempted threads run, and those that are not preempted continue to block
        if (main_qp)
        {
            printf("th%d: %d\n", th, jp->id);
            queue_remove(main_qp, jp);
        }
    }
    pthread_cleanup_pop(1);
    pthread_exit((void *)0);
}

int main(int argc, char const *argv[])
{
    int err, i;
    struct job_st *jp;
    pthread_t tids[5];
    main_qp = queue_init();
    for (i = 0; i < 5; i++)
    {
        err = pthread_create(tids + i, NULL, thr_fun, (void *)i);
        if (err)
        {
            fprintf(stderr, "thread created %s\n", strerror(err));
            exit(err);
        }
    }
    for (i = 0; i < 10; i++)
    {
        jp = malloc(sizeof(*jp));
        jp->id = i;
        jp->tid = tids[i % 5];
        queue_insert(main_qp, jp);
    }
    sleep(2);
    queue_destory(main_qp);
    main_qp = NULL;
    pthread_mutex_unlock(&mut);
    pthread_cond_broadcast(&cond);
    for (i = 0; i < 5; i++)
    {
        err = pthread_join(tids[i], NULL);
        if (err)
        {
            fprintf(stderr, "thread join %s\n", strerror(err));
            exit(err);
        }
    }
    pthread_mutex_destroy(&mut);
    pthread_cond_destroy(&cond);
    return 0;
}
Execution results:
th4: 9
th4: 4
th3: 8
th3: 3
th2: 7
th2: 2
th1: 6
th0: 5
th0: 0
th1: 1
th2: cleanup
th3: cleanup
th4: cleanup
th1: cleanup
th0: cleanup

Topics: C Linux Multithreading