CMU 15-213 Introduction to Computer Systems学习笔记(21) Synchronization: Basic

实际上,有一类很重要的并发编程,这种并发编程模型叫做【fork】和【join】(分叉和会和),这个模型中,程序有一系列阶段组成,在每个阶段,都有多个工作线程和一个管理线程,程序会创建多个工作线程,然后每个工作线程解决该阶段的某些问题,在程序中,你可以使用某个可以切分成多个部分你的数据结构,然后每个线程更新其该数据结构中的某个块,但出于某种原因,管理线程必须等待所有的工作线程才能进入下一阶段,这里做的事叫做【join】,前面的叫做【fork】。管理线程等待所有的工作线程完成,通过调用pthread_join,因为只有当所有的工作做完时,才可以去做下一阶段的工作,这个模型在科学计算等方面非常的重要,在一些领域你可能需要做一些仿真,比如仿真自然界的一些现象,

Threads review

我们看到了使用线程程序好的方面,使用线程的并发可以共享所有的全局变量,但是这种共享可能会产生意外的情况,所以我们需要一种机制,可以控制线程交错的顺序(进程调度的顺序),只有这样当我们共享数据结构的时候,才不会发生坏事,好的,控制进程交错的过程称为同步(synchronization),我们将学习线程同步的一些技术,使用它们可以编写出正确的线程程序。

Shared Variables in Threaded C Programs

如果一个进程没有共享任何数据结构,这些线程是独立运行的,线程交错如何都不会有问题,但是线程一旦共享资源,我们就要小心

我们要了解什么是线程中的资源共享,答案并不是像【全局变量是共享的,栈变量是不共享的】这么简单,

比如有这个定义:变量x是被共享的如果多个线程引用了x的实例

Threads Memory Model

我们来看下线程的内存模型,概念模型和真实模型略有不同,从概念上来讲,有多个线程在单个进程的上下文中运行,有些进程上下文是共享的,有些不是共享的,每个线程有自己独立线程id,栈,栈指针,程序计数器,条件码,通用目的的寄存器,然后线程共享剩下的进程上下文,进程上下文由内核维护,如虚拟内存系统中的数据结构,打开的文件描述符,信号处理程序等等。但不幸的是,实际系统不是严格按照这种模型实现的,虽然寄存器是独立的,内核为所有寄存器维护单独的上下文,但是由于线程共享地址空间,所有线程可以访问所有的栈,一个线程可以访问另一个线程的栈,虽然从概念上来讲这些栈是独立的,但是实际情况并非如此。

Mapping Variable Instances to Memory

Shared Variable Analysis

volatile告诉了编译器永远不要把变量放在寄存器中,所以volatile修饰的变量总是从内存中读取,修改后又存储到内存中。volatile可以防止变量永久存储再寄存器中,volatile修饰的变量可以被加载到寄存器中,但紧接着该变量的值要被写回到内存。

 

Progress Graphs

现在的问题是我们如何保证安全的轨迹,也就是我们所说的同步,我们想以某种方式配置内核,使得内核永远不会调度不安全的轨迹,那我们应该怎么做呢,我们必须同步这些线程的执行,但从另外一个角度讲,我们只需要保证对临界区的互斥访问。一旦一个线程开始执行临界区的第一条指令,我们不希望它被另一个具有相同临界区的线程打断,例如,一个对应于某个全局变量的临界区,不能被执行同样的临界区的另一个线程打断,

Enforcing Mutual Exclusion

 

Using Semaphores for Mutual Exclusion

Semaphores

信号量是一个非负的全局整数,信号量作为同步的变量,被两个内核函数P和V操作,两个系统调用叫做P和V,P和V对应于荷兰语Proberen(测试)和Verhogen(增加),我们简称P和V,只需要了解两个函数的作用就好,这两个函数都以信号量作为参数,并且P操作具有以下语义,如果s不为0,将其减1并且立即返回,如果s=0,就挂起这个线程,知道s变为非0,然后通过V操作重启该线程,如果信号量为0,P就会阻塞,线程就会被挂起,直到它被V操作重新启动,然后重新启动P操作,现在P操作可以减1并将控制权返回给调用者。

V操作只将s增加1,这里的增加不像前面的cnt++,这里的增加操作是原子的,因此永远不会被打断,然后在它增加s后,它会检查是否有线程阻塞在P操作上,你可以认为,内核维护了一个被P操作阻塞的线程队列,并且在V操作增加信号量之后,它会检查该队列是否又被P操作阻塞的线程,这些线程在执行P操作时,信号量为0,然后它以某种不确定的顺序重新启动其中一个线程,按照某种顺序,所以你不能假设按照某种顺序,内核使用某种选择算法从队列中选择一个线程,然后内核重启这个被P操作阻塞的线程,然后P操作可以将信号量减1。

这样定义P和V操作的主要目的是,让信号量获得一个信号量不变性的属性,信号量不变性,也就是对于一个信号量,只通过P和V操作对信号量进行操作的时候,信号量的值总是大于或者等于0。这个性质非常的有用,利用这个属性可以实现对临界区的互斥访问,

C Semaphore Operations

Using Semaphores for Mutual Exclusion

使用信号量去修复课程Slides中的bug,基本的想法是初始化信号量为1,根据定义,任何初始化为1的信号量称为互斥锁,因为它用来提供互斥操作,这种叫法可以追溯到Dijkstra早期的论文。因此我们为程序中的每个共享变量提供一个唯一的互斥锁,即初始化值为1的信号量,在我们这个程序中,我们只关心cnt这个共享变量,因此我们只创建一个称为mutex的互斥锁,然后使用PV操作将临界区操作cnt的部分保护起来,也就是,你先调用P,然后执行临界区,然后再调用V。

现在讲一下我们使用信号量时我们要用到的一些术语

首先是二进制信号量,它是一个信号量,其值始终为0和1,然后是互斥锁(互斥量),即用来互斥的二进制信号量,P操作称为互斥锁加锁,V操作称为互斥锁释放。如果进程持有互斥锁,那么意味着互斥锁已经加锁,但未被释放,因此互斥量和二进制信号量始终初始化为1,互斥锁用于互斥,但还有一种信号量叫做counting semaphore,它可以用来对系统的事件计数,对于accounting semaphore,信号量的值通常大于1,好,下面我们使用互斥锁修复程序。

Why Mutexes Work

现在看这一个进度图,这个进度图对于使用PV操作同步后的程序,所以我们在临界区之前调用P,然后执行临界区,然后我们调用V,现在,P会减少信号量,而V会增加信号量,因此如果你记录了执行状态空间中每个点的信号量值,你会得到这些信号量的值,原点的信号量值为1,然后程序开始执行,在H(1)之后,信号量为1,然后我们执行P操作,信号量减1,然后程序继续执行。

PV操作创造了禁止区,禁止区对应于状态空间中的,信号量为-1的地方,因为这些地方是不可到达的,根据P和V的定义,这些地方永远无法到达,这样就形成了一个包含不安全区域的禁止区了,这样就可以提供对临界区的互斥访问。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值