操作系统 --- 进程/线程 同步

资源竞争 — race condition

  • 在多线程/多进程中,容易出现race condition
  • race condition: 多个进程或线程对同一个资源进行同时进行修改时会出现混乱
  • 如下图P1和P2的代码转换为汇编之后实际是三行代码
  • 正常情况应该是P1对count修改完P2才能对count进行修改
  • 但是实际情况是当P1正在对count进行修改时P2也对count进行修改,出现了race condition
    在这里插入图片描述
    Producer-Consumer Problem
  • 在counter++和counter–时有可能出现race condition
    在这里插入图片描述

临界区 — critical section

  • critical section的代码会使用critical resource(大部分是shared variable,也可能是IO device或者network connection)
  • 临界区问题: 如何避免临界区的race condition
    在这里插入图片描述

解决临界区问题 ---- 实现进程同步

进程同步的三个原则

互斥(mutual exclusive)

  • 我们需要保证不能有两个进程或线程同时进入critical section,来避免race condition. 也就是critical section need to be protected
  • 也就是要实现互斥(mutual exclusive): 同一时间只能有一个任务进入critical section
  • 所以执行critical section的进程或线程不能被打断,需要实现原子操作(atomic execution)
  • 原子操作(atomic execution): 代码不被打断,一次执行完毕

有限等待(bounded waiting)

  • 在一个进程或线程申请进入临界区并被授权进入临界区之后,此进程或线程能进入临界区的次数是有限的(也就是不能永远占用临界区,要让别的进程或线程也能使用临界区)

前进(progress)

  • 当无进程或线程处于临界区,可允许一个请求进入临界区的进程或线程立即进入自己的临界区

进程同步的简单解决方案 — 锁(lock)

Intuitive Solution

  • 对于进程/线程同步最直接的想法就是加一个shared variable来控制critical section
loop{
	if not lock {
		lock = true;
		critical_section
		lock = false;
		
	}
}
  • 但是问题是 lock 也是shared variable,而对于lock的读写是分开的,所以也存在race condition问题
  • 会导致两个进程或线程同时进入critical section
    在这里插入图片描述

Software solution

  • 以下是两个著名的解决race condition的算法 (通过改进intuitive solution)
Dekker算法
wants_to_enter[0] = false;
wants_to_enter[1] = false;
turn = 0; //or 1

p0:
	//当一个进程或线程想进入critical section时,首先将自己的wants_to_enter[0]变量设置为true
	wants_to_enter[0] = true;
	//查看对方是否想进入, 如果对方不想进入,则自己进入critical section
	while(wants_to_enter[1]) {
		//如果对方想进入,则查看是谁的turn
		if (turn != 0) {
			//如果是对方的turn,则表示自己不想进入
			wants_to_enter[0] = false;
			//然后等待
			while (turn != 0) {
				//busy wait;
			}
			//对方从critical section出来了,turn是自己了,将 wants_to_enter[0] = true
			//表示自己想进入
			wants_to_enter[0] = true;
		}
	}
	
	//critical section
	turn = 1;
	wants_to_enter[0] = false;
	//reminder section
  • 两个进程放在一看:
  • 两个人同时用洗手间,同时表示想进去(wants_to_enter)
    然后看是谁的turn,没有轮到的那个人则表示自己不想进,此时另外一个人就可以进去了
    进去的那个人用完之后表示自己不想进了,然后将turn设置为对方
    在这里插入图片描述
Peterson算法
  • 比dekker算法简单一些
wants_to_enter[A] = true;
turn = B;
while (want_to_enter[B] == true && turn == B) {
	//wait;
}
//critical section

wants_to_enter[A] = false;
  • 只有当对方不想进或者是我的turn的时候进入critical section
    在这里插入图片描述
Software Solution的优缺点

优点:

  • 在用户空间运行,不需要硬件的协助

缺点:

  • 在现代计算机中,不一定能成功,因为software solution依赖于sequential consistency. 而现代计算机为了优化,在编译时会打乱sequential consistency
  • sequential consistency: 简单来说就是按顺序执行代码
  • 同时Software solution只能保证在单核计算机中成功,当多个任务在不同的核运行时,可能会同时进入critical section
  • 所以以上的两种算法已经被淘汰,只有历史意义

Hardware assisted solution

Test_and_Set( )
loop{
	if not lock {
		lock = true;
		critical_section
		lock = false;
		
	}
}
  • 在上面这个代码中,因为对于lock的读和写不是atomic instruction所以会出现race condition
  • 为了解决这个问题,现代计算机提供了atomic read and write指令(汇编指令). 也就是read和write连在一起是一个整体,是atomic的. 这样就在硬件的帮助下解决了这个问题
  • 这个指令叫做 “Test and Set” (TSL)
//pseudo code
//如果lock == true,则继续lock,然后返回true
//如果lock == false,则将lock设为true,然后返回false
function Test_and_Set(lock) {
	current_value = lock;
	lock = true;
	return current_value;
}
//使用test_and_set解决lock问题
//如果lock ==  true则返回true,一直while循环等待
//如果lock == false则返回false并将lock设置为true, 打破循环进入critical section
function Lock(*lock) {
	whileTest_and_Set(lock));
}

function Unlock(*lock) {
	lock = false;
}
compare_and_swap( ) aka CAS
  • 如果系统没有提供Test_and_Set( ),则可以使用compare_and_swap( )
  • compare_and_swap(ptr, old, new), 根据ptr查看所指向的内存里储存的值
  • ptr: 内存地址, old: 备份旧数据, new: 基于旧数据构造新数据
  • 如果 *ptr == old ,说明当前没有其它进程或线程在操作,所以,我们把new这个值写入ptr所指向的内存中
  • 如果 *ptr != old,说明原来备份的旧数据已经被改动, 需要根据被改动的旧数据重新计算新数据
  • 概括:CPU去更新一个值,但如果想改的值不再是原来的值,操作就失败,因为很明显,有其它操作先改变了这个值
  • CPU会保证上述操作是atomic

使用compare_and_swap解决lock问题

//如果lock == fasle,则设置为true,然后返回true
//如果lock == true, 则直接return false
function Lock(*lock) {
 while(!compare_and_swap(&lock, false, true));
 return
}

function Unlock(*lock) {
 lock = false
}
TS和CAS的优缺点 – 自旋锁和ABA问题
  • 自旋锁
  • 从上面代码可以看到,TS和CAS都是通过一个while loop不断查看lock的值,这种情况被叫做busy waiting也就是CPU一直在执行这个进程,开销很大。这种锁被叫叫做自旋锁(spinlock)
  • 当然自旋锁也有好处,就是在进程等待时不会有context switch.而context switch的时间消耗比较大
  • CAS的ABA问题
  • CAS还有个问题就是ABA问题,比如第一次拿到内存里的值时是A,然后被其他线程修改为B, 然后又修改为A, 而此时去比较内存里的值会发现没有变,但是实际上还是有改动
  • 举个通俗点的例子,你倒了一杯水放桌子上,干了点别的事,然后同事把你水喝了又给你重新倒了一杯水,你回来看水还在,拿起来就喝,如果你不管水中间被人喝过,只关心水还在,还好 ; 但是假若你是一个比较讲卫生的人,那你肯定就不高兴了
  • ABA问题的解决思路: 使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加1,那么A→B→A就会变成1A→2B→3A了
互斥锁(mutex lock)
  • 为了解决spinlock CPU开销大的问题,我们可以让一个进程或线程进入阻塞状态如果这个进程或线程需要进入的critical section是locked,然后等待CPU调度唤醒再次查看critical section的状态
  • 这种锁被叫做互斥锁(mutex lock)

data structure of mutex:

//pesudo code
struct {
	int lock; //mutex value
	PCB *PCB_block_q; //waitlist of process or threads, 放入阻塞的线程,等待CPU唤醒
}

常用的C library mutex函数

#include<pthread.h>
int pthread_mutex_init(pthread_mutex_t *restrict mutex, 
					   const pthread_mutexattr_t *restrict attr);
int pthread_mutex_destory(pthread_mutex_t *mutex);
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
//以上所有函数成功时返回值为0,失败会返回error code

//pthread_mutex_t的定义
 typedef struct {
         struct pthread_queue queue; //waitlist of process or threads
         char lock; //mutex value
         struct pthread *owner;
         int flags;
 #ifdef _POSIX_THREADS_PRIO_PROTECT
         int prioceiling;
         pthread_protocol_t protocol;
         int prev_max_ceiling_prio;
 #endif
 } pthread_mutex_t;
  

pthread mutex example

#include <stdio.h>
#include <pthread.h>
//假设我们有这样的一个数据结构可以被多个线程访问
struct foo {
	int 			f_count; //记录有多少个线程访问了这个数据结构
	pthread_mutex_t f_lock; //一个mutex lock用来保护这个数据结构
	int 			f_id;
};

//初始化数据结构
strcut foo* foo_alloc(int id) {
	struct foo *fp;
	if ((fp = malloc(sizeof(struct foo))) != NULL) {
		fp->f_count = 1; //初始值为1, 暂时没有其他线程访问
		fp->f_id = id;
		//初始化mutex,如果初始化失败则释放内存,返回
		if (pthread_mutex_init(&fp->f_look, NULL) != 0) {
			free(fp);
			return(NULL);
		}
	}
	return(fp);
}

//如果一个线程正在访问数据结构fp, foo_hold会increment f_count,用mutex锁保护f_count
void foo_hold(struct foo *fp) {
	//先上锁
	pthread_mutex_lock(&fp->f_lock);
	fp->f_count++;
	//然后解锁
	pthread_mutex_unlock(&fp->f_lock);
}

//如果一个线程完成访问,foo_rele会decrement f_count
void foo_rele(struct foo *fp) {
	//先上锁
	pthread_mutex_lock(&fp->f_lock);
	//如果f_count等于0,则说明没有线程在访问,所以先解锁, 然后销毁锁,然后释放fp
	if (--fp->f_count == 0) {
		pthread_mutex_unlock(&fp->f_lock);
		pthread_mutex_destroy(&fp->f_lock);
		free(fp);
	}
	else {
		//其余情况直接解锁
		pthread_mutex_unlock(&fp->f_lock);
	}
}

死锁,活锁,饥饿(deadlock, livelock and starvation)

死锁(deadlock)
  • 死锁: 是指两个或两个以上的进程或线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程

发生死锁的条件:

  • 当前进程或线程拥有其他进程或线程需要的资源
  • 当前进程或线程等待其他进程或线程已拥有的资源
  • 都不放弃自己拥有的资源,也就是不能被其他进程或线程剥夺,只能在使用完以后由自己释放

Example 1:

mutex m
function {
	lock(m) //成功拿到锁
	lock(m) //拿不到锁,因为已经被自己拿了,所以会无限等待下去
	//critical section
	unlock(m)
	unlock(m)
}

Example 2:
在这里插入图片描述

  • task A成功拿到M1的锁,同时task B成功拿到M2的锁
  • task A等待获取M2的锁,同时task B等待获取M1的锁
  • task A只有获得M2的锁才能往下继续然后释放M1的锁
  • task B只有获得M1的锁才能往下继续然后释放M2的锁

避免死锁:

  • 在所有进程或线程中使用相同的加锁顺序
  • 加锁时限(线程尝试获取锁的时候加上一定的时限,超过时限则放弃对该锁的请求,并释放自己占有的锁)
  • 通过算法实现死锁检测机制
//Example: 在所有进程或线程中使用相同的加锁顺序
Thread A:
functionA {
    //some code
	lock(A)
	lock(B)
	// ....
}

Thread B:
functionB {
    //some code
	lock(A) //不能是lock(B) 然后lockA
	lock(B)
	// ....
}
活锁(livelock)
  • 是指线程1可以使用资源,但它很礼貌,让其他线程先使用资源,线程2也可以使用资源,但它很绅士,也让其他线程先使用资源。这样你让我,我让你,最后两个线程都无法使用资源。
饥饿(starvation)
  • 是指一个可运行的进程尽管能继续执行,但被调度器无限期地忽视,而不能被调度执行的情况

乐观锁和悲观锁(Optimistic Locking and Pessimistic Locking)

  • 乐观锁和悲观锁严格的说不是一种锁,而是一种策略
  • 加锁是一种悲观的策略,它总是认为每次访问共享资源的时候,总会发生冲突,所以宁愿牺牲性能(时间)来保证数据安全。
  • 无锁是一种乐观的策略,它假设线程访问共享资源不会发生冲突,所以不需要加锁,因此线程将不断执行,不需要停止。一旦碰到冲突,就重试当前操作直到没有冲突为止。无锁的策略之一就是使用CAS

更好的解决进程同步的方法— 信号量(semaphore)

什么是semaphore

  • semaphore的本质是一个计数器
  • semaphore主要用于调度进程, 维护对共享资源的顺序
  • 所以利用semaphore可以解决很多进程同步的问题,如mutex,生产者消费者问题,read/writer问题等

semaphore的实现

  • 数据结构semaphore包括一个 counter用来表示目前的资源数量,和一个queue用于存放等待中的线程
  • semaphore的实现包括两个函数: post(signal), wait
  • wait对counter进行递减操作,表示消费掉资源一个, 如果递减完counter为负说明目前没有资源可供消费,则需要将目前进程放进等待队列, 然后block掉
  • post对counter进行递增操作,表示资源增加一个,如果递增完counter小于等于0则说明有线程在排队等待消费资源,所以需要队列中的资源移出,然后唤醒
//pseudo code
struct {
	int counter;//表示目前的资源数量
	queue q;//用于存放等待中的线程
} sem_t 

//v operation
signal(sem_t *s) {
	s.counter++;
	//counter小于等于0则说明有线程在排队等待消费资源,所以需要队列中的资源移出,然后唤醒线程	
	if (s.counter <= 0) {
		remove(s.q, p);
		wakeup(q);
	}
}

//p operation
wait(sem_t *s) {
	s.counter--;
	//counter为负说明目前没有资源可供消费,则需要将目前进程放进等待队列, 然后block掉
	if (s.counter < 0) {
		add this thread to s.q;
		block();
	}
}

semaphore的应用

semaphore控制代码执行顺序
  • 以下是两个任务,可以利用semaphore实现statement1在statement2之前运行
  • sem_t synch.counter = 0;
  • wait(synch)会对counter – 为 -1, 所以T2会被放进queue,block掉
  • signal(synch) 会对counter++为0,所以会唤醒T2,让T2继续执行
    在这里插入图片描述
semaphore实现mutex功能
  • 当counter等于1时,semaphore实现了mutext的功能 (也就是mutex就是semaphore的value等于1的情况)
  • 假设A首先执行wait(M), 则counter = 0,B和C会被放进queue,而counter被B和C递减两次变为 -2
  • A执行完signal之后,会对counter++变为-1,然后唤醒B
  • B执行完signal之后,会对counter++变为0,然后唤醒C
  • C执行完signal之后,会对counter++变为1
    在这里插入图片描述
semaphore解决消费者/生产者问题
  • 有多个消费者和生产者要对下图的buffer进行操作
    在这里插入图片描述
sem_t has_space = N;
sem_t has_item = 0;
sem_t mutex = 1;

producer() {
	int item;
	while (True) {
		item = produce_item( );
		//producer需要查看buffer里还有没有space
		wait(has_space);
		//has_item和has_space的作用是负责资源分配,
		//但是不能保证不会有多个produce和conumer同时进入critical section所以需要加mutex
		wait(mutex);
		
		//critical section
		buffer[in] = item;
		in = (in + 1) % N;
		
		signal(mutex);
		//生产完成后,递增资源数量
		signal(has_item);
	}
}

consumer() {
	int item;
	while(True) {
		//consumer需要查看buffer里有没有item
		wait(has_item);
		//has_item和has_space的作用是负责资源分配,
		//但是不能保证不会有多个produce和consumer同时进入critical section所以需要加mutex
		wait(mutex);
		
		//critical section
		item = buffer[out];
		out = (out + 1) % N;
		
		signal(mutex);
		//消费完成后,递增space数量
		signal(has_space);
	}
}

  • 注意上面wait(has_item)和wait(mutex)的顺序不能互换,否则l两个mutex会形成deadlock
  • 假设producer先拿到mutex锁但是没有space了,所以会被block掉等待space, 但是此时consumer在等待获得mutex锁才能进入critical section消费资源
    在这里插入图片描述
semaphore解决writer/reader问题 — reader preference
  • 多个reader和writer同时对资源进行读写,如何同步
  • 第一种方法: reader preference
  • 多个reader可以同时读取资源,reader之间不需要竞争
  • reader和writer不能同时访问资源
  • 即使有writer在等待,reader也可以访问资源
  • 只有当所有reader访问完之后writer才能访问资源
  • 只有第一个reader需要和writer竞争
  • 也就是第一个reader代表所有reader和writer竞争资源,当reader成功竞争到资源之后,剩下的所有reader可以直接读取资源,而writer只能等待所有reader完成之后才能访问资源,所以最后一个reader访问完毕后需要解锁
int reader_count = 0; //用于记录reader的数量
sem_t resource_mutex = 1 //保护资源的mutex
sem_t reader_count_mutex = 1//保护变量reader_count的mutex,因为reader_count也是共享变量
reader() {
	wait(reader_count_mutex);	
	/这部分也是critial section, 因为有count
	reader_count++;
	if (reader_count == 1) {
		wait(resource_mutex);
	}
	///
	signal(reader_count_mutex);
	
	 //critical section
	 //read;
	 
	wait(reader_count_mutex);
	//这部分也是critial section, 因为有count
	reader_count--
	if (reader_count == 0) {
		signal(resource_mutex);
	}
	/
	signal(reader_count_mutex);
}


writer () {
	wait(resource_mutex)//critical section
	//write
	signal(resource_mutex);
}
  • reader preference存在的问题是,如果不断的有reader进来,则writer会进入饥饿状态
  • 所以reader preference需要改进为 ---- writer preference
semaphore解决writer/reader问题 — writer preference
  • 为了解决reader preference下writer进入饥饿状态的情况
  • 主要思路是降低reader访问资源的机率
  • 假设资源放在会议室里,在之前reader preference的模式下会议室只有一道门(resource_mutex), 而现在需要在会议室外再加一道门
  • 所有的reader需要互相竞争进入第一道门, 而除去第一个writer需要和reader竞争以外,其余所有的writer都可以直接进入第一道门
  • 然后按照reader preference分配第二道门的权限
int reader_count = 0; //第二道门:需要记录reader的数量
int writer_count = 0;//第一道门: 需要记录writer的数量
sem_t first_door_mutex = 1 //第一道门的mutex
sem_t resource_mutex = 1 //保护资源的mutex(第二道门的mutex)
sem_t reader_count_mutex = 1//保护变量reader_count的mutex,因为reader_count也是共享变量
sem_t writer_count_mutex = 1//保护变量writer_count的mutex,因为writer_count也是共享变量

reader() {
	wait(first_door_mutex);
	wait(reader_count_mutex);
	reader_count++;
	if (reader_count == 1) {
		wait(resource_mutex);
	}
	signal(reader_count_mutex);	
	signal(first_door_mutex);
	//read critical section
	
	reader_count--;
	if (reader_count == 0)
		signal(reource_mutex);
	signal(reader_count_mutex);
	
}

writer() {
	wait(writer_count_mutex);
	writer_count++;
	if (writer_count == 1)
		wait(first_door_mutex);
	signal(writer_count_mutex);
	
	wait(reource_mutex);
	//write --- critcial section
	signal(reource_mutex);
	
	wait(writer_count_mutex);
	writer_count--;
	if (writer_count == 0) {
		signal(first_door_mutex);
	}
	signal(writer_count_mutex);
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值