12.互斥量(mutex)
当信号量(Semaphore)的计数功能不再需要,信号量简化之后就成为一种新的变量互斥量(mutex)。互斥量在处理共享资源和代码之间的互斥访问方面非常有用。互斥量实现起来简单高效,这一点对于用户空间的线程库非常有用。
互斥是那种只有两种状态,但每次只能处在其中一种状态的变量。这两种状态分别是锁定和非锁定状态。因此,只需要一个比特就能表示互斥量的两种状态,例如,0代表非锁定和1表示锁定。互斥量有两个相关的操作:mutex_lock和mutex_unlock。
mutex_lock:当线程需要进入临界区时,它调用mutex_lock操作,若当前mutex未锁定,则mutex_lock调用成功,若mutex已经锁定,则调用线程会阻塞,直到当前获得mutex锁的那个线程离开临界区,使得mutex解锁。
mutex_unlock:当线程离开临界区,需要解除对mutex的锁定,这时候线程调用mutex_unlock。mutex很简单,因此在用户空间可以很简单的通过TSL和XCHG指令实现,其原理的代码如下:
这个代码和前面讲到的enter_region的代码非常类似,但是却有一个很关键的区别:在enter_region中,如果进程暂时不能进入临界区,那么他一直测试条件,看能否进入,直到时间片用完为止。
而在用户线程中,根本没有时钟中断来使得运行过长的线程停下来,结果就是一个使用上述的忙等待方式企图获取锁的线程会时钟循环,因为别的获得锁的线程根本没机会运行。
而在mutex_lock中则不同,如果当前mutex锁定,他就阻塞,让调度机调度其他的线程运行,从而更加充分的利用CPU资源。
由于thread_yield仅仅是在用户空间对线程调度器的调用,因此,不需要内核支持,这样实现起来是很简单的。
除了mutex_lock和mutex_unlock之外,可能还需要其他的一些特性,例如mutex_trylock,该调用会尝试获得锁,但是如果不成功就以失败状态返回,然后该干嘛干嘛,至少并不阻塞。
由于线程之间共享一个内存空间,所以对于多个线程来说,共享mutex不是什么难事。但是之前提到的Peterson方案、信号量方案等主要是对于进程来说的。他们之间并不共享一份内存空间。那怎么保证多个进程之间共享变量,例如之前提到的turn呢?至少有这么几种办法:
- 将共享数据类型,例如信号量,存储在内核,然后只允许通过系统调用来访问
- 大部分现代操作系统(Windows和Unix)都支持使多个进程共享一部分内存空间
- 实在不行多个进程还能共享文件吧
如果说多个进程共享大部分内存空间,那么进程和线程的区别就不是那么的明显,但还是存在的。比如说,两个进程还是有自己单独的打开的文件等独有的属性,但这些属性对于多个线程来说是共享的。
13.Pthread库中的互斥量
Pthread库中同步的方法的原理还是互斥量。如果一个线程需要进入临界区,那么它需要尝试锁住相关的互斥量。如果互斥量未锁定,它进入临界区并且立马锁定互斥量,并借此防止别的线程进入临界区。
Pthread的几个主要的调用如下所示,他们的作用都在注释中显示:
Pthread库在实现同步方面还需要使用到另外一种机制:条件变量(Condition Variable)。条件变量往往和互斥量一起使用来确保同步。回想一下生产者-消费者问题。当生产者先检查缓冲区是否已经满了,这可以通过mutex实现,而不需要其他线程的干扰,但是如果发现已经满了,那么就需要一种机制让它阻塞以及之后被唤醒。这就需要通过条件变量来实现了。
与条件变量相关的库调用如下所示:
互斥量和条件变量总是一起被使用。使用的基本模式如下:一个线程先锁定mutex,然后等待条件变量(换句话说,就是它需要的资源)知道另一个线程接下来可以给它发信号以便继续。
还是以只有一个单元的缓冲区生产者-消费者问题为例看看Pthread的使用:
14.监视器(Monitor)
在上一篇博客中提到了那个信号量的例子。对于生产者来说,如果将两个down操作的顺序颠倒一下,后果就会很严重。我们先把那个代码回顾一遍:
如果这两个操作颠倒了,换句话说生产者其实还没生产出来就跑去通知消费者了。假设这时候buffer已经满了,那么生产者会阻塞,下面的up(&mutex)操作根本就没执行。好了,轮到消费者了,消费者上来先down(&full),也就是消费掉一个消息,接着要down(&mutex)。但是这时候一看,mutex已经是0,然后一直等,等到晕倒了(阻塞)。于是乎,大家就一直等下去。这就是死锁。
所以说,在使用信号量解决类似的问题上,程序猿必须非常小心。一不注意,就会出事。鉴于这样的情况,又有人提出了新的办法,他们是Brinch Hansen和Hoare。他们提出了一种高层的同步原语——监视器(monitor)。监视器是一组过程(procedures)、变量(variables)、数据结构打包在一个特殊的模块或者包中。进程可以随时调用里面的过程,但是不能直接访问过程暴露给进程之外的内部数据结构。下面是一个监视器的例子,它是用一种假设的语言写的。
监视器有一个明显的特点就是同一时刻只允许一个进程进入其中。由于监视器属于语言本身的一部分,因此监视器保证互斥往往是由语言的编译器来实现的。这样的话程序员可以基本不关注监视器如何安排互斥。但是还有一个问题就是,当进程无法继续时如何使它阻塞。这个问题通过条件变量的引进来解决。条件变量有两个相关的操作:wait和signal。当一个监视器过程发现自己无法运行,那么他对某个条件变量进行一次wait操作,例如,对full这个条件变量进行wait。wait操作会使得调用线程阻塞,从而允许别的进程进入监视器。
而另外一个进程,也就是消费者,可以通过给生产者需要的那个条件变量发信号来使得生产者从阻塞中醒来。当执行完signal之后该怎么继续下去呢?有三种方案:
- 让被唤醒的进程执行;
- 执行了signal操作的进程必须立刻离开监视器
- 让执行signal操作的进程继续进程,等该进程离开监视器后再由被唤醒的进程进入监视器。
这里的条件变量并不是计数器,它不会保存数值以便后面使用,这一点与信号量是不同的。如果对一个没有被wait的条件变量执行signal操作,那么这个signal将会永远消失。因此必须保证wait必须在signal之前。
使用监视器处理生产者、消费者问题的框架如下面的代码:
实际上,上面是采用一种假象的语言来模拟的。实际上Java语言采用了类似的设计思路,实现同步synchronized。一旦某个线程正在执行synchronized方法,此时,该对象中的其他线程不允许调用其中的任何同步方法。下面看看Java中如何解决生产者消费者问题:
15.消息传递机制
消息传递是另外一种进程间通信的方式。消息传递机制使用两个原语:send和receive。这一点和信号量很类似。send和receive是系统调用,而不是语言层次的设计。
前面一个调用将消息发送给指定的接收者,后者从一个发送者处接收消息。如果没有消息则阻塞或者说返回错误代码。
消息传递系统再设计上有一些信号量或者监视器等所不曾碰到的问题,尤其是当通信进程处在不同的机器上。比如,当消息在网络传输中丢失了。因此一旦接受者接收到了消息,必须返回一个回执(Acknowledgement)。如果说发送者没有收到这个回执消息,那么他会重新发送。
那么这又有一个新的问题,怎么区别发送者发送的两次消息是一个消息,只不过由于没有收到回执从新发送了一次?因此给消息一个唯一的序列号,如果接受者接收到的两次消息是一个序列号,那证明是从新发送。
使用消息传递机制实现的生产者消费者问题代码如下:
16.屏障(Barrier)
最后要说的一种同步机制是针对一组进程设计的。假设这样的一组情况:有的应用程序会被分成很多不同的阶段(Phase),并且有一个规定,必须所有的进程都执行完一个阶段才能进入下一个阶段。这样就可以通过在每个阶段的末尾放置一个屏障来实现。如图:

举个例子:假设需要一个时间段测量一次一根铁轨的不同段的温度,存储在一个数组中,每个一段时间用来做科学计算一次。假设这根铁轨特别长,因此,分成若干段,每段的温度由一个进程收集记录。所以,每次提交所有温度之前必须所有进程都统计完这阶段的温度。这时候急需要屏障啦。
~~~~~完~~~~~
分享到:
相关推荐
在个人计算机的早期,操作系统主要在实模式下运行,但随着技术的发展,保护模式成为了现代操作系统的标准模式。保护模式提供了一种更安全的环境,通过内存保护和权限控制防止程序间的相互干扰。它允许操作系统划分...
1. **多道程序设计与分时技术**:这两种技术是现代操作系统的基础,多道程序设计允许多个程序同时在内存中运行,而分时技术则使得多个用户可以共享处理机时间,从而提高了系统资源的利用率。 2. **实时操作系统**:...
理解Linux内核如何管理和分配资源,以及如何实现进程间通信,对于优化系统性能、解决系统瓶颈问题具有重要意义。 总之,这一系列的学习笔记全面覆盖了嵌入式开发的多个层面,从底层硬件到操作系统,再到用户界面,...
C语言的编程基础涉及数据类型、控制结构、函数、指针等核心概念,而高级编程则涵盖更复杂的话题,如动态内存管理、文件操作、多线程和进程间通信。 内容部分由于OCR扫描结果存在文字识别错误和遗漏,导致部分信息...
此外,操作系统还需要处理进程间的通信和同步问题,如信号量、管程等机制,以避免竞态条件和死锁。 3. 文件系统:文件系统是操作系统管理数据存储的重要组件。它负责组织、命名、检索和保护文件,以及管理磁盘空间...
本章详细阐述了Linux内核如何管理和调度进程,包括进程创建、销毁以及进程间通信等机制。此外,还会讨论Linux的调度算法,例如O(1)调度器,以及这些算法是如何优化系统性能的。 #### 三、内存管理(第3章) 内存...
- **第 4 章:使用原生文件对话框和进程间通信** —— 探讨如何集成操作系统的原生功能,并实现不同进程之间的数据交换。 - **第 5 章:处理多窗口管理** —— 讲解如何管理和控制多个窗口,以提供更加丰富的用户...
SAP Basis系统与系统环境是SAP技术体系的基础,它为构建在SAP Basis之上的组件系统(如SAP R/3)提供了运行平台。这个系统环境的目标是描绘mySAP.com互联网商业框架的概念,理解决策SAP Basis系统及其在系统景观中的...
- **概述**: 用于进程间通信的文件。 - **实践**: 在`/tmp`或`/var/run`目录下可以找到socket文件。 **5. 疑难杂症——删除不掉的文件** - **概述**: 有时文件因为权限问题或其他原因无法删除。 - **实践**: ...
D-Bus是一种用于Linux和其他类Unix操作系统上进程间通信的机制。它为应用程序提供了一种简单而强大的方式来进行通信和同步操作。在Bluez项目中,D-Bus扮演着关键角色,它允许不同的应用程序通过一个统一的接口访问和...
- **操作系统发展史**:从早期的批处理系统到现代的操作系统发展历程。 - **Linux与嵌入式Linux**:介绍Linux操作系统及其在嵌入式领域的应用特点。 - **操作系统内核** - **内存管理** - **内存管理功能**:...
Go语言的核心特性之一是其并发模型,它采用了轻量级线程——goroutines,以及通道(Channels)来实现进程间的通信。这种模型让开发者能够轻松地编写出高效、安全的并发程序,而无需深入理解复杂的锁和信号量机制。 ...