一文搞懂操作系统中的管程

目录

为什么要引入管程

管程的定义和基本特征

1.管程的定义

2.管程的组成

3.管程的基本特征

用管程解决生产者消费者问题


为什么要引入管程

管程(Monitor)是一种操作系统中的同步机制,它的引入是为了解决多线程或多进程环境下的并发控制问题。

在传统的操作系统中,当多个进程或线程同时访问共享资源时,可能会导致数据的不一致性、竞态条件和死锁等问题。为了避免这些问题,需要引入一种同步机制来协调并发访问。

管程提供了一种高级的同步原语,它将共享资源和对资源的操作封装在一个单元中,并提供了对这个单元的访问控制机制。

相比于信号量机制,用管程编写程序更加简单,写代码更加轻松。


管程的定义和基本特征

1.管程的定义

"管程是一种机制,用于强制并发线程对一组共享变量的互斥访问(或等效操作)。此外,管程还提供了等待线程满足特定条件的机制,并通知其他线程该条件已满足的方法。"

这个定义描述了管程的两个主要功能:

  1. 互斥访问:管程确保多个线程对共享变量的访问互斥,即同一时间只有一个线程可以访问共享资源,以避免竞态条件和数据不一致性问题。
  2. 条件等待和通知:管程提供了等待线程满足特定条件的机制,线程可以通过条件变量等待某个条件满足后再继续执行,或者通过条件变量通知其他线程某个条件已经满足。

可以将管程理解为一个房间,这个房间里有一些共享的资源,比如变量、队列等。同时,房间里有一个门,只有一把钥匙。多个线程或进程需要访问房间内的资源时,它们需要先获得这把钥匙,一次只能有一个线程或进程持有钥匙,进入房间并访问资源。其他线程或进程必须等待,直到当前持有钥匙的线程或进程释放钥匙,才能获得钥匙进入房间。

此外,管程还提供了条件变量,类似于房间内的提示牌。线程在进入房间后,如果发现某个条件不满足(比如队列为空),它可以通过条件变量来知道自己需要等待,暂时离开房间,并将钥匙交给下一个等待的线程。当其他线程满足了等待的条件(比如向队列中添加了元素),它可以通过条件变量通知告诉正在等待的线程,使其重新获得钥匙进入房间,并继续执行。

 

2.管程的组成

管程由以下几个主要部分组成:

  1. 共享变量:管程中包含了共享的变量或数据结构,多个线程或进程需要通过管程来访问和修改这些共享资源。

  2. 互斥锁(Mutex):互斥锁是管程中的一个关键组成部分,用于确保在同一时间只有一个线程或进程可以进入管程。一旦一个线程或进程进入管程,其他线程或进程必须等待,直到当前线程或进程退出管程。

  3. 条件变量(Condition Variables):条件变量用于实现线程或进程之间的等待和通知机制。当一个线程或进程需要等待某个条件满足时(比如某个共享资源的状态),它可以通过条件变量进入等待状态。当其他线程或进程满足了这个条件时,它们可以通过条件变量发送信号来唤醒等待的线程或进程。

  4. 管程接口(对管程进行操作的函数):管程还包括了一组操作共享资源的接口或方法。这些接口定义了对共享资源的操作,并且在内部实现中包含了互斥锁和条件变量的管理逻辑。其他线程或进程通过调用这些接口来访问共享资源,从而确保了对共享资源的有序访问。

例如:

#include 
#include 
#include 

class Monitor {
private:
    int count;                      // 共享变量
    std::mutex mtx;                 // 互斥锁
    std::condition_variable cond;   // 条件变量

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

    void enter() {
        std::unique_lock 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.管程的基本特征

管程的基本特征包括:

  1. 互斥性(Mutual Exclusion):管程提供了互斥访问共享资源的机制,同一时间只允许一个线程或进程进入管程并执行操作,以避免数据竞争和冲突。

  2. 封装性(Encapsulation):管程将共享资源和对资源的操作封装在一起,对外部提供了一组抽象的接口或方法,使得其他线程或进程只能通过这些接口来访问和修改共享资源。

  3. 条件等待(Condition Wait):管程提供了条件变量,允许线程或进程在某个条件不满足时等待,并在条件满足时被唤醒继续执行。条件等待能够避免忙等待,提高系统的效率。

  4. 条件通知(Condition Signal):管程允许线程或进程在某个条件发生变化时发出通知,唤醒等待的线程或进程继续执行。条件通知使得线程或进程之间能够有效地进行协作和同步。

  5. 可阻塞性(Blocking):当一个线程或进程尝试进入管程时,如果管程已经被其他线程或进程占用,它将被阻塞,直到管程可用。同样,当一个线程或进程等待某个条件满足时,如果条件不满足,它也会被阻塞,直到条件满足。

  6. 公平性(Fairness)管程通常会提供公平性保证,即线程或进程按照它们等待的顺序获得对管程的访问权限。这样可以避免某些线程或进程一直被其他线程或进程抢占,导致饥饿现象。

这些特征使得管程成为一种强大的并发编程机制,可以简化并发程序的编写和调试过程,并提供了良好的线程或进程间的协作方式。


用管程解决生产者消费者问题

例子:

#include 
#include 
#include 
#include 
#include 

class Monitor {
private:
    std::queue buffer;               // 共享的缓冲区
    int maxSize;                          // 缓冲区的最大容量
    std::mutex mtx;                       // 互斥锁
    std::condition_variable bufferFull;   // 缓冲区满的条件变量
    std::condition_variable bufferEmpty;  // 缓冲区空的条件变量

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

    void produce(int item) {
        std::unique_lock lock(mtx);
        while (buffer.size() == maxSize) {
            bufferFull.wait(lock);  // 缓冲区满,生产者等待
        }
        buffer.push(item);
        std::cout << "Produced item: " << item << std::endl;
        bufferEmpty.notify_one();   // 通知一个消费者
    }

    int consume() {
        std::unique_lock lock(mtx);
        while (buffer.empty()) {
            bufferEmpty.wait(lock);  // 缓冲区空,消费者等待
        }
        int item = buffer.front();
        buffer.pop();
        std::cout << "Consumed item: " << item << std::endl;
        bufferFull.notify_one();    // 通知一个生产者
        return item;
    }
};

Monitor monitor(5);  // 缓冲区大小为5

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

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

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

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

    return 0;
}

这个示例中,我们创建了一个Monitor类,它包含了一个共享的缓冲区buffer、缓冲区的最大容量maxSize、互斥锁mtx和两个条件变量bufferFullbufferEmpty

生产者通过调用monitor.produce(item)来生产一个物品,并将其放入缓冲区中。如果缓冲区已满,则生产者线程会等待,直到有消费者消费一个物品并通知生产者。

消费者通过调用monitor.consume()来从缓冲区中消费一个物品。如果缓冲区为空,则消费者线程会等待,直到有生产者生产一个物品并通知消费者。

在主函数中,我们创建了一个生产者线程和一个消费者线程,并让它们并发运行。生产者生产10个物品,消费者消费10个物品。每个生产和消费操作之间有一定的时间间隔,以便观察到生产者和消费者的交替执行。

通过使用管程,我们保证了生产者和消费者之间的同步和互斥访问,避免了缓冲区的竞争条件,实现了正确而安全的生产者-消费者问题的解决方案。


你可能感兴趣的:(操作系统学习笔记,开发语言)