Linux C多任务编程(线程篇3)

一、经典同步问题

        1.哲学家就餐问题

        问题概述:有五位哲学家坐在同一张桌子上就餐,桌上只有五只叉子,每俩个哲学家之间放一只叉子。哲学家围在一起先思考,思考中途饿了就会想进餐,而哲学家进餐时必须使用左右的俩只叉子才能进餐,进餐结束后把叉子放回原位,继续思考。

        请问,如何保证哲学家的动作有序进行,而不会出现有人永远拿不到叉子的情况?        

         咳咳咳,题意读懂了吧。

        方案一:        

        我猜大多数人和我一样第一个想法是信号量!信号量的PV操作来解决这个问题。那么我们尝试一下吧

#define N 5                   //哲学家人数
semaphore fork[5]             //信号量初值为1
                              //也就是叉子的个数

void smart_person(int i)      //i 为哲学家编号 0-4
{
    while(TURE)
    {
        think();                     //哲学家思考
        P(fork[i]);                  //去拿左边的叉子
        P(fork[(i + 1) % N]);        //去拿右边的叉子
        eat();                       //进餐
        V(fork[i]);                  //放下左边的叉子
        V(fork[(i + 1) % N]);        //放下右边的叉子
    }
}

         打眼一看,问题好像解决了。那我们来细品一下。如果每个哲学家同时拿了左边的叉子,桌子上没了叉子,没有人拿到右边的叉子,这样每一位哲学家都在等待,即在P(fork[(i + 1) % N])这条语句阻塞,这样就会导致出现死锁的现象。

         方案二:

        既然同时竞争左边的叉子会导致死锁问题,那么我们就给信号量加一个互斥(内心一万只cnm飞过,来啊,干死我,看你还有什么幺蛾子!)吐槽归吐槽,代码还是得老老实实写,来一起看看吧。

#define N 5                   //哲学家人数
semaphore fork[5]             //每个叉子一个信号量,信号量初值为1
semaphore mutex;              //互斥信号量,初值为1

void smart_person(int i)      //i 为哲学家编号 0-4
{
    while(TURE)
    {
        think();                     //哲学家思考
        P(mutex);                    //进入临界区
        P(fork[i]);                  //去拿左边的叉子
        P(fork[(i + 1) % N]);        //去拿右边的叉子
        eat();                       //进餐
        V(fork[i]);                  //放下左边的叉子
        V(fork[(i + 1) % N]);        //放下右边的叉子
        V(mutex);                    //退出临界区
    }
}

  咳咳咳,这次直接强迫以为哲学家进入临界区后,也就是其它哲学家都不能动,都给爷看着。等这位哲学家进餐结束,放下叉子,然后下一位哲学家进餐,其它人继续等。(直接暴力压制,我的地盘我做主,我就让你们五个一次只能一个人吃!)

        

         好了,虽然都能吃上饭了,但我们冷静下来想想,这样一次只能一个哲学家进餐,效率太过于低下,不符合我们程序员追求极致的个性。按道理来说,每次可以满足有俩个哲学家共同进餐的。好,那我们再来!

        方案三:

        既然互斥信号量只能让一个哲学家进餐,那我们换一个!回顾一下上面俩钟办法,方案一的问题出在了哲学家会同时拿起左边的叉子的可能。那我们不用互斥信号量,而是采用分支结构,根据哲学家的编号不同,采取不同的动作。(奇数编号的先拿右边的,后拿左边的,偶数编号的先拿左边的后拿右边的)细品一下,让刚开始拿第一把叉子就竞争,而且只是俩俩竞争,这样没拿到第一把叉子的哲学家就会进入语句阻塞,而不再回头和另外一边的哲学家去竞争第二把叉子,这样第一次拿到叉子的三个哲学家去拿他们各自的第二把叉子,因为只有5把叉子,总会有一个人没拿到第二把,那没拿到的也进入语句阻塞,等待没拿到叉子那边的哲学家放下叉子。而俩位拿到俩把叉子的哲学家就可以大大方方的进餐了。细细品过后感觉没啥毛病,那么就一个字,干!

#define N 5                   //哲学家人数
semaphore fork[5]             //每个叉子一个信号量,信号量初值为1


void smart_person(int i)      //i 为哲学家编号 0-4
{
    while(TURE)
    {
        think();                     //哲学家思考
        if(i % 2 == 0)
        {
            P(fork[i]);                  //去拿左边的叉子
            P(fork[(i + 1) % N]);        //去拿右边的叉子
        }
        else
        {
            P(fork[(i + 1) % N]);        //去拿右边的叉子
            P(fork[i]);                  //去拿左边的叉子
        }

        eat();                       //进餐
        V(fork[i]);                  //放下左边的叉子
        V(fork[(i + 1) % N]);        //放下右边的叉子        
    }
}

        ok,问题解决!需要注意的一点:在 P 操作时,根据哲学家的编号不同,拿起左右两边叉子的顺序不同。另外,V 操作是不需要分支的,因为 V 操作是不会阻塞的。

         方案四:

        既然已经解决了问题,大多数人已经选择下一个了。那这个时候我们来想想,有没有一种可能,还有其它方法也可以来解决这个问题呢。换个角度来想想,三个方案以来,我们的关注点一直在叉子上,没有注意过哲学家,只知道他想吃,没有关注过哲学家什么状态时想要进餐。

        那我们接下来就从哲学家状态角度来思考一下这个问题。哲学家一共有三个状态:思考状态、饥饿状态(试图拿叉子)、进餐状态。那我们要是用一个数组来记录每位哲学家的这三个状态,但一个哲学家只有俩个邻居都没有进餐时,才可以进入进餐状态。仔细再品品,好像没啥毛病吧。那就浅试一下吧,看能不能解决这个问题。

#define N 5                           //哲学家人数
#define LEFT ( i + N - 1 ) % N        //i 的左边邻居编号
#define RIGHT ( i + 1 ) % N           //i 的右边邻居编号

#define THINKING 0                    //思考状态
#define HUNGRY   1                    //饥饿状态
#define EATING   2                    //进餐状态

int state[N];                        //数组记录每个哲学家的状态

semaphore s[5];                       //每个哲学家一个信号量,初始值为0
semaphore mutex;                      //互斥信号量,初始值为1

void test(int i)                      //i 为哲学家编号 0-4
{
    //如果 i 号的两边的邻居都不是进餐状态, 把 i 号哲学家标记为进餐状态
    if(state[i] == HUNGRY && state[LEFT] != EATING && state[RIGHT] != EATING)
    {
        state[i] = EATING;            //俩把叉子到手,进餐状态
        V(s[i]);                      //通知第 i 哲学家可以进餐了
    }
}

//功能: 要么拿到俩把叉子,要么被阻塞起来
void take_forks(int i)                //i 为哲学家编号 0-4
{
    P(mutex);                         //进入临界区
    state[i] = HUNGRY;                //标记哲学家处于饥饿状态
    test(i);                          //尝试获取俩把叉子
    V(mutex);                         //离开临界区
    P(s[i]);                          //没有叉子则阻塞,有财政则继续正常执行
}

//哲学家主代码
void smart_person(int i)              //i 为哲学家编号 0-4
{
    while(TRUE)
    {
         think();                     //思考
         take_forks(i);               //准备拿去叉子吃饭
         eat();                       //就餐
         put_forks(i);                //吃完返回叉子
    }
}

        看到这里能看懂的可以奖励自己一个钟雪高了(开个玩笑),通过上述办法也完美的解决了问题。

         2.读者-写者问题

        问题概述:读者只会读取数据,不会修改数据,而写着即可以读也可以修改数据。

        【读-读】允许:同一时刻,允许多个读者同时读

        【读-写】允许:没有写者时读者才能读,没有读者时写者才能写

        【写-写】互斥:没有其它写者时,写者才能写

        方案一:

        来吧,还是拿信号量来试试吧。 

        信号量 wMutex:控制写操作的互斥信号量,初始值为1

        读者计数 rCount:正在进行读操作的读者个数,初始值为0

        信号量 rCountMutex:控制对 rCount 读者计数器的互斥修改,初始值为1

semaphore wMutex;                      //控制写操作的互斥信号量,初始值为1
semaphore rCountMutex;                 //控制对 rCount 的互斥修改,初始值为1
int rCount = 0;                        //正在进行读操作的读者个数,初始值为0

//写着进餐/线程执行函数
void writer()
{
    while(TURE)
    {
        P(wMtuex);                     //进入临界区
        write();                       
        V(wMtuex);                     //离开临界区
    }
}

//读者进程/线程执行的函数
void reader()
{
    while(TURE)
    {
        P(rCountMutex);               //进入临界区
        if( rCount == 0)
        {
            P(wMutex);                //如果有读者,则阻塞写者
        }
        rCount++;                     //读者计数 +1
        V(rCountMutex);               //离开临界区

        read();                       //读数据

        P(rCountMutex);               //进入临界区
        rCount--;                     //读完数据,准备离开
        if( rCount == 0 )
        {
            V(wMutex);                //最后一个读者离开了,唤醒写者
        }
        V(rCountMutex);               //离开临界区
    }
}

        不知道有几个人绕出来了,上面这种方法,时读者优先的策略,因为只要有读者正在读的状态,后来的读者都可以直接进入,如果读者持续不断进入,则写着会处于饥饿状态。要是一直有读者,那写者可能到死也一直到不了这。小bug小bug,那就下一个来解决吧。

        方案二

        既然读者优先策略会饿死写者,那么换一下写者优先策略。

        写者优先策略:

                1.只要有写者准备写入,写着应尽快执行写操作,后来的读者就必须阻塞。

                2.如果有写者持续不断写入,则读者就处于饥饿。

        在方案一的基础上新增如下变量:

                信号量 rMutex:控制读者进入的互斥信号量,初始值为1

                信号量 wDataMutex:控制写着写操作的互斥信号量,初始值为1

                写者计数 wCount:记录写者数量,初始值为0

                信号量 wCountMutex:控制 wCount 互斥修改,初始值为1

semaphore wMutex;                      //控制写操作的互斥信号量,初始值为1
semaphore rMutex;                      //控制 wCount 互斥修改,初始值为1

semaphore rCountMutex;                 //控制对 rCount 的互斥修改,初始值为1
semaphore wDataMutex;                  //控制写者写操作的互斥信号量,初始值为1

int rCount = 0;                        //正在进行读操作的读者个数,初始值为0
int wCount = 0;                        //正在进行读操作的写者个数,初始值为0

//写着进餐/线程执行函数
void writer()
{
    while(TURE)
    {
        P(wCountMutex);                //进入临界区
        if( wCount == 0 )
        {
            P( rMutex );               //当写一个写者进入,如果有读者则阻塞读者
        }
        wCount++;                      //写者计数 +1
        V(wCountMutex);                //离开临界区

        P(wDataMutex);                 //写者写操作之间互斥,进入临界区
        write();                       //写数据
        V(wDataMutex);                 //离开临界区

        P(wCountMtuex);                //进入临界区
        wCount--;                      //写完数据,准备离开
        if( wCount == 0 )
        {
            V(rMutex);                 //最后一个写者离开了,则唤醒读者
        }   
        V(wCountMtuex);                //离开临界区
    }
}


//读者进程/线程执行的函数
void reader()
{
    while(TURE)
    {
        P(rMutex);                
        P(rCountMutex);               //进入临界区
        if( rCount == 0 )
        {
            P(wDataMutex);            //当第一个读者进入,如果有写者则阻塞写者写操作
        }
        rCount++;                     //读者计数 +1
        V(rCountMutex);               //离开临界区
        V(rMutex);

        read();                       //读数据

        P(rCountMutex);               //进入临界区
        rCount--;                     //读完数据,准备离开
        if( rCount == 0 )
        {
            V(wDataMutex);                //最后一个读者离开了,唤醒阻塞中写者的写操作
        }
        V(rCountMutex);               //离开临界区
    }
}

        是不是又懵了呢,来回顾一下细细品品。这里rMutex的作用,开始有多个读者读数据,它们全部进入读者队列,此时来了一个写者,执行了P(rMutex)之后,后续的读者由于阻塞在rMutex上,都不能再进入读者队列,而写者到来,则可以全部进入写者队列,因此保证了写者优先。

        同时,第一个写者执行了P(rMutex)之后,也不能马上开始写,必须等到所有进入读者队列的读者都执行完读操作,通过V(wDataMutex)唤醒写者的写操作。问题也出现了,如果写者队列中有很多写者,那么读者就会饿死。

        方案三:

        既然读者优先和写者优先策略都会造成饥饿现象,那么我们再薅一下头发,给他们一个公平策略吧。

        公平策略

        1.优先级相同

        2.写者、读者互斥访问

        3.只能一个写者访问临界区

        4.可以有多个读者同时访问临界资源

semaphore rCountMutex;                      //控制对 rCount 的互斥修改,初始值为1
semaphore wCountMutex;                      //控制对 wCount 的互斥修改,初始值为1
semaphore flag;                             //用于实现公平竞争,初始值为1
int rCount = 0;                             //正在进行读操作的读者个数,初始值为0

//写者进程/进程执行的函数
void writer()
{
    while(TRUE)
    {
        P(flag);                           
        P(wDataMutex);                      //写者写操作之间互斥,进入临界区
        write();                            //写数据
        V(wDataMutex);                      //离开临界区      
        V(flag);                     
    }
}

//读者进程/线程执行的函数
void reader()
{
    while(TRUE)
    {
        P(flag);
        P(rCountMutex);                     //进入临界区
        if(rCount == 0)
        {
            P(wDataMutex);                  //当第一个读者进入,如果有写者则阻塞写者写操作
        }
        rCount++;
        V(rCountMutex);                     //离开临界区
        V(flag);           

        read();                             //读数据

        P(rCountMutex);                     //进入临界区
        rCount--;
        if(rCount == 0)
        {
            V(wDataMutex);                  //当没有读者了,则唤醒阻塞中写者的写操作
        }
        V(rCountMutex);                     //离开临界区
    }
}

        咳咳咳,结束啦。不知道你可理解了?我们通过flag这个信号量,就阻止了读者的特殊权限(特殊权限:只要读者到达,就可以进入读者队列)。通过flag的控制,我们可以清晰的看见,假设开始的时候来了一些读者数据,他们全部进入读者队列,此时来了一个写者,执行P(flag)操作,使得后续到来的读者都阻塞再flag上,不能进入读者队列,这会使得读者队列逐渐为空,即rCount减为0。

        这个写者也不能立马开始写(因为此时读者队列不为空),会阻塞在信号量wCountMutex上,读者队列中的读者全部读取结束后,最后一个读者进程执行V(wDataMutex),唤醒刚才的写者,写者则继续开始写操作。

        ok,细品结束,不知道你可回过味了。这一篇就想和大家分享这些,后续会继续对线程的其它问题进行分享。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值