通过哲学家进餐问题学习线程间协作(代码实现以leetcode1226为例)

简介: 通过哲学家进餐问题学习线程间协作(代码实现以leetcode1226为例)

(代码实现以leetcode1226为例)


提到多线程和锁解决问题,就想到了os中哲学家进餐问题。


问题场景


回想该问题产生场景,五个哲学家共用一张圆桌,分别坐在周围的五张椅子上,在圆桌上有五个碗和五只筷子,他们的生活方式是交替的进行思考和进餐。平时,一个哲学家进行思考,饥饿时便试图取用其左右最靠近他的筷子,只有在他拿到两只筷子时才能进餐。进餐完毕,放下筷子继续思考。


解决思路


由于五位哲学家应相互独立,因此可以创建五个线程分别代表五位哲学家,相互独立(异步),依次编号为0~4。


对于资源(筷子),应该认为这是五种临界资源,而不是一种临界资源。因为每位哲学家只能使用自己面前的两个筷子。同样对五只筷子进行资源编号0~4,并且定义第i位哲学家左侧的筷子编号为i,哲学家右侧的筷子编号为(i+1)%5(这里要注意对编号为0的进行特殊处理)。


临界资源的定义,一次只允许一个线程使用的公共资源。所以当一只筷子被一位哲学家使用时应该加锁。


形成一种解决思路如下:

while(1){
    think()
    hungry()
    p(左筷子)
    p(右筷子)
    eat()
    v(左筷子)
    v(右筷子)
}

显而易见这种解决办法会出现死锁问题,即当每位哲学家获取左筷子后,永久陷入了等待右筷子状态,且每个人都不会主动放弃获取的临界资源。


解决死锁问题


①我们可以只允许四个哲学家都拿起同一边的筷子,这样会保证一位哲学家可以得到两边筷子完成进食,释放临界资源,保证别的哲学家可以进行相应的线程。


这里要新增一个信号量,控制拿起同一边筷子的哲学家数量。


修改后的解决思路如下:

while(1){
    think()
    hungry()
    p(还可以拿起一边筷子)
    p(左筷子)
    p(右筷子)
    eat()
    v(左筷子)
    v(拿起一边筷子的名额)
    v(右筷子)
}

②采用AND信号量机制,即当一个线程要执行时一次性将所需的资源全部给他,否则不分给他资源。

新的解决思路如下:

while(1){
  think()
  hungry()
  Swait(左筷子,右筷子)
  eat()
  Spost(左筷子,右筷子)
}

针对不用的代码语言,当不支持and信号量机制时,可以考虑增加新的互斥信号量来实现。全局互斥信号量,当一个哲学家企图拿筷子时候,就将全部资源锁住。

修改后的解决思路如下:

while(1){
  think()
  hungry()
  p(lock)
  p(左筷子)
    p(右筷子)
    v(lock)
    eat()
    v(左筷子)
    v(右筷子)
}

③区分奇数偶数哲学家,规定奇数号哲学家先拿起他左边的筷子,然后再去拿他右边的筷子,而偶数号的哲学家则相反,这样的话总能保证一个哲学家能获得两根筷子完成进餐,从而释放其所占用的资源。

解决思路如下:

while(1){
  think(i)
  hungry(i)
  if(i % 2 == 0){
        p(左筷子)
        p(右筷子)
        eat()
        v(右筷子)
        v(左筷子)
    }
    else{
      p(右筷子)
      p(左筷子)
        eat()
        v(左筷子)
        v(右筷子)
    }
}


代码实现


c++


①只允许四个哲学家都拿起同一边的筷子 room信号量4

#include<semaphore.h>
class DiningPhilosophers {
public:
    DiningPhilosophers() {
        fork = vector<sem_t>(5);
        sem_init(&room,0,4);
        for(int i = 0;i < 5;i++){
            sem_init(&(fork[i]),0,1);
        }
    }
    void wantsToEat(int philosopher,
                    function<void()> pickLeftFork,
                    function<void()> pickRightFork,
                    function<void()> eat,
                    function<void()> putLeftFork,
                    function<void()> putRightFork) {
        sem_wait(&room);
        sem_wait(&(fork[philosopher]));
        sem_wait(&(fork[(philosopher+1)%5]));
        pickLeftFork();
        pickRightFork();
        eat();
        putRightFork();
        putLeftFork();
        sem_post(&(fork[(philosopher+1)%5]));
        sem_post(&(fork[philosopher]));
        sem_post(&room);
    }
    vector<sem_t> fork;
    sem_t room;
};

②AND信号量方式

#include<semaphore.h>
class DiningPhilosophers {
public:
    DiningPhilosophers() {
        fork = vector<sem_t>(5);
        for(int i = 0;i < 5;i++){
            sem_init(&(fork[i]),0,1);
        }
        sem_init(&mutex,0,1);
    }
    void wantsToEat(int philosopher,
                    function<void()> pickLeftFork,
                    function<void()> pickRightFork,
                    function<void()> eat,
                    function<void()> putLeftFork,
                    function<void()> putRightFork) {
        sem_wait(&mutex);
        sem_wait(&(fork[philosopher]));
        sem_wait(&(fork[(philosopher+1)%5]));
        pickLeftFork();
        pickRightFork();
        sem_post(&mutex);
        eat();
        putRightFork();
        putLeftFork();
        sem_post(&(fork[(philosopher+1)%5]));
        sem_post(&(fork[philosopher]));
    }
    vector<sem_t> fork;
    sem_t mutex;
};

③奇偶数

#include<semaphore.h>
class DiningPhilosophers {
public:
    DiningPhilosophers() {
        fork = vector<sem_t>(5);
        for(int i = 0;i < 5;i++){
            sem_init(&(fork[i]),0,1);
        }
    }
    void wantsToEat(int philosopher,
                    function<void()> pickLeftFork,
                    function<void()> pickRightFork,
                    function<void()> eat,
                    function<void()> putLeftFork,
                    function<void()> putRightFork) {
        if(philosopher % 2 == 0){
            sem_wait(&(fork[philosopher]));
            sem_wait(&(fork[(philosopher+1)%5]));
            pickLeftFork();
            pickRightFork();
            eat();
            putRightFork();
            putLeftFork();
            sem_post(&(fork[(philosopher+1)%5]));
            sem_post(&(fork[philosopher]));
        }
        else{
            sem_wait(&(fork[(philosopher+1)%5]));
            sem_wait(&(fork[philosopher]));
            pickLeftFork();
            pickRightFork();
            eat();
            putRightFork();
            putLeftFork();
            sem_post(&(fork[philosopher]));
            sem_post(&(fork[(philosopher+1)%5]));
        }
    }
    vector<sem_t> fork;
};


go


奇偶数

package main
import (
  "fmt"
  "sync"
)
type DiningPhilosophers struct {
  getkey    []chan int
  endsSgnal *sync.WaitGroup
}
func pickLeftFork(i int) {
  fmt.Printf("philosopher %d pickLeftFork\n", i)
}
func pickRightFork(i int) {
  fmt.Printf("philosopher %d pickRightFork\n", i)
}
func eat(i int) {
  fmt.Printf("philosopher %d eat\n", i)
}
func putLeftFork(i int) {
  fmt.Printf("philosopher %d putLeftFork\n", i)
}
func putRightFork(i int) {
  fmt.Printf("philosopher %d putRightFork\n", i)
}
func (s *DiningPhilosophers) wantsToEat(philosopher int, pickLeftFork func(int), pickRightFork func(int), eat func(int), putLeftFork func(int), putRightFork func(int)) {
  if philosopher%2 == 0 {
    <-s.getkey[philosopher]
    pickLeftFork(philosopher)
    <-s.getkey[(philosopher+1)%5]
    pickRightFork(philosopher)
    eat(philosopher)
    putRightFork(philosopher)
    s.getkey[(philosopher+1)%5] <- 1
    putLeftFork(philosopher)
    s.getkey[philosopher] <- 1
  } else {
    <-s.getkey[(philosopher+1)%5]
    pickRightFork(philosopher)
    <-s.getkey[philosopher]
    pickLeftFork(philosopher)
    eat(philosopher)
    putLeftFork(philosopher)
    s.getkey[philosopher] <- 1
    putRightFork(philosopher)
    s.getkey[(philosopher+1)%5] <- 1
  }
  s.endsSgnal.Done()
}
func main() {
  s := &DiningPhilosophers{
    getkey:    make([]chan int, 5),
    endsSgnal: &sync.WaitGroup{},
  }
  for i := 0; i < len(s.getkey); i++ {
    s.getkey[i] = make(chan int, 1)
    s.getkey[i] <- 1
  }
  n := 100
  for i := 0; i < n; i++ {
    s.endsSgnal.Add(5)
    go s.wantsToEat(0, pickLeftFork, pickRightFork, eat, putLeftFork, putRightFork)
    go s.wantsToEat(1, pickLeftFork, pickRightFork, eat, putLeftFork, putRightFork)
    go s.wantsToEat(2, pickLeftFork, pickRightFork, eat, putLeftFork, putRightFork)
    go s.wantsToEat(3, pickLeftFork, pickRightFork, eat, putLeftFork, putRightFork)
    go s.wantsToEat(4, pickLeftFork, pickRightFork, eat, putLeftFork, putRightFork)
  }
  s.endsSgnal.Wait()
}
相关文章
|
3月前
|
安全 Python
告别低效编程!Python线程与进程并发技术详解,让你的代码飞起来!
【7月更文挑战第9天】Python并发编程提升效率:**理解并发与并行,线程借助`threading`模块处理IO密集型任务,受限于GIL;进程用`multiprocessing`实现并行,绕过GIL限制。示例展示线程和进程创建及同步。选择合适模型,注意线程安全,利用多核,优化性能,实现高效并发编程。
60 3
|
2月前
|
Java 开发者
解锁并发编程新姿势!深度揭秘AQS独占锁&ReentrantLock重入锁奥秘,Condition条件变量让你玩转线程协作,秒变并发大神!
【8月更文挑战第4天】AQS是Java并发编程的核心框架,为锁和同步器提供基础结构。ReentrantLock基于AQS实现可重入互斥锁,比`synchronized`更灵活,支持可中断锁获取及超时控制。通过维护计数器实现锁的重入性。Condition接口允许ReentrantLock创建多个条件变量,支持细粒度线程协作,超越了传统`wait`/`notify`机制,助力开发者构建高效可靠的并发应用。
77 0
|
1月前
|
监控 Java 调度
【Java学习】多线程&JUC万字超详解
本文详细介绍了多线程的概念和三种实现方式,还有一些常见的成员方法,CPU的调动方式,多线程的生命周期,还有线程安全问题,锁和死锁的概念,以及等待唤醒机制,阻塞队列,多线程的六种状态,线程池等
105 6
【Java学习】多线程&JUC万字超详解
|
2月前
|
Java Windows
【Azure Developer】Windows中通过pslist命令查看到Java进程和线程信息,但为什么和代码中打印出来的进程号不一致呢?
【Azure Developer】Windows中通过pslist命令查看到Java进程和线程信息,但为什么和代码中打印出来的进程号不一致呢?
|
2月前
|
Java 开发者
解锁Java并发编程的秘密武器!揭秘AQS,让你的代码从此告别‘锁’事烦恼,多线程同步不再是梦!
【8月更文挑战第25天】AbstractQueuedSynchronizer(AQS)是Java并发包中的核心组件,作为多种同步工具类(如ReentrantLock和CountDownLatch等)的基础。AQS通过维护一个表示同步状态的`state`变量和一个FIFO线程等待队列,提供了一种高效灵活的同步机制。它支持独占式和共享式两种资源访问模式。内部使用CLH锁队列管理等待线程,当线程尝试获取已持有的锁时,会被放入队列并阻塞,直至锁被释放。AQS的巧妙设计极大地丰富了Java并发编程的能力。
34 0
|
3月前
|
安全 Java API
Java并发编程的艺术:解锁多线程同步与协作的秘密
【7月更文挑战第28天】在Java的世界中,并发编程如同一场精心编排的交响乐,每一个线程都是乐团中的乐手,而同步机制则是那指挥棒,确保旋律的和谐与统一。本文将深入探讨Java并发编程的核心概念,包括线程的创建、同步机制、以及线程间的通信方式,旨在帮助读者解锁Java多线程编程的秘密,提升程序的性能和响应性。
38 3
|
3月前
|
安全 Java 数据处理
Java并发编程:线程同步与协作的深度解析
在探索Java并发编程的海洋中,线程同步与协作的灯塔指引着航向。本文将深入挖掘线程同步机制的核心原理,揭示锁、条件变量等工具如何确保数据的一致性和线程间有序的通信。通过案例分析,我们将解码高效并发模式背后的设计哲学,并探讨现代Java并发库如何简化复杂的同步任务。跟随文章的步伐,您将获得提升多线程应用性能与可靠性的关键技能。 【7月更文挑战第24天】
36 5
|
4月前
|
Java
【代码诗人】Java线程的生与死:一首关于生命周期的赞歌!
【6月更文挑战第19天】Java线程生命周期,如诗般描绘了从新建到死亡的旅程:创建后待命,`start()`使其就绪,获得CPU则运行,等待资源则阻塞,任务完或中断即死亡。理解生命周期,善用锁、线程池,优雅处理异常,确保程序高效稳定。线程管理,既是艺术,也是技术。
27 3
|
4月前
|
Java
【代码诗人】Java线程的生与死:一首关于生命周期的赞歌!
【6月更文挑战第19天】在Java中,线程经历新建、就绪、运行、阻塞和死亡5个阶段。通过`start()`从新建转为就绪,进而可能运行;阻塞可能因等待资源;完成任务或中断后死亡。管理线程生命周期涉及合理使用锁、线程池、异常处理和优雅关闭,如使用`volatile`和中断标志。了解这些,能提升程序效率和稳定性。
25 2
|
4月前
|
NoSQL Redis
Redis系列学习文章分享---第五篇(Redis实战篇--优惠券秒杀,全局唯一id 添加优惠券 实现秒杀下单 库存超卖问题分析 乐观锁解决超卖 实现一人一单功能 集群下的线程并发安全问题)
Redis系列学习文章分享---第五篇(Redis实战篇--优惠券秒杀,全局唯一id 添加优惠券 实现秒杀下单 库存超卖问题分析 乐观锁解决超卖 实现一人一单功能 集群下的线程并发安全问题)
105 0