One article to understand the monitor in the operating system

Directory

Why introduce the pipeline

Definition and basic characteristics of monitor

1. Definition of monitor

2. The composition of the tube process

3. The basic characteristics of the tube process

Solve the producer-consumer problem with monitors


Why should we introduce monitor

Monitor is a synchronization mechanism in the operating system, which is introduced to solve the concurrency control problem in a multi-thread or multi-process environment.

In traditional operating systems, when multiple processes or threads access shared resources at the same time, problems such as data inconsistency, race conditions, and deadlocks may occur. In order to avoid these problems, a synchronization mechanism needs to be introduced to coordinate concurrent access.

The monitor provides a high-level synchronization primitive, which encapsulates shared resources and operations on resources in a unit, and provides an access control mechanism for this unit.

Compared with the semaphore mechanism, it is easier to write programs with monitors, and it is easier to write code.

Definition and basic characteristics of a monitor

1. Definition of monitor

“A monitor is a mechanism for enforcing mutual exclusion (or equivalent operation) of concurrent threads on a set of shared variables. Additionally, a monitor provides the ability to wait for threads to meet certain conditions mechanism and notify other threads that the condition has been met.”

This definition describes the two main functions of a monitor:

  1. Mutually exclusive access: The monitor ensures that multiple threads have exclusive access to shared variables, that is, only one thread can access shared resources at a time to avoid race conditions and data inconsistencies.
  2. Conditional waiting and notification: The monitor provides a mechanism for waiting for a thread to meet a specific condition. A thread can wait for a condition to be met through a condition variable before continuing to execute, or notify other threads that a certain condition has been met through a condition variable.

The monitor can be understood as a room in which there are some shared resources, such as variables, queues, and so on. At the same time, there is a door in the room with only one key. When multiple threads or processes need to access the resources in the room, they need to obtain the key first. Only one thread or process can hold the key at a time, enter the room and access the resources. Other threads or processes must wait until the thread or process currently holding the key releases the key to obtain the key to enter the room.

In addition, the monitor also provides condition variables, similar to the prompts in the room. After a thread enters the room, if it finds that a certain condition is not met (for example, the queue is empty), it can use the condition variable to know that it needs to wait, temporarily leave the room, and hand over the key to the next waiting thread. When other threads meet the waiting conditions (such as adding elements to the queue), it can tell the waiting thread through the condition variable notification, so that it can regain the key to enter the room and continue to execute.

2. The composition of the monitor

The monitor consists of the following main parts:

  1. Shared variables: The monitor contains shared variables or data structures, and multiple threads or processes need to access and modify these shared resources through the monitor.

  2. Mutex: Mutex is a key component in the monitor, which is used to ensure that only one thread or process can enter the monitor at a time. Once a thread or process enters the monitor, other threads or processes must wait until the current thread or process exits the monitor.

  3. Condition Variables (Condition Variables): Condition variables are used to implement the waiting and notification mechanism between threads or processes. When a thread or process needs to wait for a certain condition to be met (such as the state of a shared resource), it can enter the waiting state through the condition variable. When other threads or processes meet this condition, they can send a signal through the condition variable to wake up the waiting thread or process.

  4. Monitor interface (functions that operate on the monitor): the monitor also includes a set of interfaces or methods for operating shared resources. These interfaces define operations on shared resources, and the internal implementation includes management logic for mutexes and condition variables. Other threads or processes access shared resources by calling these interfaces, thus ensuring orderly access to shared resources.

For example:

#include <iostream>
#include <mutex>
#include <condition_variable>

class Monitor {
private:
    int count; // shared variable
    std::mutex mtx; // mutex
    std::condition_variable cond; // condition variable

public:
    Monitor() : count(0) {}

    void enter() {
        std::unique_lock<std::mutex> lock(mtx);
    }

    void exit() {
        mtx. unlock();
    }

    void wait() {
        count + + ;
        cond. wait(lock);
        count--;
    }

    void notify() {
        if (count > 0) {
            cond. notify_one();
        }
    }

    void notifyAll() {
        if (count > 0) {
            cond. notify_all();
        }
    }
};

3. The basic features of the tube process

The basic characteristics of a monitor include:

  1. Mutual Exclusion: The monitor provides a mechanism for mutually exclusive access to shared resources. Only one thread or process is allowed to enter the monitor and perform operations at the same time to avoid data competition and conflicts.

  2. Encapsulation: The monitor encapsulates shared resources and resource operations, and provides a set of abstract interfaces or methods to the outside, so that other threads or processes can only access and modify shared resources through these interfaces.

  3. Condition Wait (Condition Wait): The monitor provides a condition variable that allows a thread or process to wait when a certain condition is not met, and wake up to continue execution when the condition is met. Conditional waiting can avoid busy waiting and improve the efficiency of the system.

  4. Condition Signal (Condition Signal): The monitor allows a thread or process to issue a notification when a certain condition changes, and wake up the waiting thread or process to continue execution. Conditional notification enables efficient cooperation and synchronization between threads or processes.

  5. Blocking: When a thread or process tries to enter a monitor, if the monitor is already occupied by another thread or process, it will be blocked until the monitor becomes available. Likewise, when a thread or process waits for a certain condition to be met, if the condition is not met, it will also be blocked until the condition is met.

  6. Fairness: Monitors typically provide fairness guarantees that threads or processes gain access to the monitor in the order in which they wait. This can prevent certain threads or processes from being preempted by other threads or processes all the time, resulting in starvation.

These features make the monitor a powerful concurrent programming mechanism, which simplifies the process of writing and debugging concurrent programs, and provides a good way of cooperation between threads or processes.

Using monitors to solve producer-consumer problems

example:

#include <iostream>
#include <queue>
#include <mutex>
#include <condition_variable>
#include <thread>

class Monitor {
private:
    std::queue<int> buffer; // shared buffer
    int maxSize; // the maximum capacity of the buffer
    std::mutex mtx; // mutex
    std::condition_variable bufferFull; // buffer full condition variable
    std::condition_variable bufferEmpty; // buffer empty condition variable

public:
    Monitor(int size) : maxSize(size) {}

    void produce(int item) {
        std::unique_lock<std::mutex> lock(mtx);
        while (buffer. size() == maxSize) {
            bufferFull.wait(lock); // The buffer is full, the producer is waiting
        }
        buffer. push(item);
        std::cout << "Produced item: " << item << std::endl;
        bufferEmpty.notify_one(); // notify one consumer
    }

    int consume() {
        std::unique_lock<std::mutex> lock(mtx);
        while (buffer.empty()) {
            bufferEmpty.wait(lock); // buffer is empty, consumer waits
        }
        int item = buffer. front();
        buffer. pop();
        std::cout << "Consumed item: " << item << std::endl;
        bufferFull.notify_one(); // notify one producer
        return item;
    }
};

Monitor monitor(5); // buffer size is 5

void producer() {
    for (int i = 1; i <= 10; + + i) {
        monitor. produce(i);
        std::this_thread::sleep_for(std::chrono::milliseconds(500)); // production interval 500ms
    }
}

void consumer() {
    for (int i = 1; i <= 10; + + i) {
        int item = monitor. consume();
        std::this_thread::sleep_for(std::chrono::milliseconds(800)); // consumption interval 800ms
    }
}

int main() {
    std::thread producerThread(producer);
    std::thread consumerThread(consumer);

    producerThread. join();
    consumerThread. join();

    return 0;
}

In this example, we create a Monitor class, which contains a shared buffer buffer, the maximum capacity of the buffer maxSize, mutual Reject lock mtx and two condition variables bufferFull and bufferEmpty.

The producer produces an item by calling monitor.produce(item) and puts it in the buffer. If the buffer is full, the producer thread waits until a consumer consumes an item and notifies the producer.

Consumers consume an item from the buffer by calling monitor.consume(). If the buffer is empty, the consumer thread waits until a producer produces an item and notifies the consumer.

In the main function, we create a producer thread and a consumer thread and let them run concurrently. The producer produces 10 items and the consumer consumes 10 items. There is a time interval between each producing and consuming operation so that alternate executions of producers and consumers are observed.

By using monitors, we ensure synchronization and mutual exclusion between producers and consumers, avoid buffer race conditions, and achieve a correct and safe solution to the producer-consumer problem.