What are the mechanisms, differences and meanings of mutually exclusive access to Linux multithreaded global variables?

Keywords: less Linux Programming

Write before:
Why write this article, I began to understand these three quantities very hard, gnawed for a long time "Modern Operating System", also read a lot of blogs, and finally a little experience.This article is based on that brick book and some blogs, with a personal summary and understanding, and I hope it will be helpful for your little partners.(mostly in case I forget *-*)
What is mutually exclusive access?
In short, when two processes access a shared memory area, when A enters the memory area, B is not allowed or not, because this causes unnecessary confusion.So how to prevent this "grab into memory" phenomenon, we need to be mutually exclusive.

Mechanism 1 mutex:
We know that a simple solution to interprocess communication is to use a mutex lock, that is, a lock that is tested when process A is about to enter the critical zone. If a lock of 0 means it can enter, then A enters the critical zone and changes the lock to 1, telling other processes that they cannot enter and have to wait.When A ends, it exits the critical zone and locks to 0, indicating that it is accessible, then B can enter the critical zone "safely".

Busy waiting:
What is busy waiting? Simply speaking, it means that when two processes operate in the shared area, when process A enters the critical area, we can not let process B enter the critical area because of competition. We must wait until process A exits before entering.But B knows when A exits, so the most straightforward way is to keep asking if A leaves the critical zone.

In this way, the B process is busy waiting, also known as spin lock, which is obviously very CPU intensive, so we need to avoid it.So it's easy to think of blocking B so that it doesn't poll anymore and consumes CPU resources, so suspend it until A comes out and signal B into the critical zone.That's what we'll say next (sleep and wakeup system calls)

Mechanism 2 semaphore:
The semaphore itself is not one of the mechanisms of POSIX IPC, but rather a tool for synchronizing mutually exclusive processes, similar to mutually exclusive locks, but much more complex.For shared storage, both semaphores and mutexes can be used for mutex and synchronization. When we set the semaphore to 1, we can treat it as a de facto mutex.

  • Semaphores are designed to solve synchronization problems

  • Mutex is used to solve mutex problem

    The semaphore is an integer. For convenience, we count the semaphore as S. There are two primitives of semaphore, P and V. It is important to remember that both P and V operations are atomic operations. How to implement P and V operations is a consideration of the operating system, and hardware/CPU instruction set support is also required. This is described in detail below.

P Operation: S minus 1;
If S minus 1 is still greater than or equal to zero, the process continues to execute.
If S minus 1 is less than zero, the process is blocked, queued for the signal, and then scheduled

V Operation: S plus 1;
If the additive result is greater than zero, the process continues to execute.
If the additive result is less than or equal to zero, wake up a waiting process from the waiting queue of the signal, and then return to the original process to continue execution or process scheduling
It is important that semaphore operations only be performed by the kernel

Example: When the maximum semaphore value is 1

If there are three threads/processes A, B, C that all want to perform the P operation, and because the P operation is atomic, then ABC performs the P (and no process performs the V operation), then the semaphore S will be -2, and both subsequent sites that perform the P operation will be joined to the sleep process queue.
When the P operation is successful and no pending process performs the V operation, S becomes -1. At this time, the V operation also needs to go to sleep queue to wake up a process. As to which process to wake up, it depends on how the operating system processes are scheduled.
Then repeat the second step above until S equals 1
Look carefully at the steps above. Isn't this a lock?

From this example, you may be able to understand this sentence more easily:

When S is greater than or equal to zero, the number of resource entities available to concurrent processes is represented, and when S is less than zero, the number of processes waiting to use the critical zone.

This example is actually what many people have said. When the maximum value of a semaphore is 1 (or when the counting power of a semaphore is not required), this simplified semaphore is called mutex.Because of the lack of technical capabilities, mutex has only two states of 0/1.It is important to note that this semaphore is only one way to implement mutex, and conceptually there is no direct relationship between them (perhaps from the beginning mutex evolved from the semaphore, but then the mutex was pulled into a single concept), there are many ways to implement mutex (because mutex is very simple).

Mechanism 3 Conditional Variables

There are no concepts. Conditional variables are a programming interface provided by linux. Others like to call conditional variables conditional locks. In fact, they are all one thing.A conditional variable actually solves a problem: one process waits for a variable to be established, and another thread modifies it, which must avoid race, so a mutex is often required so that both wait and modification are in the same mutex critical zone.This may be misleading for many people. We'll list the interfaces provided by Linux directly. We'll just look at the two most critical interface functions

int pthread_cond_wait(pthread_cond_t *cond,pthread_mutex_t *mutex); hibernates the current process, releases the mutex, and retrieves the mutex when awakened
int pthread_cond_signal(pthread_cond_t *cond); wakes up the process waiting for the corresponding conditional variable

Conditional variables are often used with mutexes, and this pattern causes a thread to lock a mutex and then wait for a conditional variable when it fails to get the result it expects.Finally, it is signaled by another thread that it can continue to run.
Give a consumer-producer code based on conditional variables and mutexes

//This uses pthread's conditional variables and mutexes to perfect the producer-consumer problem
#include "pthread.h"
#include "stdio.h"
#include "stdlib.h"
#include "unistd.h"

#define NLOOP 20
#define BUFFSIZE 10 //Define buffer size 20

struct buf_t
{
    int     b_buf[BUFFSIZE];
    int     b_nitems;
    int     b_nextget;
    int     b_nextput;
    pthread_mutex_t  b_mutex; //mutex
    pthread_cond_t   b_cond_consumer;//Conditional variables for consumers
    pthread_cond_t   b_cond_producer;//Producer's Conditional Variables
}buf_t;

 void       *produce_loop(void *);
 void       *consume_loop(void *);

 int main(int argc, char **argv)
 {
    int				n;
    pthread_t		tidA, tidB;

    printf("main, addr(n_in_stack) = 0x%x, addr(buf_t) = 0x%x, addr(produce_loop) = 0x%x\n", &n, &buf_t, &produce_loop);
    
    pthread_create(&tidA, NULL, &produce_loop, NULL);
    pthread_create(&tidB, NULL, &consume_loop, NULL);

    /* wait for both threads to terminate */
    pthread_join(tidA, NULL);
    pthread_join(tidB, NULL);
    exit(0);
}
void produce(struct buf_t *bptr, int val)       /* Thread function defining producer */
{
    pthread_mutex_lock(&bptr->b_mutex);         /* down Lock buffer, mutex mutex */
    /* wait if buffer is fu11 */
    while(bptr->b_nitems >= BUFFSIZE)
        pthread_cond_wait(&bptr->b_cond_producer, &bptr->b_mutex);      /* Waiting for the conditional variable to arrive because the buffer is full */
    
    printf ("produce %d\n", val);
    bptr->b_buf[bptr->b_nextput] = val;
    if (++bptr->b_nextput >= BUFFSIZE)
        bptr->b_nextput = 0;
    bptr->b_nitems++;

    /*Signal consumer*/
    pthread_cond_signal(&bptr->b_cond_consumer);    /* Buffer full, send a signal to the consumer to tell him to come to the buffer to fetch data */
    pthread_mutex_unlock(&bptr->b_mutex);           /* up Release buffer, mutex mutex */
}
int consume(struct buf_t *bptr)                 /* Define threading functions for consumers */
{
    int         val;

    pthread_mutex_lock(&bptr->b_mutex);         /* down Lock buffer, mutex mutex */
    while (bptr->b_nitems <= 0)
        pthread_cond_wait(&bptr->b_cond_consumer, &bptr->b_mutex);      /* Waiting for the conditional variable to arrive because the buffer is empty */

    val = bptr->b_buf[bptr->b_nextget];
    printf("consume %d\n", val);
    if (++bptr->b_nextget >= BUFFSIZE)
        bptr->b_nextget = 0;
    bptr->b_nitems--;

    pthread_cond_signal(&bptr->b_cond_producer);    /* Buffer empty, signal producer to buffer production data */
    pthread_mutex_unlock(&bptr->b_mutex);           /* up Release buffer, mutex mutex */
    return(val);
}

void * produce_loop(void *vptr)
{
    int i;
    printf("produce_loop thread, addr(stack) = %x\n", &i);
    for(i = 0; i < NLOOP; i++){
        produce(&buf_t, i);
}
    return (NULL);
}

void * consume_loop(void *vptr)
{
    int         i, val;
    printf("consume_loop thread, addr(stack) = %x\n", &i);
    for(i = 0; i < NLOOP; i++){
        val = consume(&buf_t);
}
    return (NULL);
}
//Not my original, I just added notes and reprints, thanks to my instructor

Posted by SsirhC on Wed, 24 Jun 2020 18:49:52 -0700