Operating system process thread (3) - process state, synchronization mutex, lock, deadlock

atomic operation

The concept of atomic operations

An atomic operation is one or a series of operations that cannot be interrupted.

How atomic operations are implemented

bus lock

Use a LOCK# signal provided by the processor. When a processor outputs this signal on the bus, the requests of other processors will be blocked, so the processor can monopolize the memory.

cache lock

The overhead of the bus lock is relatively high, because the communication between the CPU and the memory is locked, which makes other processors unable to operate data at other memory addresses during the lock.

Frequently used memory will be cached in the processor's L1, L2, and L3 caches, then atomic operations can be performed directly in the processor's internal cache, and the "cache locking" method can be used in Pentium 6 and current processors To achieve complex atomicity.

The so-called cache lock refers to the fact that when a CPU changes the cached data, it will notify the CPU that cached the data to discard the cached data or re-read it from the memory. (As a whole, the cache coherence mechanism is to notify other CPUs to give up the caches stored in them or re-read them from the main memory when a certain CPU operates on the data in the cache.)。

  • There are two cases where cache locks cannot be used: the first case is that the data of the operation cannot be cached inside the processor, or when the data of the operation spans multiple cache lines (cache lines), the processor will call the bus lock; the second The second case is that the processor does not support cache locking. For Intel 486 and Pentium processors, bus locking is invoked even if the locked memory region is in the processor's cache line.

Synchronization mechanism under Linux

  • POSIX semaphore: can be used for process synchronization and thread synchronization
  • POSIX mutex + condition variable: can only be used for thread synchronization.

Four methods of process synchronization

critical section

Access to critical resources.

Synchronization and Mutex

  • Synchronization: Multiple processes have a direct constraint relationship due to cooperation, so that the processes have a certain sequence of execution.
  • Mutual exclusion: Only one of multiple processes can enter the critical section at the same time.

amount of signal

The semaphore represents the number of resources, and the corresponding variable is an integer (sem) variable. In addition, there are two atomic operation system call functions to control the semaphore, namely:

  • P operation: set sem-1, if sem<0, the thread/process is blocked, otherwise continue.
  • V operation: add 1 to sem, if sem<=0, wake up a waiting process/thread, indicating that V operation will not block.
    Image source Kobayashi coding
    insert image description here

Using semaphores to solve the producer-consumer problem

After the producer generates data, it is placed in a buffer, and the consumer reads the data from the buffer for data processing. At any time, only one producer or consumer can access the buffer.

So three semaphores are needed:

  • Mutual exclusion semaphore mutex: mutual exclusion access buffer
  • Resource semaphore full: used by consumers to ask whether there is data in the buffer, and read the data if there is data. The initialization value is 0, indicating that the buffer is empty at the beginning
  • Resource semaphore empty: used by the producer to ask whether there is space in the buffer, and generate data if there is space, and the initialization value is the size of the buffer
#define N 100
typedef int semaphore;
semaphore mutex = 1;
semaphore empty = N;
semaphore full = 0;

void producer() {
    
    
   while(TRUE) {
    
    
       int item = produce_item();
       down(&empty);
       down(&mutex);
       insert_item(item);
       up(&mutex);
       up(&full);
   }
}

void consumer() {
    
    
   while(TRUE) {
    
    
       down(&full);
       down(&mutex);
       int item = remove_item();
       consume_item(item);
       up(&mutex);
       up(&empty);
   }
}

You can't down(mutex) first and then down(empty), otherwise the producer will wait if empty=0, but at this time, the consumer can't operate on empty, so he will wait forever.

Monitor

The monitor separates the control code, which is not only less prone to errors, but also makes it easier to call the client code.
Only one process can use the monitor at a time. When a process cannot continue to execute, it cannot occupy the monitor all the time, otherwise other processes will never be able to use the monitor.

classic synchronization problem

philosophers dine

Option One

#define N 5

void philosopher(int i) {
    
    
    while(TRUE) {
    
    
        think();
        take(i);       // 拿起左边的筷子
        take((i+1)%N); // 拿起右边的筷子
        eat();
        put(i);
        put((i+1)%N);
    }
}

Problem: If all philosophers pick up the left-handed chopsticks at the same time, they will wait for the right-handed chopsticks together, resulting in a deadlock

Option II

#define N 5
semaphore mutex;
void philosopher(int i) {
    
    
    while(TRUE) {
    
    
        think();
        P(mutex);
        take(i);       // 拿起左边的筷子
        take((i+1)%N); // 拿起右边的筷子
        eat();
        put(i);
        put((i+1)%N);
        V(mutex);
    }
}

Problem: The deadlock problem can be solved, but there is only one philosopher for each meal, which is not the best solution in terms of efficiency

third solution

Even-numbered philosophers take the left fork first and then the right fork, and odd-numbered philosophers take the right fork first and then the left fork.

#define N 5
semaphore mutex;
void philosopher(int i) {
    
    
    while(TRUE) {
    
    
        think();
		if(i%2==0){
    
    
		  take(i);       // 拿起左边的筷子
          take((i+1)%N); // 拿起右边的筷子
		}
		else if(i%2!=0){
    
    
		  take((i+1)%N);       // 拿起左边的筷子
          take(i); // 拿起右边的筷子
		}
      
        eat();
        put(i);
        put((i+1)%N);
      
    }
}

Option four

Use an array state to record the state of each philosopher, which are eating state, thinking state, and hungry state (trying to take a fork). Then,
a philosopher can enter the eating state only when two neighbors have not eaten. .
Code source Axiu's study notes

#define N 5
#define LEFT (i + N - 1) % N // 左邻居
#define RIGHT (i + 1) % N    // 右邻居
#define THINKING 0
#define HUNGRY   1
#define EATING   2
typedef int semaphore;
int state[N];                // 跟踪每个哲学家的状态
semaphore mutex = 1;         // 临界区的互斥,临界区是 state 数组,对其修改需要互斥
semaphore s[N];              // 每个哲学家一个信号量

void philosopher(int i) {
    
    
    while(TRUE) {
    
    
        think(i);
        take_two(i);
        eat(i);
        put_two(i);
    }
}

void take_two(int i) {
    
    
    down(&mutex);
    state[i] = HUNGRY;
    check(i);
    up(&mutex);
    down(&s[i]); // 只有收到通知之后才可以开始吃,否则会一直等下去
}

void put_two(i) {
    
    
    down(&mutex);
    state[i] = THINKING;
    check(LEFT); // 尝试通知左右邻居,自己吃完了,你们可以开始吃了
    check(RIGHT);
    up(&mutex);
}

void eat(int i) {
    
    
    down(&mutex);
    state[i] = EATING;
    up(&mutex);
}

// 检查两个邻居是否都没有用餐,如果是的话,就 up(&s[i]),使得 down(&s[i]) 能够得到通知并继续执行
void check(i) {
    
             
    if(state[i] == HUNGRY && state[LEFT] != EATING && state[RIGHT] !=EATING) {
    
    
        state[i] = EATING;
        up(&s[i]);
    }
}

reader writer problem

Code source Axiu's study notes

typedef int semaphore;
semaphore count_mutex = 1;
semaphore data_mutex = 1;
int count = 0;

void reader() {
    
    
    while(TRUE) {
    
    
        down(&count_mutex);
        count++;
        if(count == 1) down(&data_mutex); // 第一个读者需要对数据进行加锁,防止写进程访问
        up(&count_mutex);
        read();
        down(&count_mutex);
        count--;
        if(count == 0) up(&data_mutex);//最后一个读者要对数据进行解锁,防止写进程无法访问
        up(&count_mutex);
    }
}

void writer() {
    
    
    while(TRUE) {
    
    
        down(&data_mutex);
        write();
        up(&data_mutex);
    }
}

Lock

read-write lock

Read-write locks allow multiple threads to read shared resources at the same time, but only one thread is allowed to write to shared resources. Read-write locks are divided into two types: shared locks (read locks) and exclusive locks (write locks). Write locks take precedence over read locks.

shared lock

Allows multiple threads to simultaneously acquire locks and read shared resources, but prevents other threads from acquiring exclusive locks. In a read-write lock, if a thread has acquired a shared lock, other threads can also acquire a shared lock, but other threads are not allowed to acquire an exclusive lock.

exclusive lock

Only one thread is allowed to acquire the lock and modify the shared resource, and other threads must wait for the current thread to release the lock before acquiring the write lock. In a read-write lock, if a thread has acquired an exclusive lock, other threads cannot acquire any locks and must wait for the current thread to release the lock.

scenes to be used

It is generally applicable to situations where read operations are frequent and write operations are few, but read-write locks may be starved when write operations are frequent.

example
#include <iostream>
#include <thread>
#include <mutex>
#include <shared_mutex>
#include <chrono>

using namespace std;

shared_mutex rw_lock;   // 读写锁
int shared_data = 0;    // 共享数据

// 读取共享数据的线程函数
void read_thread(int id)
{
    
    
    while (true) {
    
    
        // 获取共享锁
        shared_lock<shared_mutex> lock(rw_lock);

        // 读取共享数据
        cout << "thread " << id << " read shared data: " << shared_data << endl;

        // 释放共享锁
        lock.unlock();

        // 等待一段时间
        this_thread::sleep_for(chrono::milliseconds(100));
    }
}

// 修改共享数据的线程函数
void write_thread()
{
    
    
    while (true) {
    
    
        // 获取独占锁
        unique_lock<shared_mutex> lock(rw_lock);

        // 修改共享数据
        shared_data++;

        // 输出修改后的共享数据
        cout << "write thread write shared data: " << shared_data << endl;

        // 释放独占锁
        lock.unlock();

        // 等待一段时间
        this_thread::sleep_for(chrono::milliseconds(500));
    }
}

int main()
{
    
    
    // 创建多个读取共享数据的线程
    thread t1(read_thread, 1);
    thread t2(read_thread, 2);
    thread t3(read_thread, 3);

    // 创建一个修改共享数据的线程
    thread t4(write_thread);

    // 等待所有线程结束
    t1.join();
    t2.join();
    t3.join();
    t4.join();

    return 0;
}

mutex

Only one thread can own a mutex at a time. The mutex is to actively give up the CPU to enter the water surface state when the lock grab fails, and then wake up when the state of the lock changes.
The lock needs to be directly handed over to the operating system for management, so the mutex involves context switching when locking.
The mutex locking time is about 100ns. In fact, a possible implementation isSpin for a period of time first, and put the thread to sleep after the spin time exceeds the threshold, so the effect of using mutexes in concurrent operations may be no less than that of using spinlocks.

condition variable

A mutex has only two states:locking and non-locking. Condition variables make up for the lack of mutexes by allowing threads to block and wait for another thread to send a signal . They are usually used together with mutexes to avoid race conditions.
When the condition is not met, the thread often unlocks the corresponding mutex and blocks the thread and waits for the condition to change. Once some other thread changes the condition variable, it will notify the corresponding condition variable to wake up one or more threads that are interacting with each other. The thread blocked by the condition variable.In general, mutex is a mutual exclusion mechanism between threads, and condition variable is a synchronization mechanism

spin lock

If the process cannot acquire the lock, it will not give up the CPU immediately, but will keep trying to acquire the lock in a loop until it is acquired.Spin locks are generally used in scenarios where the shackle time is very short, and the efficiency is relatively high.

  • The spin lock keeps occupying the CPU. If the lock is not acquired for a short time, it will keep spinning, reducing CPU efficiency.
  • Deadlock possible with recursive calls
  • If it is a single CPU and cannot be preempted, the spin lock is a no-op
#include <iostream>
#include <thread>
#include <mutex>
#include <atomic>

// 互斥锁实现
std::mutex mtx;

void increment_mutex(int &x) {
    
    
    for (int i = 0; i < 1000000; ++i) {
    
    
        mtx.lock();
        ++x;
        mtx.unlock();
    }
}

// 自旋锁实现
std::atomic_flag spin_lock = ATOMIC_FLAG_INIT;

void increment_spin(int &x) {
    
    
    for (int i = 0; i < 1000000; ++i) {
    
    
        while (spin_lock.test_and_set(std::memory_order_acquire)); // 自旋等待
        ++x;
        spin_lock.clear(std::memory_order_release); // 释放自旋锁
    }
}

int main() {
    
    
    int x = 0;

    // 使用互斥锁的线程
    std::thread t1(increment_mutex, std::ref(x));
    std::thread t2(increment_mutex, std::ref(x));

    t1.join();
    t2.join();

    std::cout << "互斥锁实现的x的值: " << x << std::endl;//2000000

    x = 0;

    // 使用自旋锁的线程
    std::thread t3(increment_spin, std::ref(x));
    std::thread t4(increment_spin, std::ref(x));

    t3.join();
    t4.join();

    std::cout << "自旋锁实现的x的值: " << x << std::endl;//2000000

    return 0;
}

deadlock

what is deadlock

Two or more threads are waiting for each other's data. Deadlock will cause the program to freeze. If it is not unlocked, it will never proceed.

cause

  • mutually exclusive
  • No deprivation: Before the resources obtained by the process are released, they cannot be snatched by other processes and can only be released by themselves
  • Request and hold: When the resources currently owned by the process request other resources, the process continues to occupy them
  • Circular waiting: There is a process resource circular waiting chain, and the resource obtained by each process in the chain is simultaneously requested by the next process in the chain.

example

#include <iostream>
#include <thread>
#include <mutex>

using namespace std;

mutex mtx1, mtx2;

void ThreadA()
{
    
    
    mtx1.lock(); // 线程A获取mtx1
    cout << "Thread A obtained mutex 1" << endl;

    // 在获取mtx2之前,先暂停一会儿,让线程B有机会获取mtx2
    this_thread::sleep_for(chrono::milliseconds(100));

    mtx2.lock(); // 尝试获取mtx2,但是已经被线程B获取了,因此线程A将被阻塞

    cout << "Thread A obtained mutex 2" << endl;

    // 完成任务后,释放互斥锁
    mtx2.unlock();
    mtx1.unlock();
}

void ThreadB()
{
    
    
    mtx2.lock(); // 线程B获取mtx2
    cout << "Thread B obtained mutex 2" << endl;

    // 在获取mtx1之前,先暂停一会儿,让线程A有机会获取mtx1
    this_thread::sleep_for(chrono::milliseconds(100));

    mtx1.lock(); // 尝试获取mtx1,但是已经被线程A获取了,因此线程B将被阻塞

    cout << "Thread B obtained mutex 1" << endl;

    // 完成任务后,释放互斥锁
    mtx1.unlock();
    mtx2.unlock();
}

int main()
{
    
    
    // 创建两个线程,分别运行ThreadA和ThreadB函数
    thread t1(ThreadA);
    thread t2(ThreadB);

    // 等待两个线程执行完毕
    t1.join();
    t2.join();

    return 0;
}

In this example, both thread A and thread B are trying to acquire mutexes mtx1 and mtx2. However, when thread A acquires mtx1, it sleeps for a while, giving thread B a chance to acquire mtx2. When thread B acquires mtx2, it will also sleep for a while, giving thread A a chance to acquire mtx1. Since thread A and thread B both need the mutex, they are both blocked, resulting in a deadlock.

deadlock handling method

ostrich strategy

pretend nothing happened

Deadlock detection and deadlock recovery

Take measures to recover when a deadlock is detected. Deadlock detection using resource allocation graphs, including one resource of each type and multiple resources of each type.

deadlock recovery

  • seize
  • rollback
  • kill process

deadlock prevention

  • break the mutex condition
  • Destroy requests and hold conditions
  • break the non-deprivation condition
  • break loop wait

deadlock avoidance

Avoid deadlocks while the program is running

security status

It is safe if no deadlocks occur and there is still some order in which each process runs to completion even if all processes suddenly have the greatest demand for the requested resource.

banker's algorithm

Axiu's study notes

Guess you like

Origin blog.csdn.net/qaaaaaaz/article/details/130578583