其他分享
首页 > 其他分享> > 第五章 并发性:互斥和同步

第五章 并发性:互斥和同步

作者:互联网

相关术语
一、硬件对互斥的支持
中断禁用
专用机器指令
CAS指令
Exchange指令
机器指令的缺点
二、基于软件的并发同步机制
信号量
信号量原语定义
有限缓冲区生产-消费问题
信号量的实现
管程
管程的优势
管程方案
消息传递
信息传递原语
发送、接收形式
寻址方式
消息格式
排队原则
有限缓冲区生产-消费问题
读者-写者问题toc

相关术语

术语 解释
原子操作 一个或多个指令的序列,对外是不可分的,没有其他线程能看到其中间状态,此操作也不能被中断
临界区 指的是一个保护共用资源的程序片段,同一临界区不能被多个线程同时访问
临界资源 不可被多个线程同时访问的资源
死锁 两个或以上的线程,每个都在等其他线程做完某些事情而不能继续执行的情况
活锁 两个或以上的线程为了响应其他线程而持续改变自身状态但不做有用工作的情况
互斥 当一个线程在临界区访问共享资源时,其他线程不能进入该临界区的情况
条件竞争 多个线程读写同一共享数据时,结果依赖于他们执行的相对时间的情况
忙等待(自旋等待) 线程在得到临界区访问权限之前,只能继续执行测试变量的指令来得到访问权,除此之外不能做其他事情

一、硬件对互斥的支持

中断禁用

单处理器机器中可用此方法达成互斥,通过禁用中断的方式避免进程被中断,从而避免中断处理程序带来的竞争态。

/*关闭中断*/
/*临界区*/
/*开启中断*/

专用机器指令

CAS指令

cas(compare and swap)是一个原子指令,不接受中断,由比较和交换操作组成:

//版本一 总是返回目标内存的旧值。后续判断返回值与测试值是否相同,相同则表明目标内存单元已被更新
int compare_and_swap(int *word, int testval, int newval){
    int oldval;
    oldval = *word;    //取内存值的副本,这个很关键
    if(oldval == testval){
        *word = newval;
    }
    return oldval;
}
版本二 发生交换返回true,否则返回false
bool compare_and_swap(int *word, int testval, int newval){
    int oldval;
    oldval = *word;    //取内存值的副本,这个很关键
    if(oldval == testval){
        *word = newval;
        return true;
    }
    return false;
}

使用CAS构造临界区(达成互斥)的方法

int target = 0;    //所有线程均可访问
...
//每个线程内
while(!compare_and_swap(&target, 0, 1));    //版本二
/*临界区*/
target = 0;
/*非临界区*/
...

Exchange指令

Exchange原子的交换两个寄存器间、寄存器与内存间的内容。

void exchange(int register, int memory);

使用Exchange构造临界区(达成互斥)的方法

int target = 0;    //所有线程均可访问
...
//每个线程内
int key = 1;
do exchange(key, target)
while(0 != key);
/*临界区*/
target = 0;
/*非临界区*/
...

机器指令的缺点

二、基于软件的并发同步机制

同步机制 说明
信号量 用于进程间传递信号的整数,仅有初始化、递减、递增三种操作,并都是原子的,递减操作可以用于阻塞一个线程,递增可以用于唤醒阻塞线程
二元信号量 只取0、1的信号量
互斥量 类似二元信号量,关键区别在于,加锁的线程与解锁的线程必须是同一线程
条件变量 一种数据类型,用于阻塞线程,直到特定条件为真
管程 在一个抽象数据类型中封装了变量、访问过程、初始化代码。管程的变量只能由管程自己的访问过程(访问临界区)来访问,每次只能有一个进程在其中执行,管程可以有一个线程等待队列
事件标志 作为同步机制的内存字。为标志中每个位关联不同事件,线程等待一个或多个事件时,通过测试标志中的一个或多个位是否设定,来决定线程阻塞或是唤醒
消息/信箱 两个进程用于交换信息的方法,可用于同步
自旋锁 一种互斥机制,锁变量变为可用之前无限循环测试锁是否可用

信号量

可以对信号量进行的操作

信号量原语定义

struct semaphore{
    int count;
    queueType queue;
};

void semWait(semaphore& s){
    s.count--;
    if (s.count < 0){
        /*把当前线程推入队列*/
        /*阻塞当前线程*/
    }
}

void semSignal(semaphore& s){
    s.count++;
    if (s.count <= 0){
        /*从队列中移除一个线程*/
        /*将被移除的线程添加到就绪队列*/
    }
}

信号量的操作函数semWait、semSignal必须作为原子原语实现,后续说明实现方式
信号量分类:
信号量在实现时,使用了队列保存了阻塞的线程,根据线程移出顺序分为

强信号量保证线程不会饥饿,弱信号量无法保证,强信号量也是操作系统提供的典型信号量形势
使用信号量构造临界区(达成互斥)的方法

semaphore s = 1;    //所有线程均可访问
...
semWait(s);
/*临界区*/
semSignal(s);
/*非临界区*/
...

需要保证semWait与semSignal间不会抛出异常也不会返回,假设后面所有相关代码均不抛出异常、不返回

有限缓冲区生产-消费问题

书中有一个利用信号量处理有限缓冲区生产消费问题的代码示例,我添加了注释
producer()与consumer()分别为生产者与消费者线程
条件e表明了缓冲区可用(剩余)大小
条件s用于访问共享缓冲区访问控制,使共享资源在某一时刻仅被一个线程访问
条件n表明缓冲区内数据个数

const int sizeofbufer = /*缓冲区大小*/;
semaphore s = 1, n = 0, e = sizeofbufer;
void producer(){
    while(true){
        produce();       //生产
        semaWait(e);    //e为缓冲区剩余空间大小,剩余空间大于0才可以继续执行,并对缓冲区剩余数目预减
        semaWait(s);    //配合semSignal(s),使共享缓冲区成为临界资源,保证某时刻仅有一个线程访问该资源
        append();         //往共享缓冲区添加数据
        semSignal(s);    //释放临界资源
        semSignal(n);    //n为缓冲区内数据个数,增加数据计数
    }
}

void consumer(){
    while(true){
        semaWait(n);    //n为缓冲区内数据个数,数据个数大于0才可以继续执行,并对缓冲区数据数目预减
        semaWait(s);    //配合semSignal(s),使共享缓冲区成为临界资源,保证某时刻仅有一个线程访问该资源
        take();             //从共享缓冲区取出数据
        semSignal(s);    //释放临界资源
        semSignal(e);    //e为缓冲区剩余大小,增加可用计数
        consume();        //消费
    }
}

信号量的实现

采用硬件的CAS指令实现互斥,来保证信号量操作函数semWait与semSignal的原子性

struct semaphore{
    int flag;
    int count;
    queueType queue;
};

void semWait(semaphore& s){
    while(!compare_and_swap(&s.flag, 0, 1));    //compare_and_swap为版本二
    s.count--;
    if (s.count < 0){
        /*把当前线程推入队列s.queue*/
        /*阻塞当前线程*/
    }
    s.flag = 0;        
}

void semSignal(semaphore& s){
    while(!compare_and_swap(&s.flag, 0, 1));    //compare_and_swap为版本二
    s.count++;
    if (s.count <= 0){
        /*从队列s.queue中移除一个线程*/
        /*将被移除的线程添加到就绪队列*/
    }
    s.flag = 0;        
}

管程

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

管程的优势

管程方案

书上提到了两种管程方案,Hoar与Lampson/Redell方案

两种方案比较:

消息传递

消息传递可以通信也可以进行同步、互斥,可用于分布式系统中

信息传递原语

send(destination, message);    
receive(source, message);

信息传递分为发送与接收两个部分,均是 动作(地址,内容)形式

发送、接收形式

寻址方式

间接寻址方式解除了发、收者间的耦合,有了更多灵活性,发送者与接收者间关系可以是一对一、多对一、一对多或多对多

消息格式

可变长消息典型格式

部分 额外解释

消息类型

目标ID
消息头 源ID

消息长度

控制信息 如消息数目、顺序号、优先级
消息体 消息内容 实际消息

排队原则

先进先出(FIFO)为基本原则,若有紧急消息,可指定消息优先级

有限缓冲区生产-消费问题

使用消息传递构造临界区(达成互斥)的方法

//box内有一个消息
...
message msg;
receive(box, msg);
/*临界区*/
send(box, msg);
/*非临界区*/
...

书中使用消息传递方式,处理有限缓冲区生产消费问题例子,我添加了注释

const int capacity = /*缓冲区大小*/
const int null = /*空消息*/

void producer(){
    message msg;
    while(true){
        receive(mayProduce, msg);    //从信箱mayProduce接收生产信号,没有信息阻塞,有则使可生产个数减少
        msg = produce();            //产生数据
        send(mayConsume, msg);        //向信箱mayConsume发送数据,随后数据个数增加
    }
}

void consumer(){
    message msg;
    while(true){
        receive(mayConsume, msg);    //从信箱mayConsume接收数据,,没有信息阻塞,有则使数据个数减少
        consume(msg);                //消费数据
        send(mayProduce, null);        //向信箱mayProduce发送生产信号,随后可生产个数增加
    }
}

void main(){
    createMailBox(mayProduce);        //mayProduce信箱内消息个数代表可生产个数,用于控制发送节奏
    createMailBox(mayConsume);        //mayConsume信箱用于传递数据,并充当数据队列
    for(int i = 0; i < capacity; i++){
        send(mayProduce, null);        //将mayProduce信箱填capacity个消息,由于send与receive一一对应,数据队列大小也将为capacity
    }
    parbegin(producer, consumer);    //并行执行 producer、consumer
}

读者-写者问题

读写者定义在并发环境中,有读写两种线程操作共享数据,读线程只读、写线程只写。
读写者问题需满足条件如下:



来自为知笔记(Wiz)

标签:并发,int,管程,阻塞,信号量,互斥,临界,第五章,线程
来源: https://www.cnblogs.com/Keeping-Fit/p/14939735.html