并发与同步、信号量与管程、生产者消费者问题

本文深入探讨并发与同步,分析了临界区的多种实现,包括禁用硬件中断、软件同步和原子操作原语。接着介绍了信号量和管程的概念,通过具体的P、V操作解释了它们的工作原理。进一步,文章通过生产者消费者问题展示了信号量和管程的实现,最后比较了两者之间的差异,并总结了并发编程中可能遇到的问题及解决方案。
摘要由CSDN通过智能技术生成

正文

   计算机硬件发展到今天,不管是专业服务器还是PC,甚至于最普遍的移动设备基本上都是多核CPU,程序的并发执行可以更加充分利用这些计算资源。除此之后,为了协调CPU与外设(如磁盘)的速度差异,我们也需要并发。本文是笔者学习清华大学和UCSD(加州大学圣迭戈分校)的操作系统课程的笔记和总结,以及自己的思考和实践。

并发与同步:

回到顶部
    并发不是多核时代的产物,在早期的多核CPU已经通过时分复用来实现程序之间的并发。并发带来的好处很多,比如资源共享、提高程序执行效率、模块化等等。但并发对编程带来了很多挑战,比如互斥、死锁。
    所谓同步互斥,就是在并发的情况下,保证一些操作的原子性。 原子操作(Atomic Operation)即在一次执行过程中要么全部成功,要么全部失败的操作,不存在部分执行的情况。原子性在各种应用场景都非常重要,比如数据库的ACID,其中A就是原子性。生活中,也要很多原子性的例子,比如银行卡之间转账,分成两个操作:A扣钱,B加钱,但这两个操作必须满足原子性,不然就出大问题了。
    我们常见意义上的并发一般是指多进程之间或者多线程之间。当然,近些年越来越流行的协程也算一种并发。对于非操作系统内核开发人员,需要同步互斥的场景大多数都是线程之间的并发。更好的资源共享是线程的特性之一,下图(来自 UCSD:Principles of Operating Systems )所示是同一个进程内三个线程的内存使用情况:
    
    对于每一个进程内的多个线程,static data segment(包括全局变量、static对象)、Heap(堆,malloc和new分配的空间)是共享的。每个线程有自己独立的Stack(栈),存储局部变量。
    后文主要以多线程为例,但同样适用于多进程。

临界区

    提到并发编程,首先就想到 临界区(critical section)这个概念,临界区是线程中访问临界资源的一段需要互斥执行的代码。临界资源是指线程之间共享的资源,但不同的执行序列结果不确定的,这也叫做 竞态条件(race condition)。举个例子,我们都知道linux的系统调用fork会创建一个子进程,该调用在父进程会返回子进程的PID,操作系统中每个进程的PID必须是独一无二的。假设pid的生成是这样的:
new_pid = next_pid++
    上面的代码中next_pid就是临界资源,多个进程可能同时访问这个资源, 但上述代码不是原子的(在汇编下是几条指令),但是需要互斥执行的,因此需要临界区。当然,临界区只是一个概念,具体怎么实现依赖于系统与编程语言。在编码中使用临界区的伪代码如下:
entry section        
     critical section 
exit section         
    分为三部分:
    entry section: 判断能否进入,如果能则设置标志,不能则等待
    critical section:需要互斥访问的代码段
    exit section: 清除设置的标志,使得其他进程(线程)可以进入临界区
    临界区的实现有几种方式:
  • 禁用硬件中断:

  我们知道,系统调用以及执行流程的切换都是依靠软中断。禁用中断之后,进程(线程)就不会被切换出去,从而保证代码段能执行结束。但坏处也很明显,由于中断被禁用,如果临界区代码一直执行,其他进程就没机会执行了。而且,只能禁止单个CPU的中断。

  • 基于软件同步:

  即基于代码实现同步互斥,比较有名的是peterson算法,用来解决两个进程对临界区的互斥访问问题。

  • 基于原子操作原语的方法:

  上述两种方式都比较复杂,我们需要更加高级的武器。支持并发的语言都提供了锁(lock)这个概念,在现实生活中也很好理解,如果只能一个人在屋子里,那么进去之后就锁上,出来的时候再打开锁;没有锁的人只能在外面等着。在编程语言中,大概是这样样子的:

acquire(lock)
  critical section 
release(lock)
  acquire,release实现的也就是entry section和 exit section的功能,上面的代码是面向过程的写法,面向对象一般写成lock.acquire和lock.release,或者使用RALL(Resource Acquisition Is Initialization)来避免release函数未被调用到的问题。
    lock的实现需要基于硬件提供的“ 原子操作指令”,这些操作虽然理解起来是几步操作,但硬件保证其原子性。比较常见的原子操作指令包括  test and setcompare and swap,接下来通过test and set来看看lock的实现。

Spinlock实现

  test-and-set是一个原子操作,其作用对某个变量赋值为1(set),并返回变量之前的值,下面用C语言描述这个过程
复制代码
1     #define LOCKED 1
2     int TestAndSet(int* lockPtr) {
3         int oldValue;
4          
5         oldValue = *lockPtr;
6         *lockPtr = LOCKED;
7 
8         return oldValue;
9     }
复制代码

 

    对应的acqurie和release的伪码如下:
复制代码

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值