[Linux] thread - thread safety

Posted by Jaquio on Fri, 13 Mar 2020 13:03:53 +0100

Thread safety

concept

  • Multiple execution streams scramble for access to critical resources without causing data ambiguity or logical confusion, which is called thread safe.

Implementation method

Thread safety implementation:

  • Synchronization: realize the timing rationality of critical resource access through condition judgment
  • Mutual exclusion: security of access to critical resources through unique access

mutex

  • Implementation technology of mutual exclusion: mutual exclusion lock, semaphore
  • Principle of mutual exclusion: as long as only one execution flow can access resources at the same time, it is mutual exclusion
  • Mark critical resources with status: when no one is visiting, it is marked with 1, indicating that they can be accessed; when someone is visiting, it is marked with 0, indicating that they can't be accessed; before accessing critical resources, state judgment is made to determine whether they can be accessed, and if they can't be accessed, dormancy is made.

mutex

Principle of mutually exclusive lock
  • Mutex: in fact, it is a counter with only 0 / 1 counter, which is used to mark the current access status of resources
    1 ---- accessible
    0 ---- not accessible
  • In order to realize mutual exclusion, each thread must first access the same mutex (lock) before accessing the critical resources, which means that the mutex itself is a critical resource (involving the modification of counters, the modification process must be safe, if you can't even protect yourself, how to protect others?)
    If it is a normal counter, the operation step is to load the value of mutex into a register of CPU, then judge whether it can be accessed, and finally return data. This operation is not atomic.
  • How to realize atomicity for counter operation of mutex:
    Sketch Map:

A simple example, for example, mutex is 1, which means the lock is locked. If mutex is 0, it means the lock is unlocked. At this time, if the CPU wants to acquire the lock, first fill in a register as 1, and then exchange the value of the register and the memory through the xchg instruction (atomic operation). If the value of the register is 0 after the exchange, it means that the CPU acquires the lock and prevents other CPUs from acquiring the lock. If the value of the register is 1 after the swap, it means that the lock has been acquired by other CPUs.

Conclusion:

  1. Mutex is a counter whose counting operation is atomic
  2. Mutex determines whether it can be locked by accessing mutex before accessing critical resources
Mutex operation flow:
  1. Define mutex
pthread_mutex_t mutex;
  1. Initialize mutex
mutex=PTHREAD_MUTEX_INITIALIZER / 
int pthread_mutex_init(pthread_mutex_t *mutex, pthread_mutexattr_t *attr)
mutex: Mutex variable first address
attr: Mutex property, usually set toNULL
  1. Before accessing critical resources, lock (access lock, judge whether it can be accessed) --- the process of protecting access to critical resources
int pthread_mutex_lock(pthread_mutex_t *mutex);// Blocking and locking: if the lock cannot be locked, wait all the time
int pthread_mutex_trylock(pthread_mutex_t *mutex);// Non blocking locking: if it is not possible to lock, an error will be reported immediately; if it is possible to lock, it will be returned after locking
  1. After accessing critical resources, remember to unlock (Mark status as accessible)
int pthread_mutex_unlock(pthread_mutex_t *mutex);
  1. Do not use the lock. Finally, release the resources and destroy the mutex
int pthread_mutex_destroy(pthread_mutex_t *mutex);
Give an example

Take scalpers for example:
Unlocked Code:

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

int g_tickets = 100; // There are 100 tickets in all

void *thr_Scalpers(void *arg)
{
  while(1)
  {
    if(g_tickets > 0)
    {
      usleep(1000);
      printf("I : [%p] got a ticket : %d\n", pthread_self(), g_tickets);
      g_tickets--;
    }
    else 
    {
      pthread_exit(NULL); // Exit after ticket grabbing
    }
  }
  return NULL;
}

int main()
{
  int i = 0;
  pthread_t tid[4]; // 4 threads (cattle)
  for(i = 0; i < 4; ++i)
  {
    int ret = pthread_create(&tid[i], NULL, thr_Scalpers, NULL);
    if(ret != 0)
    {
      printf("thread create error!\n");
      return -1;
    }
  }
  for(i = 0; i < 4; ++i)
  {
    pthread_join(tid[i], NULL);
  }
  return 0;
}

Result:

The observation results show that there is a continuous ticket number of 8, and there is a ticket number of 0, - 1, - 2 that should not appear
Analyze the code to get the reason:
When the number of tickets is 8, scalper A enters the ticket grabbing process, which takes time (usleep(1000)). During this period, other scalpers also enter the ticket grabbing process and see eight tickets, so they all think they have grabbed one ticket 8
The situation of 0, - 1, - 2 ticket numbers is similar
Code after locking:

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

int g_tickets = 100;

// 1. Define the mutex variable, which is also a critical resource and needs to be accessible by all threads
// The mutex variable can be defined as a global variable or a local variable, and then passed to the thread through function parameters
pthread_mutex_t mutex;

void *thr_Scalpers(void *arg)
{
  // Try to avoid locking operations that do not need to be locked, which will affect efficiency, because no one else can operate during locking
  // The more operations you lock, the longer it takes
  while(1)
  {
    pthread_mutex_lock(&mutex); // Locking must be before critical resource access, and only critical area is protected
    if(g_tickets > 0)
    {
      usleep(1000);
      printf("I : [%p] got a ticket : %d\n", pthread_self(), g_tickets);
      g_tickets--;

      // Unlocking is after critical resource access
      pthread_mutex_unlock(&mutex);
      usleep(1000); // Prevent a scalper from grabbing again after grabbing (a thread is allocated to a time slice, which is locked immediately after unlocking)
    }
    else 
    {
      // In any place where it is possible to exit the thread, remember to unlock
      // If the exit is not unlocked, other threads will be stuck if they cannot obtain the lock
      pthread_mutex_unlock(&mutex);
      pthread_exit(NULL);
    }
  }
  return NULL;
}

int main()
{
  int i = 0;
  pthread_t tid[4];
  // 2. Initialize the mutex before creating a thread!
  pthread_mutex_init(&mutex, NULL);

  for(i = 0; i < 4; ++i)
  {
    int ret = pthread_create(&tid[i], NULL, thr_Scalpers, NULL);
    if(ret != 0)
    {
      printf("thread create error!\n");
      return -1;
    }
  }
  for(i = 0; i < 4; ++i)
  {
    pthread_join(tid[i], NULL);
  }
  // 3. Destroy the mutex to release resources
  pthread_mutex_destroy(&mutex);
  return 0;
}

Result:

matters needing attention:

  1. Lock protected area, it's better to only access critical resources -- the more protection, the longer execution time, and the lower efficiency
  2. In any place where it is possible to exit the thread, remember to unlock
  3. Lock initialization must be done before thread creation
  4. The destruction of the lock must be to ensure that no one uses the mutually exclusive lock

deadlock

Deadlock: multiple execution flows scramble for access to lock resources, but they wait for each other due to improper promotion order, resulting in program flow unable to continue

Necessary conditions for Deadlock:
  1. Mutually exclusive condition: a resource can only be used by one execution flow at a time
  2. Request and hold condition: when an execution flow is blocked by a request resource, the obtained resource is held
  3. Non deprivation condition: a resource acquired by an execution flow cannot be forcibly deprived until it is used up
  4. Circular waiting condition: the relationship between several execution flows forms a circular waiting resource with head to tail connection
Deadlock prevention

Pay attention to the necessary conditions of deadlock during coding

Avoid deadlock
  • Deadlock Detection Algorithm
  • Banker Algorithm

synchronization

  • The rationality of thread access to critical resources is realized through condition judgment (when can access resources and when can not access resources; if can not access, the thread will block; if can access, the thread will wake up)
  • Realization technology of synchronization: condition variable, semaphore

Conditional variable

  • The idea of realizing synchronization provides users with two interfaces (one is to let the thread sleep; the other is to wake up the dormant thread) + pcb waiting queue
  • The condition variable only provides the interface of waiting and waking up, but does not provide the function of condition judgment (the condition variable itself does not have the function of judging when to wait and when to wake up), which means that the condition judgment needs to be completed by the user himself
Interface function provided by condition variable
  1. Defining conditional variables
pthread_cond_t cond;
  1. Initialization condition variable
pthread_cond_init(pthread_cond_t *cond, pthread_condattr_t *attr); / 
cond=PTHREAD_COND_INITIALIZER;
  1. A thread waiting interface
pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);// Blocking wait
pthread_cond_timedwait(pthread_cond_t *cond, pthread_mutex_t *mutex, 
         struct timespec *abstime); //Blocking operation to limit the waiting time: waiting for a specified period of time,
                                    //When the time is up, the call will report an error and return - --- ETIMEOUT
  • The condition variable is used with the mutex: the condition variable does not provide the function of condition judgment, which requires the user to judge by himself (usually the condition judgment is the access of a critical resource). Therefore, the access of this critical resource needs to be protected by using the mutex.
  1. An interface to wake up threads
pthread_cond_signal(pthread_cond_t *cond); // Wake up at least one waiting thread
pthread_cond_broadcast(pthread_cond_t *cond); // Wake up all waiting threads
  1. Destroy the release resource if the condition variable is not used
pthread_cond_destroy(pthread_cond_t *cond);
Example

Take customers and chefs for example:

  • Now there's a customer and a chef and a bowl
  • Customer:
  1. Lock with mutex
  2. Judge whether the bowl is empty - full, eat / empty, cond_wait, enter the waiting queue
    Unlock
    Hang up
    Lock (when awakened)
  3. Having dinner
  4. Wake up cook
  5. Unlock
  • Chef:
  1. Lock with mutex
  2. Judge whether the bowl is empty - if it is empty, cook / if it is full, cond_wait and enter the waiting queue
  3. cook
  4. Wake up the customer pthread ABCD signal
  5. Unlock
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <pthread.h>

int bowl = 0; // The initial bowl is 0, indicating that there is no rice

pthread_mutex_t mutex;
pthread_cond_t cond;

void *thr_customer(void *arg)
{
  // This is a customer process
  while(1)
  {
    // 0. lock up
    pthread_mutex_lock(&mutex);
    if(bowl == 0)
    {
      // If there is no rice, you have to wait. Because it has been locked, you need to unlock it before waiting, and lock it after being awakened
      // So pthread ABCD cond ABCD wait sets up three operations: unlocking / suspending / locking
      // Unlocking and suspending is an atomic operation - cannot be interrupted
      // When the customer is unlocked and has not yet suspended the dormancy, the chef will cook and wake up the customer (the actual customer is still dormant)
      pthread_cond_wait(&cond, &mutex);
    }
    printf("good tast!\n");
    bowl = 0; // The meal was eaten up.
    // Wake up the chef and make another bowl
    pthread_cond_signal(&cond);
    // Unlocking operation
    pthread_mutex_unlock(&mutex);
  }
  return NULL;
}

void *thr_cook(void *arg)
{
  // This is a chef process
  while(1)
  {
    // 0. Lock operation because the bowl is to be operated
    pthread_mutex_lock(&mutex);
    if(bowl == 1)
    {
      // Food, waiting
      pthread_cond_wait(&cond, &mutex);
    }
    printf("cook...\n");
    bowl = 1; // Made a bowl of rice
    // Wake up customers
    pthread_cond_signal(&cond);
    // Unlocking operation
    pthread_mutex_unlock(&mutex);
  }
  return NULL;
}

int main()
{
  pthread_t ctid[2];
  int ret;

  pthread_mutex_init(&mutex, NULL);
  pthread_cond_init(&cond, NULL);

  ret = pthread_create(&ctid[0], NULL, thr_customer, NULL);
  if(ret != 0)
  {
    printf("create customer failed!\n");
    return -1;
  }

  ret = pthread_create(&ctid[1], NULL, thr_cook, NULL);
  if(ret != 0)
  {
    printf("create cook failed!\n");
    return -1;
  }

  pthread_join(ctid[0], NULL);
  pthread_join(ctid[1], NULL);

  pthread_mutex_destroy(&mutex);
  pthread_cond_destroy(&cond);
  return 0;
}
problem analysis
  • There's no problem with code and logic when there's only one customer and one chef, but there's chaos if there's more than one customer and more than one chef.

Analysis:

  1. Suppose there are three customers and three chefs. The customers are advanced. Because the bowl is empty, three customers sleep and enter the waiting queue;
  2. Chef A got the bowl, and the other two cooks were waiting on the lock;
  3. Cook A finishes the meal, wakes up the customer to eat and unlocks;
  4. The other two chefs found that there was rice in the bowl and they also entered the waiting line;
  5. Suppose that customer A and B are awakened to eat, at this time, only customer A can eat, and customer B is waiting on the lock;
  6. Customer A wakes up the chef after eating and unlocks; the chef wakes up, but there is not necessarily A time slice. After unlocking, it is possible that customer B grabs the lock. At this time, there is no rice in the bowl, but customer B still eats (has no rice). At this time, the logic is chaotic.

Therefore, it should be a circular judgment to judge whether there is rice in the bowl. In the case of multiple customers, everyone scrambles for the lock. After getting the lock, judge whether there is rice again. If there is rice, you will no longer wait and eat; if there is no rice, you will call pthread_cond_wait to sleep again.

The logic is correct at this time, but there are still problems

In the waiting queue of pcb, there are both customer pcb and chef pcb;
After dinner, the customer will go to the queue to wake up the chef;
But everyone is in the same queue, so it is possible to wake up not the chef, but another customer; the wake-up role is wrong, because the customer does not have a meal to sleep, the program is stuck.

So different roles should wait in different queues.

That is, the number of roles in the program, and the number of condition variables, each waiting in its own queue.

Modified code:

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

int bowl = 0; // The initial bowl is 0, indicating that there is no rice

pthread_mutex_t mutex;
pthread_cond_t consumer_cond;
pthread_cond_t cook_cond;

void *thr_customer(void *arg)
{
  // This is a customer process
  while(1)
  {
    // 0. lock up
    pthread_mutex_lock(&mutex);
    while(bowl == 0)
    {
      // If there is no rice, you have to wait. Because it has been locked, you need to unlock it before waiting, and lock it after being awakened
      // So pthread ABCD cond ABCD wait sets up three operations: unlocking / suspending / locking
      // Unlocking and suspending is an atomic operation - cannot be interrupted
      // When the customer is unlocked and has not yet suspended the dormancy, the chef will cook and wake up the customer (the actual customer is still dormant)
      pthread_cond_wait(&consumer_cond, &mutex);
    }
    printf("good tast!\n");
    bowl = 0; // The meal was eaten up.
    // Wake up the chef and make another bowl
    pthread_cond_signal(&cook_cond);
    // Unlocking operation
    pthread_mutex_unlock(&mutex);
  }
  return NULL;
}

void *thr_cook(void *arg)
{
  // This is a chef process
  while(1)
  {
    // 0. Lock operation because the bowl is to be operated
    pthread_mutex_lock(&mutex);
    while(bowl == 1)
    {
      // Food, waiting
      pthread_cond_wait(&cook_cond, &mutex);
    }
    printf("cook...\n");
    bowl = 1; // Made a bowl of rice
    // Wake up customers
    pthread_cond_signal(&consumer_cond);
    // Unlocking operation
    pthread_mutex_unlock(&mutex);
  }
  return NULL;
}

int main()
{
  pthread_t ctid[2];
  int ret;
  int i = 0;

  pthread_mutex_init(&mutex, NULL);
  pthread_cond_init(&cook_cond, NULL);
  pthread_cond_init(&consumer_cond, NULL);

  for(i = 0; i < 4; ++i)
  {
    ret = pthread_create(&ctid[0], NULL, thr_customer, NULL);
    if(ret != 0)
    {
      printf("create customer failed!\n");
      return -1;
    }
  }


  for(i = 0; i < 4; ++i)
  {
    ret = pthread_create(&ctid[1], NULL, thr_cook, NULL);
    if(ret != 0)
    {
      printf("create cook failed!\n");
      return -1;
    }
  }

  pthread_join(ctid[0], NULL);
  pthread_join(ctid[1], NULL);

  pthread_mutex_destroy(&mutex);
  pthread_cond_destroy(&cook_cond);
  pthread_cond_destroy(&consumer_cond);
  return 0;
}
Be careful
  1. Pthread ﹣ cond ﹣ wait includes three steps (unlocking + sleeping + (after being awakened) to lock. Unlocking and sleeping are connected atomic operations)
  2. User's own condition judgment needs to use while loop judgment
  3. Different roles should wait on different conditional variables (as many roles as there are conditional variables)
Published 28 original articles, won praise 8, visited 1427
Private letter follow