目录
进程同步
1. 临界区
对临界资源进行访问的那段代码称为临界区。
为了互斥访问临界资源,每个进程在进入临界区之前,需要先进行检查。
// entry section
// critical section;
// exit section
2. 同步与互斥
-
同步:多个进程按一定顺序执行;
-
互斥:多个进程在同一时刻只有一个进程能进入临界区。
3. 信号量
信号量(Semaphore)是一个整型变量,可以对其执行 down 和 up 操作,也就是常见的 P 和 V 操作。
-
down : 如果信号量大于 0 ,执行 -1 操作;如果信号量等于 0,进程睡眠,等待信号量大于 0;
-
up :对信号量执行 +1 操作,唤醒睡眠的进程让其完成 down 操作。
down 和 up 操作需要被设计成原语,不可分割,通常的做法是在执行这些操作的时候屏蔽中断。
如果信号量的取值只能为 0 或者 1,那么就成为了 互斥量(Mutex) ,0 表示临界区已经加锁,1 表示临界区解锁。
typedef int semaphore;
semaphore mutex = 1;
void P1()
{
down(&mutex);
// 临界区
up(&mutex);
}
void P2()
{
down(&mutex);
// 临界区
up(&mutex);
}
使用信号量实现生产者-消费者问题
问题描述:使用一个缓冲区来保存物品,只有缓冲区没有满,生产者才可以放入物品;只有缓冲区不为空,消费者才可以拿走物品。
因为缓冲区属于临界资源,因此需要使用一个互斥量 mutex 来控制对缓冲区的互斥访问。
为了同步生产者和消费者的行为,需要记录缓冲区中物品的数量。数量可以使用信号量来进行统计,这里需要使用两个信号量:empty 记录空缓冲区的数量,full 记录满缓冲区的数量。其中,empty 信号量是在生产者进程中使用,当 empty 不为 0 时,生产者才可以放入物品;full 信号量是在消费者进程中使用,当 full 信号量不为 0 时,消费者才可以取走物品。
注意,不能先对缓冲区进行加锁,再测试信号量。也就是说,不能先执行 down(mutex) 再执行 down(empty)。如果这么做了,那么可能会出现这种情况:生产者对缓冲区加锁后,执行 down(empty) 操作,发现 empty = 0,此时生产者睡眠。消费者不能进入临界区,因为生产者对缓冲区加锁了,消费者就无法执行 up(empty) 操作,empty 永远都为 0,导致生产者永远等待下,不会释放锁,消费者因此也会永远等待下去。
#define N 100
typedef int semaphore;
semaphore mutex = 1;
semaphore empty = N;
semaphore full = 0;
void producer()
{
while(TRUE)
{
int item = produce_item();
down(&empty);
down(&mutex);
insert_item(item);
up(&mutex);
up(&full);
}
}
void consumer()
{
while(TRUE)
{
down(&full);
down(&mutex);
int item = remove_item();
consume_item(item);
up(&mutex);
up(&empty);
}
}
4. 管程
使用信号量机制实现的生产者消费者问题需要客户端代码做很多控制,而管程把控制的代码独立出来,不仅不容易出错,也使得客户端代码调用更容易。
c 语言不支持管程,下面的示例代码使用了类 Pascal 语言来描述管程。示例代码的管程提供了 insert() 和 remove() 方法,客户端代码通过调用这两个方法来解决生产者-消费者问题。
monitor ProducerConsumer
integer i;
condition c;
procedure insert();
begin
// ...
end;
procedure remove();
begin
// ...
end;
end monitor;
管程有一个重要特性:在一个时刻只能有一个进程使用管程。进程在无法继续执行的时候不能一直占用管程,否者其它进程永远不能使用管程。
管程引入了 条件变量 以及相关的操作:wait() 和 signal() 来实现同步操作。对条件变量执行 wait() 操作会导致调用进程阻塞,把管程让出来给另一个进程持有。signal() 操作用于唤醒被阻塞的进程。
使用管程实现生产者-消费者问题
// 管程// 管程
monitor ProducerConsumer
condition full, empty;
integer count := 0;
condition c;
procedure insert(item: integer);
begin
if count = N then wait(full);
insert_item(item);
count := count + 1;
if count = 1 then signal(empty);
end;
function remove: integer;
begin
if count = 0 then wait(empty);
remove = remove_item;
count := count - 1;
if count = N -1 then signal(full);
end;
end monitor;
// 生产者客户端
procedure producer
begin
while true do
begin
item = produce_item;
ProducerConsumer.insert(item);
end
end;
// 消费者客户端
procedure consumer
begin
while true do
begin
item = ProducerConsumer.remove;
consume_item(item);
end
end;
线程同步
一、线程同步和线程互斥
首先我们要清楚什么是线程同步和线程互斥:
线程同步:
线程同步指的是多个线程之间协调同步,按照预定的先后次序进行运行,这种先后次序取决于要完成的特定任务,最基本的场景就是:A线程要完成的任务依赖于B线程的数据。
线程互斥:
线程互斥是指对于线程共享的线程资源,在各个线程访问时具有排它性。当有若干个线程要访问同一共享资源时,任何时刻只允许一个线程进行访问,直到占有资源者放弃使用该资源。线程互斥可以看成一种特殊的线程同步
二、线程同步的方式
1、互斥锁
当有多个线程需要访问同一个资源时,如果不做处理,有时候就会出现问题,比如有两个线程需要使用打印机,进程A正在使用,而进程B也要使用打印机,此时打印出来的东西就是错乱的。互斥锁就是控制对共享资源的使用。互斥锁只有两种状态:加锁、解锁。
互斥锁的特点:
原子性:把互斥量锁定位一个原子操作,这就保证了如果同一时间只会有一个线程锁定共享资源
唯一性:如果一个线程锁定了某个互斥量,那么只有该线程可以使用这个被锁定的互斥量
非繁忙等待:如果一个线程锁定了某互斥量,另一个线程又来访问该互斥量,则第二个线程会被挂起,当第一个线程解锁该互斥量后唤醒第二个线程对该互斥量进行访问。2、条件变量:
与互斥锁不同,条件变量是用来等待而不是用来上锁的。条件变量用来锁定一个线程,直到某个特殊的条件发生为止。通常条件变量和互斥锁同时发生。
条件变量可以是我们睡眠等待某种情况的发生。条件变量是利用线程间共享的全局变量进行同步的一种机制,主要包括两个动作:
一个线程等待“条件变量的条件成立”而挂起
另一个线程使条件成立
条件的检测是在互斥锁的保护下进行的。线程在改变条件变量的状态之前必须先锁定互斥量。如果一个条件为假,则线程自动阻塞,并释放等待状态改变的互斥锁。如果另外一个线程改变了条件,它发信号给相关联的条件变量,唤醒一个或多个等待它的线程,重新获得互斥锁,重新评价条件。条件变量的操作流程如下:
初始化条件变量
等待条件成立
激活条件变量
清除条件变量3、信号量
使用信号量首先我们要清楚临界资源和临界区的概念,临界资源就是同一时刻只允许一个线程(或进程)访问的资源,临界区就是访问临界资源的代码段。
信号量是一种特殊的变量,用来控制对临界资源的使用,在多个进程或线程都要访问临界资源的时候,就需要控制多个进行或线程对临界资源的使用。信号量机制通过p、v操作实现。p操作:原子减1,申请资源,当信号量为0时,p操作阻塞;v操作:原子加1,释放资源。
信号量的使用及实例:信号量
4、读写锁
读写锁和互斥锁类似,不过读写锁允许更改的并行性。互斥锁要么是加锁状态,要么是不加锁状态。而读写锁可以有三种状态:读模式下的加锁、写模式下的加锁、不加锁 状态。一次只有一个线程可以占有写模式下的读写锁,但是可以有多个线程占有读模式下的读写锁。
读写锁的特点:
如果有线程读数据,则允许其他线程读数据,但不允许写
如果有线程写数据,则不允许其他线程进行读和写5、自旋锁
自旋锁和互斥锁的功能一样,但是互斥锁在线程阻塞时会让出cpu,而自旋锁则不会让出cpu,一直等待,直到得到锁。
本文深入解析了进程和线程同步的多种方式,包括临界区、信号量、管程、互斥锁、条件变量等,阐述了它们在解决生产者-消费者问题中的应用。
1481

被折叠的 条评论
为什么被折叠?



