进程和线程的管理

  • 多道程序设计:管理单处理器系统中的多个进程。
  • 多处理器技术:管理多处理器系统中的多个进程。
  • 分布式处理器技术:管理分布式环境下的多个进程。

并发出现的三种环境

  • 多应用程序:多个运行中的应用程序共享处理器时间。
  • 结构化应用程序:单个应用程序设计成多个并发进程。
  • OS 结构:部分 OS 自身作为一组进程或者线程来实现。

相关术语

  • 原子操作:一个或者多个指令构成的序列,满足如下要求:这个指令序列要么全部执行完成要么全部都不执行。
  • 临界区:一段访问共享资源的代码。
  • 死锁:2个或以上数量的进程分别同时占用部分资源、等待其他进程释放资源导致每个进程都无法执行的情况。
  • 活锁:2个或以上数量的进程因为响应其他进程的变化而不断改变自身状态、没有做任何有用的工作的情况。
  • 互斥:一个进程进入临界区访问共享资源,其他进程无法进入临界区的情况。
  • 竞争条件:多个进程(或者线程)同时读写一个共享数据,数据的最终结果由执行顺序决定的情况。
  • 饥饿:一个处于就绪态的进程长时间得不到执行的情况。
  • 临界资源:在一个时间点只能被一个程序访问的资源。
  • 临界区:程序中访问临界资源的那部分代码段。
  • 忙等待(自旋等待):进程在得到进入临界区的访问权之前,只能执行测试变量而不能做其他事情的情况。

并发的原理

单处理器系统中,多个进程交替执行,从外部表现出并发的特征。多处理器系统中,多个进程不仅可以交替执行,而且可以重叠执行,每个处理器可以在某个时间点执行一个进程。

由于进程的相对执行速度不可预测,出现了几个问题:1、全局资源共享存在危险,2、OS 难以对全局资源进行最优化分配,3、定位程序设计错误困难。

对于某个全局变量,一个进程甲修改了它的值,然后被中断。进程乙访问这个全局变量,也修改了它的值(假设该值与进程甲修改的值不同,这个概率极大),然后使用它,执行完毕。进程甲恢复执行,使用了它的值,执行完毕。这个时候进程甲使用的值是已经被乙改变了的值,得到的结果和预期不符。

对于该问题的解决方案是控制访问全局变量的方式。如当某个进程访问一个全局变量时,只有当该进程执行完毕,其他进程才能访问该变量。

竞争条件是多个进程或者线程读写数据时,最终结果取决于多个进程指令的执行顺序。

并发设计和管理相关问题:1、OS 需跟踪不同进程,2、OS 需要为每个活动进程分配和释放资源,3、OS 需保护每个进程的数据和使用中的资源免受其他进程的干扰,4、一个进程的功能和结果需和执行速度无关。

根据进程之间是否知道对方的存在的程度,可以将进程之间的交互分为三种:1.进程之间不知道对方的存在、2.进程间接知道对方的存在、3.进程直接知道对方的存在。

第一种情况下,多个进程竞争同一个资源时,会发生冲突。一个进程的执行会影响到与之竞争的进程的行为(主要是对资源的访问)。竞争相关的控制问题有三个:互斥、死锁和饥饿。第三种情况下,相关的控制问题有互斥、死锁、饥饿、数据完整性和数据一致性(主要是对共享数据的访问)。第三种情况下,进程之间通过通信合作,相关的控制问题有死锁和饥饿。

支持互斥需要满足的要求

1.多个进程的临界区访问同一个资源或者共享对象时,在任何时刻只允许一个进程进入临界区。

2.在非临界区停止的进程不能妨碍其他进程。

3.不允许需要进入临界区的进程无限延迟。

4.没有进程处于临界区中时,需要进入临界区的进程可以立刻进入临界区。

5.对相关进程的执行速度和处理器的数量没有限制。

6.进程驻留在临界区中的时间是有限的。

互斥:硬件支持

对于单处理器系统,在进入临界区时启用禁用中断功能,保证进入临界区中的进程不会被中断。该进程从临界区出来之后,启用中断。这个方法可以保证进程访问临界区是互斥的。

对于单处理器系统或者多处理器系统,在一个指令周期内,保证两个动作具有原子性。如对某个内存单元进行读和写或者读和测试。

/*
** 如果内存单元中的值和测试值相等,更新内存中的值,返回内存中原先的值
*/
int compareAndSwap(int *word, int test, int new) {
int old = *word;
if (old == test) {
*word = new;
}
return old;
} const int n = /* 进程数量 */;
int bolt; // 当 bolt 为 0 时,进入临界区
void P(int i) {
while (true) {
while (compareAndSwap(bolt, 0, 1) == 1)
;
// 临界区
bolt = 0;
// 其他部分
}
} void main() {
bolt = 0;
// 多个进程并发执行
parbegin(P(1),P(2)...P(n));
}
/*
** 内存和寄存器内容交换
*/
void exchange(int *register, int *memory) {
int temp = *register;
*register = *memory;
*memory = temp;
} const int n;
int bolt; void P(int i) {
while (true) {
int key = 1;
do {
exchange(key, bolt);
} while(key != 0);
// 临界区
bolt = 0;
// 其他部分
}
} void main() {
bolt = 0;
parbegin(P(1),P(2)...P(n));
}

机器指令实施互斥

优点:

1.适用于单处理器或者多处理器的任意数量进程。

2.简单且易于证明。

3.可支持多个临界区。

缺点:

1.使用了忙等待,浪费处理器时间。

2.可能饥饿。

3.可能死锁。

信号量

相关术语

  • 信号量:又称称计数信号量(counting semaphore)或者一般信号量(general semaphore),用于进程之间传递信号的整数值,可进行三种操作:初始化、递减和递增。
  • 二元信号量:值只能为0或1的信号量。
  • 互斥量:类似二元信号量,区别是为其加锁(设置值为0)和解锁(设置值为1)的进程必须是同一个进程。
  • 条件变量:一种数据结构,用于阻塞进程或者线程,直到条件为真。
  • 管程:一种结构,在抽象数据类型中定义了变量、过程和初始化代码。变量只能由结构自身的过程访问,每次仅允许一个进程处于管程中。管程可以有一个等待队列。
  • 事件标志:用于同步的一个内存字,每一位和一个事件相关联,满足条件之前,进程或者线程一直被阻塞。
  • 信箱/消息:两个进程用于交换信息的一种方法,可用于同步。
  • 自旋锁:互斥机制,进程在无限循环中等待锁变量可用。

信号量可当作一个整数值,有如下三种操作:

1.可初始化为非负整数。

2.semWait操作使信号量减1,如果值小于0,则调用semWait的进程阻塞,否则进程继续执行。

3.semSignal操作使信号量加1,如果值小于等于0,则从被semWait操作阻塞的进程队列中选取一个进程解除阻塞。

信号量的三个结论:

1.在进程对信号量执行减1操作之前,无法得知该进程是否会被阻塞。

2.在单处理器系统中,进程对信号量执行加1操作之后,会唤醒一个处于阻塞态的另一个进程,两个进程继续并发执行(多处理器系统中则并行运行),无法得知哪个进程会立即继续执行(加1操作的进程还是被唤醒的进程)。

3.进程对信号量执行加1操作之后,可能有阻塞态进程被唤醒,也可能没有进程被唤醒(此时没有进程处于阻塞态)。

/*
** 信号量
*/
struct semaphore {
int count;
queueType queue;
}; void semWait(semaphore s) {
s.count--;
if (s.count < 0) {
// 阻塞当前进程并插入阻塞队列
}
} void semSingal(semaphore s) {
s.count++;
if (s.count <= 0) {
// 将某个阻塞进程从阻塞队列移除并插入到就绪队列中
}
}
/*
** 二元信号量
*/
struct binarySemaphore {
enum {zero, one} value;
queueType queue;
}; void semWait(semaphore s) {
if (s.value == one) {
s.value = zero;
} else {
// 阻塞当前队列并插入阻塞队列
}
} void semSignal(semaphore s) {
if (s.queue is empty) {
s.value = one;
} else {
// 将某个阻塞进程从阻塞队列移除并插入到就绪队列中
}
}

和信号量关联的阻塞队列采用先进先出方式移除阻塞进程,这种信号量称为强信号量;没有采取策略的称为弱信号量。

/*
** 用信号量解决互斥
** s.count >= 0:s.count是可以执行semWait而不被阻塞的进程(执行期间没有semSingal执行)。
** s.count < 0:被阻塞在s.queue队列中的进程数量。
*/
const int n = /* 进程数 */;
semaphore s = 1; void P(int i) {
while (true) {
semWait(s);
// 临界区
semSignal(s);
// 其他部分
}
} void main() {
parbegin(P(1),P(2)...P(n));
}

生产者和消费者问题:有一个或者多个生产者生产某种数据并放入缓冲区中,有一个消费者从缓冲区中取出数据,每次取一项。在某一时间点,只允许一个生产者向缓冲区放入数据或者消费者取数据。在缓冲区满时,不允许生产者放入数据;缓冲区空时,不允许消费者取数据。

/*
** 假设缓冲区大小是无限的
** in表示生产的数据在缓冲区的索引、out表示将要取出的数据所在的索引
*/ // producer
while (true) {
// 生产 v
b[in] = v;
in++;
} // consumer
while (true) {
while (in <= out) {
w = b[out];
out++;
// 消费 w
}
}
/*
** 二元信号量实现生产者和消费者问题
** 假设缓冲区大小是无限的
*/
int n; // 缓冲区中数据的数量
binary semaphore s = 1, delay = 0; void producer() {
while (true) {
produce();
semWait(s);
append();
n++;
if (n == 1) {
semSiganl(delay);
}
semSingal(s);
}
} void consumer() {
semWait(delay);
while (true) {
semWait(s);
take();
n--;
int m = n;
semSignal(s);
consume();
if (m == 0) {
semWait(delay);
}
}
} void main() {
n = 0;
parbegin(producer, consumer);
}
/*
** 一般信号量解决生产者消费者问题
*/
semaphore n = 0, s = 1; void producer() {
while (true) {
produce();
semWait(s);
append();
semSingal(s);
semSingal(n);
}
} void consumer() {
while (true) {
semWait(n);
semWait(s);
take();
semSignal(s);
consume();
}
} void main() {
parbegin(producer, consumer);
}
/*
** 缓冲区有限的情况下使用一般信号量
*/ // producer
while (true) {
// create v
while ((in + 1) % n == out)
;
b[in] = v;
in = (in + 1) % n;
} // consumer
while (true) {
while (in == out)
;
w = b[out];
out = (out + 1) % n;
// consume w
} const int sizeOfBuffer = /* 缓冲区大小 */;
semaphore s = 1, n = 0, e = sizeOfBuffer; void producer() {
while (true) {
produce();
semWait(e);
semWait(s);
append();
semSingal(s);
semSingal(n);
}
} void consumer() {
while (true) {
semWait(n);
semWait(s);
take();
semSingal(s);
semSignal(e);
consume();
}
} void main() {
parbegin(producer, consumer);
}

信号量的实现

/*
** 硬件实现
*/
semWait(s) {
while (compareAndSwap(s.flag, 0, 1) == 1)
;
s.count--;
if (s.count < 0) {
// 该进程加入s.queue队列,阻塞
}
s.flag = 0;
} semSignal(s) {
while (compareAndSwap(s.flag, 0, 1) == 1)
;
s.count++;
if (s.count <= 0) {
// 将某个阻塞进程放入就绪队列
}
s.flag = 0;
} /*
** 单处理器系统中断实现
*/
semWait(s) {
禁用中断
s.count--;
if (s.count < 0) {
// 该进程加入s.queue队列,阻塞,允许中断
} else {
允许中断
}
} semSignal(s) {
禁用中断
s.count++;
if (s.count <= 0) {
// 将某个阻塞进程放入就绪队列
}
允许中断
}

管程

Haroe 提出的管程(monitor)定义如下:

管程是由一个或者多个过程、一个初始化序列和局部数据组成的软件模块。主要特点:

1.管程中的局部数据只能被管程中定义的过程访问。

2.进程通过调用管程中定义的过程进入管程。

3.任何时候,只能有一个进程处于管程中。

管程通过条件变量支持同步,条件变量包含在管程中,且只能在管程内访问。有两个函数用于操纵条件变量:

1.cwait(s):调用该函数的进程在条件 s 上阻塞,管程现在可被其他进程使用。

2.csignal(s):恢复执行因条件 s 被阻塞的阻塞队列中的一个进程;如果没有阻塞进程,则什么也不做。

/*
** 管程解决生产者消费者问题
*/
monitor boundedbuffer;
char buffer[N];
int nextIn, nextOut;
int count;
cond notFull, notEmpty; void append(char x) {
if (count == N) {
cwait(notFull);
}
buffer[nextIn] = x;
nextIn = (nextIn + 1) % N;
count++;
csignal(notEmpty);
} void take(char x) {
if (count == 0) {
cwait(notEmpty);
}
x = buffer[nextOut];
nextOut = (nextOut + 1) % N;
count--;
csignal(notFull);
} // 初始化块
{
nextIn = 0;
nextOut = 0;
count = 0;
} void producer() {
char x;
while (true) {
produce(x);
append(x);
}
} void consumer() {
char x;
while (true) {
take(x);
consume(x);
}
} void main() {
parbegin(producer, consumer);
}

管程优于信号量的地方在于,所有的同步机制处于管程内部,易于验证同步的正确性,易于检测错误。

Hoare 提出的管程存在两个缺陷:

1.如果产生 csignal 的进程当前没有执行完,需要进行两次切换:阻塞该进程一次,管程可用时一次。

2.与信号相关的进程调度需要非常可靠。如果发出了 csingal 信号,此时另一个进程进入了管程;在发出信号前进程失败,都会造成错误。

Lampson 和 Redell 改进了管程,csignal 被 cnotify 替代。cnotify 表示,当一个管程中的进程执行了 notify(x),则会通知和 x 相关的阻塞队列,此进程继续执行。

/*
** 改进的管程
*/
void append() {
// 第一次进入时检查一次,当阻塞之后恢复时再检查一次
while (count == N) {
cwait(notFull);
}
buffer[nextIn] = x;
nextIn = (nextIn + 1) % N;
count++;
cnotify(notEmpty);
} void take(char x) {
while (count == 0) {
cwait(notEmpty);
}
x = buffer[nextOut];
nextOut = (nextOut + 1) % N;
count--;
cnotify(notFull);
}

可以改进的地方有:

1.给每一个因特定的条件变量阻塞的队列设置一个计时器,当计时器超时,将队列中的阻塞进程调入就绪态,防止长时间得不到处理出现饥饿。

2.当 cnotify 执行时,将特定条件变量相关的队列中的阻塞进程通过广播的方式将它们调入就绪队列。

改进的优点:更加符合模块化的方法。

消息传递

通过一对原语的形式提供功能。

send(destination, message) 将消息传递给 destination 进程。

receive(source, message) 接收来自 source 进程的消息。

根据发送者和接收者在发送/接收消息的时候是否阻塞可分为以下几种情况:

1.阻塞 send 和 receive:发送者和接收者都被阻塞,直到完成消息的传递。又称会合(rendezvous)。

2.不阻塞 send,阻塞 receive:阻塞接收者,不阻塞发送者。

3.不阻塞 send 和 receive:不要求任何一方等待。

消息在传递时确定目标进程的方式分为直接寻址和间接寻址。

直接寻址:destination 为接收进程的 PID,source 为发送进程的 PID 或者在不需要确定具体发送进程时 source 保存接收进程执行完成后的返回值。

间接寻址:发送进程将消息发送到一个临时的消息队列中,接收进程从消息队列中取消息。这个消息队列称为信箱(mailbox)。

间接寻址根据发送者和接收者的数量又可以分为:一对一、一对多、多对一和多对多四种。

可变长消息的格式为消息头和包含具体内容的消息体。消息头包含目的进程和源进程的 PID,消息长度、消息类型和其他控制信息等。

消息在队列中排队原则为 FIFO 或者根据优先级进和出。

/*
** 消息实现互斥
** 当接收到一个消息时,才允许进入临界区。消息充当了令牌的角色。
*/ const int n = /* 进程数量 */; void P(int i) {
message msg;
while (true) {
receive(box, msg);
// 临界区
send(box, msg);
// 其他部分
}
} void main() {
create mailbox box;
send(box, null);
parbegin(P(1),P(2)...P(n));
}
/*
** 消息解决生产者消费者问题
** mayproduce 中为 null 时,生产者进入临界区
*/
const int capacity = /* 缓冲区容量 */; void producer() {
message pmsg;
while (true) {
receive(mayproduce, pmsg);
pmsg = produce();
send(mayconsume, pmsg);
}
} void consumer() {
message cmsg;
while (true) {
receive(mayconsume, cmsg);
consume(cmsg);
send(mayproduce, null);
}
} void main() {
create mailbox mayproduce and mayconsume;
for (int i = 1; i <= capacity; i++) {
send(mayproduce, null);
}
parbegin(producer, consumer);
}

读者/写者问题

读者写者问题:有个共享数据区,被多个进程共享。部分进程仅读取这个数据区中的数据,部分进程仅向这个数据区中写入数据。任意数量的读进程可以同时读;任何时间点仅允许一个写进程在写数据,同时不允许读进程读。

/*
** 读者优先
** 第一个读进程进入临界区,不允许写进程进入;当所有读进程离开临界区,才允许写进程进入
*/
int readcount;
semaphore x = 1, wsem = 1; void reader() {
while (true) {
semWait(x);
readcount++;
if (readcount == 1) {
semWait(wsem);
}
semSignal(x);
read();
semWait(x);
readcount--;
if (readcount == 0) {
semSignal(wsem);
}
semSignal(x);
}
} void writer() {
while (true) {
semWait(wsem);
write();
semSignal(wsem);
}
} void main() {
readcount = 0;
parbegin(reader, writer);
}
/*
** 当写进程想写的时候,有阻止读进程读的机会
*/
int readcount, writecount;
semaphore x = 1, y = 1, z = 1, rsem = 1, wsem = 1; void reader() {
while (true) {
semWait(z); // 多个读进程在此处阻塞
semWait(rsem);
semWait(x);
readcount++;
if (readcount == 1) {
semWait(wsem);
}
semSignal(x);
semSignal(rsem);
semSignal(z); read(); semWait(x);
readcount--;
if (readcount == 0) {
semSignal(wsem);
}
semSignal(x);
}
} void writer() {
while (true) {
semWait(y);
writecount++;
if (writecount == 1) {
semWait(rsem);
}
semSignal(y);
semWait(wsem);
write();
semSignal(wsem);
semWait(y);
writecount--;
if (writecount == 0) {
semSignal(rsem);
}
semSignal(y);
}
} void main() {
readcount = 0;
writecount = 0;
parbegin(reader, writer);
}

OSIDP-并发:互斥和同步-05的更多相关文章

  1. <<操作系统精髓与设计原理>>读书笔记(一) 并发性:互斥与同步(1)

    <<操作系统精髓与设计原理>>读书笔记(一) 并发性:互斥与同步 并发问题是所有问题的基础,也是操作系统设计的基础.并发包括很多设计问题,其中有进程间通信,资源共享与竞争,多个 ...

  2. C++并发编程 互斥和同步

    C++并发编程 异步任务(async) 线程基本的互斥和同步工具类, 主要包括: std::mutex 类 std::recursive_mutex 类 std::timed_mutex 类 std: ...

  3. java并发:线程同步机制之Volatile关键字&原子操作Atomic

    volatile关键字 volatile是一个特殊的修饰符,只有成员变量才能使用它,与Synchronized及ReentrantLock等提供的互斥相比,Synchronized保证了Synchro ...

  4. Python并发编程-线程同步(线程安全)

    Python并发编程-线程同步(线程安全) 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任. 线程同步,线程间协调,通过某种技术,让一个线程访问某些数据时,其它线程不能访问这些数据,直 ...

  5. Java并发编程:同步容器

    Java并发编程:同步容器 为了方便编写出线程安全的程序,Java里面提供了一些线程安全类和并发工具,比如:同步容器.并发容器.阻塞队列.Synchronizer(比如CountDownLatch). ...

  6. 多线程爬坑之路--并发,并行,synchonrized同步的用法

    一.多线程的并发与并行: 并发:多个线程同时都处在运行中的状态.线程之间相互干扰,存在竞争,(CPU,缓冲区),每个线程轮流使用CPU,当一个线程占有CPU时,其他线程处于挂起状态,各线程断续推进. ...

  7. Java并发编程之同步

    1.synchronized 关键字 synchronized 锁什么?锁对象. 可能锁对象包括: this, 临界资源对象,Class 类对象. 1.1 同步方法 synchronized T me ...

  8. 【转】Java并发编程:同步容器

    为了方便编写出线程安全的程序,Java里面提供了一些线程安全类和并发工具,比如:同步容器.并发容器.阻塞队列.Synchronizer(比如CountDownLatch).今天我们就来讨论下同步容器. ...

  9. java多线程中并发集合和同步集合有哪些?区别是什么?

    java多线程中并发集合和同步集合有哪些? hashmap 是非同步的,故在多线程中是线程不安全的,不过也可以使用 同步类来进行包装: 包装类Collections.synchronizedMap() ...

  10. [Java多线程]-并发,并行,synchonrized同步的用法

    一.多线程的并发与并行: 并发:多个线程同时都处在运行中的状态.线程之间相互干扰,存在竞争,(CPU,缓冲区),每个线程轮流使用CPU,当一个线程占有CPU时,其他线程处于挂起状态,各线程断续推进. ...

随机推荐

  1. zabbix5.2+mysql+ubuntu20.4

    服务端 0.初始化机器 1.mysql安装 # apt-get install mysql-server # apt update 根据提示一步一步确认,要求输入的密码是创建管理员的密码 2.安装za ...

  2. Arrays.sort()降序排序

    默认的Java.util包中的Arrays.sort(),可以实现对java中的基本数据类型(byte.char.short.int.long.float.double.boolean)的数组进行升序 ...

  3. 高德地图poi关键字搜索-vue+ant-design

    最近有个需求,需要输入上车点,下车点,然后输入上车点的时候还要在下方显示地图附近的车辆.百度了一波之后,完全莫得头绪,很多代码也都用不了,即便改了之后也不怎么生效.我用的是jeecg-boot.最后静 ...

  4. 如何基于ZK实现高可用架构

    zookeeper设计步骤 设计path   节点的路径 选择znode类型 普通节点.临时节点等 设计znode数据 节点中的数据 设计watch 节点的监听事件以及对应的处理 ZK实现主备切换架构 ...

  5. postgresql 之修改psql log信息级别

    1.修改当前使用的postgresql.conf文件vim /var/lib/pgsql/10/data/postgresql.conf 2.vim 下使用\notice 找到待修改项 client_ ...

  6. JMETHOD

    #include <iostream> #define JMETHOD(type,methodname,arglist) type (*methodname) arglist JMETHO ...

  7. docker方式安装awvs和nessus渗透工具

    docker-compose.yaml文件 version: '2' services: awvsnessus: image: leishianquan/awvs-nessus:v4 environm ...

  8. Harbor离线安装

    一.安装docker-compose 1-1. #安装方式一 curl -SL https://github.com/docker/compose/releases/download/v2.11.2/ ...

  9. JS学习-PromiseWorker

    PromiseWorker PromiseWorker是一个ChromeWorker,调用而不是postMessage()发送消息,而是调用post(),它返回一个Promise. PromiseWo ...

  10. Token过期处理

    Token用于进行接口鉴权,但是Token具有由后端设置的过期时间,当Token过期以后,就无法再请求数据了 项目中后端设置的过期时间为24h,测试时我们可以手动修改token值让Token失效 处理 ...