操作系统之进程同步

简介: 操作系统之进程同步

四,进程同步🧐

4.1 进程同步和互斥

基本概念

  1. 临界资源
    临界资源是一次只允许一个进程访问的资源,各个进程以互斥的方式实现共享
  2. 临界区
    每个资源访问临界区的那段代码就是临界区,每次都只有一个进程进入临界区,就比如打印机,每次正在使用的人只有一个
    临界资源的访问过程分为4个部分:
    a.进入区。负责检查是否可以进入临界区,设置正在访问临界资源的标志。(相当于上了把锁)
    b.临界区。访问临界资源的那段代码。
    c.退出区。负责解除正在访问临界资源的标志。
    d.剩余区。做其他处理。

同步机制应该遵循的准则

这些准则的目的是为了实现进程互斥的进入自己的临界区

  1. 空闲让进
  2. 忙则等待
  3. 有限等待 (我只等有限的时间,超过这个时间就不等了)
  4. 让权等待 (这个不是必须的) 我不能进入自己的临界区了,就释放掉处理器那些资源,以防进入忙等

信号量机制😥

这是为了解决同步和互斥问题的

信号量机制是一种功能较强的机制,可用来解决互斥和同步问题,它只能被两个标准的原语wait(S)和signal(S)访问,也可记为“P操作”和“V操作”。

1. 整型信号量

整型信号量被定义为一个用于表示资源数目的整型量S。P和V操作可描述为:

wait(S){

while(S<=0); 这里是如果S资源<=0的情况下是一直在等待资源的到来

S=S-1;}

signal(S){

S=S+1}

该机制并未遵循“让权等待”的准则,而是使进程处于“忙等”的状态。

2. 记录型信号量

记录型信号量是不存在“忙等”现象的进程同步机制。除需要一个用于代表资源数目的整型变量value外,再增加一个进程链表L,用于链接所有等待该资源的进程。

typedef struct{
    int value;
  struct process *L}
semaphore;
void wait(semaphore S){s. value - - ;
if(S. value<0){add this process to S.L; //如果资源<0就挂到阻塞队列里面去
    block(S. L);}}
void signal (semaphore *S){
    S. value++;
    if(S.value<=0){  //释放资源的时候如果value<=0那说明队列中有进程还在被阻塞,然后就释放队列中的第一个进程   
  remove a process to S.L;
    wakeup(P);
}}

信号量的应用

1.互斥

相当于一把锁的作用,拿资源的时候上锁,使用完资源释放锁

2.同步

如果进程是先执行v操作的话,s+1变为1,然后可以进行p2执行C2,如果是先执行p操作的话s=-1,会把自己阻塞,只有p1执行v操作后才会释放阻塞

这样保证了c1一定是在c2之前执行的.

总结:

(1)同步信号量,值为资源可以使用的个数,信号量小于0,则线程进行等待,信号量大于0,表示可用资源个数。初始值一般为0.

(2)互斥信号量只有两个值0或1,0表示资源正在被占用,线程等待。1表示,资源没有被使用,线程可以进入。初始值为1

4.2 使用信号量解决同步问题

基本解题步骤

PV操作题目分析的步骤:

①关系分析。找出题目中描述的各个进程,分析它们之间的同步、互斥关系。

②整理思路。根据各进程的操作流程确定操作的大致顺序。

③设置信号量。设置需要的信号量,并根据题目条件确定信号量的初值。

如何实现

互斥的实现是在同一个进程中进行一对PV 操作。

同步的实现是在两个进程中进行,在一个进程中执行P操作,在另一个进程中执行v操作。

生产者消费者问题

需要注意的是互斥操作在一个个进程内成对出现,同步操作在不同进程之间同步出现

为什么这样安排?可能是因为互斥操作必须紧邻“临界区”的缘故。互斥操作要紧邻临界区,才能充分地发挥作用。这里也就是把产品放入缓冲区或者拿出缓冲区的操作(百度就这一个回答,感觉不太对)

读者-写者问题

两个并发进程共享一个文件,当两个或两个以上的读进程共同访问数据时不会产生副作用。但多个写进程同时访问数据可能会导致数据不一样。因此要求:允许多个读者可以同时对文件进行读操作。只允许一个写者往文件里写信息,在任一写者完成操作前不允许其他读者或写者工作,写者执行操作前应该给让所以其余的读者或写者退出。

算法:在上锁前进行判断,第一个读进程负责加锁,最后一个读进程负责解锁,其中还有个互斥信号量保证对读者数量的互斥操作,在这两个过程中要设置互斥变量来保证检查与赋值一气呵成。在写进程的前后设置PV,在读进程前设置PV,保证读写进程有序进行,不会出现饥饿。

哲学家就餐问题

圆桌上两个哲学家中间有一根筷子,每个哲学家只能用他两边的筷子吃饭,一旦缺少一根进程就会被阻塞。

算法:设置信号量数组chopstick[5] = {1,1,1,1,1},每个哲学家i左边的筷子编号为i,右边的筷子编号为(i+1)%5.每个哲学家先对左右两边的筷子进行P操作,在完成进程后再对两边进行V操作。

如果不加条件,如果所有哲学家并发用餐就会出现死锁现象。

可以添加的条件:1.限制最大进餐人生;2,对于奇数号哲学家先拿左边的筷子,对于偶数号的哲学家先拿右边的筷子。(用争抢防止阻塞);3.仅当哲学家左右两只筷子都可用时才允许他抓起筷子

4.3 管程

管程(Monitor)是一种用于实现并发控制的编程机制。它提供了一种结构化的方式来管理共享资源的访问,并确保线程之间的同步和互斥。管程机制最初由荷兰计算机科学家Edsger Dijkstra在1965年提出,并在之后的几十年中得到了广泛应用和研究。

管程的基本思想是将共享资源和对该资源的操作封装在一个单元内部。这个单元包含了用于操作共享资源的一组过程或方法,以及用于同步和互斥的机制。通过使用管程,可以确保只有一个线程能够同时访问共享资源,从而避免了竞态条件和数据不一致性的问题。

管程机制通常包括以下几个关键要素:

  1. 数据结构:管程内部包含一个或多个共享资源,可以是变量、队列、缓冲区等。这些数据结构被管程的过程所使用和操作。
  2. 过程(Procedure):管程内部定义了一组过程或方法,用于对共享资源进行操作。每个过程都是原子性的,一次只能由一个线程执行。通过调用这些过程,线程可以安全地访问和修改共享资源。
  3. 条件变量(Condition Variable):管程中的条件变量用于实现线程之间的等待和通知机制。线程可以通过等待一个条件变量来释放对共享资源的占用,并暂时阻塞。当某个条件满足时,其他线程可以通过通知条件变量来唤醒等待的线程。
  4. 互斥锁(Mutex):管程通常使用互斥锁来实现对共享资源的互斥访问。只有获取了互斥锁的线程才能执行管程内部的过程。其他线程必须等待互斥锁的释放才能执行管程中的操作。

管程机制的优点在于提供了一种高级抽象的并发控制方式,使得程序员能够更方便地编写并发代码。通过将共享资源和操作封装在管程内部,可以减少竞争条件和死锁等并发问题的出现。此外,管程的条件变量和互斥锁提供了线程之间的同步和互斥机制,可以确保线程按照预期的顺序访问共享资源。

需要注意的是,管程并不是一种语言提供的原生机制,而是一种抽象的概念。不同的编程语言和环境提供了不同的实现方式,如Java中的synchronized关键字和wait/notify机

在Java中,可以使用synchronized关键字和wait()/notify()方法来实现管程的功能。下面是一个使用管程的简单示例:

class Monitor {
    private int sharedData;
    
    public synchronized void processData() {
        // 管程过程:对共享资源进行操作
        // 在管程过程中可以使用synchronized关键字确保互斥访问
        // 例如,这里可以修改sharedData的值
        sharedData = 10;
        
        // 唤醒等待的线程
        notify();
    }
    
    public synchronized void waitForData() throws InterruptedException {
        // 管程过程:线程等待共享资源的条件
        // 在管程过程中可以使用synchronized关键字确保互斥访问
        // 例如,这里可以检查sharedData的值是否满足特定条件
        while (sharedData != 10) {
            // 等待共享资源满足条件
            wait();
        }
        
        // 共享资源满足条件,执行后续操作
    }
}

在上述示例中,Monitor类代表了一个简单的管程,其中包含了两个管程过程:processData()waitForData()。这两个过程都使用了synchronized关键字,以确保在同一时间只有一个线程可以进入管程执行过程。

processData()过程中,共享资源sharedData被修改,并通过notify()方法唤醒等待的线程。

waitForData()过程中,线程会等待共享资源满足特定条件。如果共享资源的值不满足条件,线程会调用wait()方法进入等待状态,释放锁并让其他线程执行。一旦共享资源满足条件,其他线程通过notify()方法唤醒等待的线程。

请注意,以上示例仅为演示如何使用synchronizedwait()notify()来实现管程的基本功能。在实际开发中,可能需要更复杂的管程实现,例如使用ReentrantLockCondition等类来实现更灵活的管程机制。

语言基本上本身不提供管程的实现

在其他编程语言中,有些语言提供了原生的管程机制,而有些语言则需要使用特定的库或框架来实现管程功能。以下是一些常见编程语言的管程实现方式:

  1. C/C++:C/C++本身没有原生的管程机制。但是可以使用线程库(如pthread)提供的互斥锁(mutex)和条件变量(condition variable)来实现管程。通过使用互斥锁和条件变量,可以实现对共享资源的互斥访问和线程之间的同步。
  2. Python:Python提供了threading模块和LockCondition等类来支持管程的实现。可以使用Lock类来实现互斥访问,使用Condition类来实现等待和通知机制。
  3. C#:C#提供了Monitor类来实现管程。可以使用Monitor类的方法如Enter()Exit()Wait()Pulse()来实现互斥访问和线程等待和通知。
  4. Rust:Rust通过标准库提供了std::sync::Mutexstd::sync::Condvar等类型,用于实现管程。Mutex用于实现互斥访问,Condvar用于实现线程等待和通知。
  5. Go:Go语言的并发模型本身就提供了原生的管程机制。通过使用关键字gochan,可以实现对共享资源的互斥访问和线程之间的同步。

需要注意的是,具体实现方式可能因编程语言和使用的库或框架而有所不同。某些编程语言可能提供了更高级的管程实现,例如活跃对象(Active Object)模型或消息传递机制。在选择编程语言和实现管程时,可以根据具体需求和语言特性来进行选择。

管程相对于普通方式实现同步和互斥具有以下优点:

  1. 结构化:管程提供了一种结构化的编程方式,将共享资源、操作和同步机制封装在一个单元内部。这样可以更清晰地组织代码,使得并发控制的逻辑更易于理解和维护。
  2. 简化同步:使用管程可以简化同步的实现。管程内部的互斥锁和条件变量可以由编程语言或框架提供,减少了手动实现同步的复杂性。开发者可以专注于共享资源的操作,而无需显式处理底层的同步机制。
  3. 避免竞态条件:管程通过互斥访问共享资源,确保同一时间只有一个线程可以访问。这样可以避免竞态条件的出现,即多个线程同时对共享资源进行修改导致不确定的结果。
  4. 避免死锁:管程内部的同步机制可以帮助避免死锁的发生。通过使用条件变量来等待和通知线程,可以确保线程之间按照预期的顺序进行同步,避免了死锁的可能性。
  5. 提高可扩展性:管程提供了一种高级抽象,可以更容易地实现并发控制。通过将共享资源和相关操作封装在管程内部,不同的线程可以独立地调用管程过程,从而提高了代码的可扩展性和重用性。

总而言之,管程提供了一种结构化、简化和安全的方式来实现并发控制。它可以帮助开发者避免常见的并发问题,提高代码的可读性和可维护性,同时提供了更高层次的抽象,使并发编程更加容易和安全。

相关文章
|
13天前
|
算法 Linux 调度
深入理解Linux操作系统的进程管理
本文旨在探讨Linux操作系统中的进程管理机制,包括进程的创建、执行、调度和终止等环节。通过对Linux内核中相关模块的分析,揭示其高效的进程管理策略,为开发者提供优化程序性能和资源利用率的参考。
37 1
|
22天前
|
算法 调度 Python
深入理解操作系统中的进程调度算法
在操作系统中,进程调度是核心任务之一,它决定了哪个进程将获得CPU的使用权。本文通过浅显易懂的语言和生动的比喻,带领读者了解进程调度算法的重要性及其工作原理,同时提供代码示例帮助理解。
|
16天前
|
调度 开发者 Python
深入浅出操作系统:进程与线程的奥秘
在数字世界的底层,操作系统扮演着不可或缺的角色。它如同一位高效的管家,协调和控制着计算机硬件与软件资源。本文将拨开迷雾,深入探索操作系统中两个核心概念——进程与线程。我们将从它们的诞生谈起,逐步剖析它们的本质、区别以及如何影响我们日常使用的应用程序性能。通过简单的比喻,我们将理解这些看似抽象的概念,并学会如何在编程实践中高效利用进程与线程。准备好跟随我一起,揭开操作系统的神秘面纱,让我们的代码运行得更加流畅吧!
|
15天前
|
C语言 开发者 内存技术
探索操作系统核心:从进程管理到内存分配
本文将深入探讨操作系统的两大核心功能——进程管理和内存分配。通过直观的代码示例,我们将了解如何在操作系统中实现这些基本功能,以及它们如何影响系统性能和稳定性。文章旨在为读者提供一个清晰的操作系统内部工作机制视角,同时强调理解和掌握这些概念对于任何软件开发人员的重要性。
|
14天前
|
Linux 调度 C语言
深入理解操作系统:从进程管理到内存优化
本文旨在为读者提供一次深入浅出的操作系统之旅,从进程管理的基本概念出发,逐步探索到内存管理的高级技巧。我们将通过实际代码示例,揭示操作系统如何高效地调度和优化资源,确保系统稳定运行。无论你是初学者还是有一定基础的开发者,这篇文章都将为你打开一扇了解操作系统深层工作原理的大门。
|
15天前
|
存储 算法 调度
深入理解操作系统:进程调度的奥秘
在数字世界的心脏跳动着的是操作系统,它如同一个无形的指挥官,协调着每一个程序和进程。本文将揭开操作系统中进程调度的神秘面纱,带你领略时间片轮转、优先级调度等策略背后的智慧。从理论到实践,我们将一起探索如何通过代码示例来模拟简单的进程调度,从而更深刻地理解这一核心机制。准备好跟随我的步伐,一起走进操作系统的世界吧!
|
15天前
|
算法 调度 开发者
深入理解操作系统:进程与线程的管理
在数字世界的复杂编织中,操作系统如同一位精明的指挥家,协调着每一个音符的奏响。本篇文章将带领读者穿越操作系统的幕后,探索进程与线程管理的奥秘。从进程的诞生到线程的舞蹈,我们将一起见证这场微观世界的华丽变奏。通过深入浅出的解释和生动的比喻,本文旨在揭示操作系统如何高效地处理多任务,确保系统的稳定性和效率。让我们一起跟随代码的步伐,走进操作系统的内心世界。
|
16天前
|
运维 监控 Linux
Linux操作系统的守护进程与服务管理深度剖析####
本文作为一篇技术性文章,旨在深入探讨Linux操作系统中守护进程与服务管理的机制、工具及实践策略。不同于传统的摘要概述,本文将以“守护进程的生命周期”为核心线索,串联起Linux服务管理的各个方面,从守护进程的定义与特性出发,逐步深入到Systemd的工作原理、服务单元文件编写、服务状态管理以及故障排查技巧,为读者呈现一幅Linux服务管理的全景图。 ####
|
19天前
|
算法 Linux 调度
深入浅出操作系统的进程管理
本文通过浅显易懂的语言,向读者介绍了操作系统中一个核心概念——进程管理。我们将从进程的定义出发,逐步深入到进程的创建、调度、同步以及终止等关键环节,并穿插代码示例来直观展示进程管理的实现。文章旨在帮助初学者构建起对操作系统进程管理机制的初步认识,同时为有一定基础的读者提供温故知新的契机。
|
18天前
|
消息中间件 算法 调度
深入理解操作系统之进程管理
本文旨在通过深入浅出的方式,带领读者探索操作系统中的核心概念——进程管理。我们将从进程的定义和重要性出发,逐步解析进程状态、进程调度、以及进程同步与通信等关键知识点。文章将结合具体代码示例,帮助读者构建起对进程管理机制的全面认识,并在实践中加深理解。