Golang实现的哲学家就餐问题变体

huangapple go评论82阅读模式
英文:

Golang implementation of dining philosophers variant problem

问题

我想实现一个变种的经典餐厅哲学家问题,其定义如下:

使用以下约束/修改实现餐厅哲学家问题。

  • 有5个哲学家共享筷子,每个相邻的哲学家之间有一根筷子。
  • 每个哲学家只能吃3次(不像我们在讲座中那样无限循环)。
  • 哲学家可以以任意顺序拿起筷子(不像我们在讲座中那样按最低编号优先)。
  • 为了吃饭,哲学家必须得到一个在自己的goroutine中执行的主持人的许可。
  • 主持人最多允许2个哲学家同时就餐。
  • 每个哲学家都有一个编号,从1到5。
  • 当一个哲学家开始吃饭(在获得必要的锁之后)时,他会单独打印一行“starting to eat”,其中是哲学家的编号。
  • 当一个哲学家吃完饭(在释放锁之前)时,他会单独打印一行“finishing eating”,其中是哲学家的编号。

我实现了以下代码:

package main

import (
	"fmt"
	"sync"
)

// 定义变量
var numPhilo int = 5
var numCS int = 5
var eatTimes int = 3
var numEatingPhilo int = 2

type Host struct {
	// 允许哲学家就餐的通道
	requestChannel chan *Philo
	// 守护进程终止信号的通道
	quitChannel chan int
	// 当前就餐的哲学家的记录
	eatingPhilos map[int]bool
	// 修改eatingPhilos变量的互斥锁
	mu sync.Mutex
}

// 管理允许就餐的哲学家的守护进程函数
func (pHost *Host) manage() {
	// 守护进程在后台运行,只有在接收到终止信号时才退出
	for {
		// 如果通道已满,则释放队列头部的哲学家
		if len(pHost.requestChannel) == numEatingPhilo {
			finished := <-pHost.requestChannel
			currEating := make([]int, 0, numPhilo)
			for tmpIdx, eating := range pHost.eatingPhilos {
				if eating {
					currEating = append(currEating, tmpIdx)
				}
			}
			fmt.Printf("%v 正在就餐,清理 %d\n", currEating, finished.idx)
			pHost.eatingPhilos[finished.idx] = false
		}

		select {
		case <-pHost.quitChannel:
			fmt.Println("停止主持")
			return
		default:

		}
	}
}

type ChopS struct {
	mu sync.Mutex
}

type Philo struct {
	// 哲学家的索引
	idx int
	// 哲学家就餐的次数
	numEat          int
	leftCS, rightCS *ChopS
	host            *Host
}

func (pPhilo *Philo) eat(wg *sync.WaitGroup) {
	for pPhilo.numEat < eatTimes {

		// 当哲学家打算就餐时,锁定相应的筷子
		pPhilo.leftCS.mu.Lock()
		pPhilo.rightCS.mu.Lock()

		// 在通道中预留一个位置用于就餐
		// 如果通道缓冲区已满,将被阻塞,直到通道空间被释放
		pPhilo.host.requestChannel <- pPhilo
		pPhilo.host.mu.Lock()
		pPhilo.host.eatingPhilos[pPhilo.idx] = true
		pPhilo.host.mu.Unlock()

		pPhilo.numEat++
		fmt.Printf("开始就餐 %d,第 %d 次\n", pPhilo.idx, pPhilo.numEat)
		fmt.Printf("结束就餐 %d,第 %d 次\n", pPhilo.idx, pPhilo.numEat)

		pPhilo.rightCS.mu.Unlock()
		pPhilo.leftCS.mu.Unlock()
		wg.Done()
	}
}

func main() {
	var wg sync.WaitGroup
	requestChannel := make(chan *Philo, numEatingPhilo)
	quitChannel := make(chan int, 1)
	host := Host{requestChannel: requestChannel, quitChannel: quitChannel, eatingPhilos: make(map[int]bool)}
	CSticks := make([]*ChopS, numCS)
	for i := 0; i < numCS; i++ {
		CSticks[i] = new(ChopS)

	}
	philos := make([]*Philo, numPhilo)
	for i := 0; i < numPhilo; i++ {
		philos[i] = &Philo{idx: i + 1, numEat: 0, leftCS: CSticks[i], rightCS: CSticks[(i+1)%5], host: &host}
	}

	go host.manage()

	wg.Add(numPhilo * eatTimes)
	for i := 0; i < numPhilo; i++ {
		go philos[i].eat(&wg)
	}
	wg.Wait()
	host.quitChannel <- 1
}

然而,我注意到程序在某些情况下实际上会失败,例如:

开始就餐 5,第 1 次
结束就餐 5,第 1 次
开始就餐 2,第 1 次
结束就餐 2,第 1 次
[5 2] 正在就餐,清理 5
[2] 正在就餐,清理 2
[] 正在就餐,清理 5
开始就餐 5,第 2 次
结束就餐 5,第 2 次
开始就餐 5,第 3 次
结束就餐 5,第 3 次
[5 2] 正在就餐,清理 2
[5] 正在就餐,清理 5
开始就餐 2,第 2 次
结束就餐 2,第 2 次
[4] 正在就餐,清理 4
开始就餐 4,第 1 次
结束就餐 4,第 1 次
[] 正在就餐,清理 2
开始就餐 4,第 2 次
结束就餐 4,第 2 次
[4] 正在就餐,清理 4
开始就餐 4,第 3 次
结束就餐 4,第 3 次
开始就餐 2,第 3 次
结束就餐 2,第 3 次
开始就餐 1,第 1 次
结束就餐 1,第 1 次
[2 4 1] 正在就餐,清理 4
[2 1] 正在就餐,清理 1
[2] 正在就餐,清理 3
开始就餐 1,第 2 次
结束就餐 1,第 2 次
[1 2] 正在就餐,清理 1
开始就餐 3,第 1 次
结束就餐 3,第 1 次
开始就餐 3,第 2 次
结束就餐 3,第 2 次
[3 2] 正在就餐,清理 1
[2 3] 正在就餐,清理 3
开始就餐 3,第 3 次
结束就餐 3,第 3 次
开始就餐 1,第 3 次
结束就餐 1,第 3 次
停止主持

有时候似乎哲学家无法同时就餐,并且根据日志,记录哲学家的映射表行为异常。

请问有人能指出实现的哪个部分不正确吗?有什么明显的错误或不良实践吗?

英文:

I would like to implement a variant of the classical dining philosophers problem which has the definition as:

Implement the dining philosopher’s problem with the following constraints/modifications.

  • There should be 5 philosophers sharing chopsticks, with one chopstick between each adjacent pair of philosophers.
  • Each philosopher should eat only 3 times (not in an infinite loop as we did in lecture)
  • The philosophers pick up the chopsticks in any order, not lowest-numbered first (which we did in lecture).
  • In order to eat, a philosopher must get permission from a host which executes in its own goroutine.
  • The host allows no more than 2 philosophers to eat concurrently.
  • Each philosopher is numbered, 1 through 5.
  • When a philosopher starts eating (after it has obtained necessary locks) it prints “starting to eat ” on a line by itself, where is the number of the philosopher.
  • When a philosopher finishes eating (before it has released its locks) it prints “finishing eating ” on a line by itself, where is the number of the philosopher.

I implemented the following code:

package main

import (
	&quot;fmt&quot;
	&quot;sync&quot;
)

// define variables
var numPhilo int = 5
var numCS int = 5
var eatTimes int = 3
var numEatingPhilo int = 2

type Host struct {
	// channel for allowed philosopher for eating
	requestChannel chan *Philo
	// channel for terminate signal for the daemon
	quitChannel chan int
	// bookkeeping of the current eating philosophers
	eatingPhilos map[int]bool
	// mutex to lock the modification of the eatingPhilos variable
	mu sync.Mutex
}

// daemon function to manage the allowed philosophers
func (pHost *Host) manage() {
	// daemon serving in the backend and only exits for terminate signal
	for {
		// if the channel is full, release the head of the queue
		if len(pHost.requestChannel) == numEatingPhilo {
			finished := &lt;-pHost.requestChannel
			currEating := make([]int, 0, numPhilo)
			for tmpIdx, eating := range pHost.eatingPhilos {
				if eating {
					currEating = append(currEating, tmpIdx)
				}
			}
			fmt.Printf(&quot;%v have been eating, clearing up %d\n&quot;, currEating, finished.idx)
			pHost.eatingPhilos[finished.idx] = false
		}

		select {
		case &lt;-pHost.quitChannel:
			fmt.Println(&quot;stop hosting&quot;)
			return
		default:

		}
	}
}

type ChopS struct {
	mu sync.Mutex
}

type Philo struct {
	// index of the philosopher
	idx int
	// number of times the philosopher has eaten
	numEat          int
	leftCS, rightCS *ChopS
	host            *Host
}

func (pPhilo *Philo) eat(wg *sync.WaitGroup) {
	for pPhilo.numEat &lt; eatTimes {

		// once the philosopher intends to eat, lock the corresponding chopsticks
		pPhilo.leftCS.mu.Lock()
		pPhilo.rightCS.mu.Lock()

		// reserve a slot in the channel for eating
		// if channel buffer is full, this is blocked until channel space is released
		pPhilo.host.requestChannel &lt;- pPhilo
		pPhilo.host.mu.Lock()
		pPhilo.host.eatingPhilos[pPhilo.idx] = true
		pPhilo.host.mu.Unlock()

		pPhilo.numEat++
		fmt.Printf(&quot;starting to eat %d for %d time\n&quot;, pPhilo.idx, pPhilo.numEat)
		fmt.Printf(&quot;finishing eating %d for %d time\n&quot;, pPhilo.idx, pPhilo.numEat)

		pPhilo.rightCS.mu.Unlock()
		pPhilo.leftCS.mu.Unlock()
		wg.Done()
	}
}

func main() {
	var wg sync.WaitGroup
	requestChannel := make(chan *Philo, numEatingPhilo)
	quitChannel := make(chan int, 1)
	host := Host{requestChannel: requestChannel, quitChannel: quitChannel, eatingPhilos: make(map[int]bool)}
	CSticks := make([]*ChopS, numCS)
	for i := 0; i &lt; numCS; i++ {
		CSticks[i] = new(ChopS)

	}
	philos := make([]*Philo, numPhilo)
	for i := 0; i &lt; numPhilo; i++ {
		philos[i] = &amp;Philo{idx: i + 1, numEat: 0, leftCS: CSticks[i], rightCS: CSticks[(i+1)%5], host: &amp;host}
	}

	go host.manage()

	wg.Add(numPhilo * eatTimes)
	for i := 0; i &lt; numPhilo; i++ {
		go philos[i].eat(&amp;wg)
	}
	wg.Wait()
	host.quitChannel &lt;- 1
}

However, I noticed that the program is actually failing in some cases, i.e.

starting to eat 5 for 1 time
finishing eating 5 for 1 time
starting to eat 2 for 1 time
finishing eating 2 for 1 time
[5 2] have been eating, clearing up 5
[2] have been eating, clearing up 2
[] have been eating, clearing up 5
starting to eat 5 for 2 time
finishing eating 5 for 2 time
starting to eat 5 for 3 time
finishing eating 5 for 3 time
[5 2] have been eating, clearing up 2
[5] have been eating, clearing up 5
starting to eat 2 for 2 time
finishing eating 2 for 2 time
[4] have been eating, clearing up 4
starting to eat 4 for 1 time
finishing eating 4 for 1 time
[] have been eating, clearing up 2
starting to eat 4 for 2 time
finishing eating 4 for 2 time
[4] have been eating, clearing up 4
starting to eat 4 for 3 time
finishing eating 4 for 3 time
starting to eat 2 for 3 time
finishing eating 2 for 3 time
starting to eat 1 for 1 time
finishing eating 1 for 1 time
[2 4 1] have been eating, clearing up 4
[2 1] have been eating, clearing up 1
[2] have been eating, clearing up 3
starting to eat 1 for 2 time
finishing eating 1 for 2 time
[1 2] have been eating, clearing up 1
starting to eat 3 for 1 time
finishing eating 3 for 1 time
starting to eat 3 for 2 time
finishing eating 3 for 2 time
[3 2] have been eating, clearing up 1
[2 3] have been eating, clearing up 3
starting to eat 3 for 3 time
finishing eating 3 for 3 time
starting to eat 1 for 3 time
finishing eating 1 for 3 time
stop hosting

where sometimes it seems the philosophers cannot eat concurrently, and also according to the logs, the bookkeeping map is acting weird.

Could someone please point out which part of the implementation is improper? Anything obviously wrong or bad practice?

答案1

得分: 1

你在这里发布的代码存在数据竞争问题(Host.eatingPhilos在没有任何保护的情况下从多个Go协程中访问)。在在code review上发布问题之前,你解决了这些问题,这在某种程度上解决了问题(但引入了其他问题)。

我在code review上对此进行了详细回答,并提供了一些建议等。由于你在那里发布的代码与这里的代码不同,所以重复回答没有太大意义。然而,我将包含我的建议解决方案,因为它采用了与你采用的方法显著不同的方法(为了解决一些逻辑问题)。请注意,这个解决方案采用了一个简单的方法来解决“饥饿哲学家”问题(每个哲学家只有一根筷子,所以没有人可以拿到第二根)。参见Wikipedia页面了解其他更好的解决方案(但我想你想使用Go协程)。

警告 - 下面的代码可能包含错误!

Playground

package main

import (
	"fmt"
	"math/rand"
	"sync"
	"time"

	"golang.org/x/exp/slices"
)

const (
	numPhilo       = 5
	eatTimes       = 3
	numEatingPhilo = 2
)

type eatRequest struct {
	who            int         // 请求者的编号
	finishedFnChan chan func() // 当批准请求时,将在此通道上发送一个带有完成时调用的函数
}

// simulateHost - 主持人必须在哲学家吃饭之前提供许可
// 当通道关闭时退出
func simulateHost(requestChannel <-chan eatRequest) {
	awaitRequest := requestChannel
	finishedChan := make(chan struct {
		who  int
		done chan struct{}
	})

	var whoEating []int // 跟踪当前正在吃饭的人

	for {
		select {
		case request, ok := <-awaitRequest:
			if !ok {
				return // 通道关闭意味着我们完成了(finishedChan保证为空)
			}
			// 检查一下 - 确认哲学家不贪心!(永远不应该发生)
			if slices.Index(whoEating, request.who) != -1 {
				panic("同一个哲学家发出多个请求")
			}
			whoEating = append(whoEating, request.who) // 新请求总是放在最后
			fmt.Printf("%d 开始吃饭(当前正在吃饭的人:%v)\n", request.who, whoEating)

			// 告诉哲学家,并提供他们在完成后告诉我们的方法
			request.finishedFnChan <- func() {
				d := make(chan struct{})
				finishedChan <- struct {
					who  int
					done chan struct{}
				}{who: request.who, done: d}
				<-d // 等待请求被处理(确保我们永远不会有两个来自同一个哲学家的活动请求)
			}
		case fin := <-finishedChan:
			idx := slices.Index(whoEating, fin.who)
			if idx == -1 {
				panic("哲学家多次停止吃饭!")
			}
			whoEating = append(whoEating[:idx], whoEating[idx+1:]...) // 删除元素
			fmt.Printf("%d 完成吃饭(当前正在吃饭的人:%v)\n", fin.who, whoEating)
			close(fin.done)
		}
		// 哲学家吃饭的人数发生了变化
		if len(whoEating) < numEatingPhilo {
			awaitRequest = requestChannel
		} else {
			awaitRequest = nil // 忽略新的吃饭请求,直到有哲学家吃完(nil通道永远不会被选中)
		}
	}
}

// ChopS 表示一根筷子
type ChopS struct {
	mu  sync.Mutex
	idx int // 包括索引可以使调试更简单
}

// philosopher 模拟一个哲学家(大脑在一个容器中!)
func philosopher(philNum int, leftCS, rightCS *ChopS, requestToEat chan<- eatRequest) {
	for numEat := 0; numEat < eatTimes; numEat++ {
		// 一旦哲学家打算吃饭,锁定相应的筷子
		for {
			leftCS.mu.Lock()
			// 尝试获取右边的筷子 - 如果有人拿着,我们替换左边的筷子并重试
			// (以避免死锁)
			if rightCS.mu.TryLock() {
				break
			}
			leftCS.mu.Unlock()
		}

		// 我们有筷子,但需要主持人的许可
		ffc := make(chan func()) // 当被接受时,我们将收到一个完成吃饭时调用的函数
		requestToEat <- eatRequest{
			who:            philNum,
			finishedFnChan: ffc,
		}
		doneEating := <-ffc

		fmt.Printf("哲学家 %d 开始吃饭(第 %d 次)\n", philNum, numEat)
		time.Sleep(time.Millisecond * time.Duration(rand.Intn(200))) // 吃饭需要随机的时间
		fmt.Printf("哲学家 %d 完成吃饭(第 %d 次)\n", philNum, numEat)

		rightCS.mu.Unlock()
		leftCS.mu.Unlock()
		doneEating() // 告诉主持人我们吃完了
	}
	fmt.Printf("哲学家 %d 已经吃饱了\n", philNum)
}

func main() {
	CSticks := make([]*ChopS, numPhilo)
	for i := 0; i < numPhilo; i++ {
		CSticks[i] = &ChopS{idx: i}
	}

	requestChannel := make(chan eatRequest)

	var wg sync.WaitGroup
	wg.Add(numPhilo)
	for i := 0; i < numPhilo; i++ {
		go func(philNo int) {
			philosopher(philNo, CSticks[philNo-1], CSticks[philNo%numPhilo], requestChannel)
			wg.Done()
		}(i + 1)
	}

	go func() {
		wg.Wait()
		close(requestChannel)
	}()

	simulateHost(requestChannel)
}
英文:

The code you posted here contains data races (Host.eatingPhilos is accessed from multiple go-routines without any protection). You resolved these before posting your question on code review which went some way towards a working solution (but introduced other issues).

I answered this fully on code review and provided some feedback etc. As the code you posted there differs from what is here there is little point in duplicating the answer. However, I will include my suggested solution because it adopts a significantly different approach to that you took (in order to work around a few logic issues). Note that this solution takes an easy way out of the "starving philosopher" issue (each philosopher has one chopstick, so none can get a second). See the Wikipedia page for some other, better, solutions (but I figure you wanted to use go routines).

warning - the below may well contain bugs!

Playground

package main

import (
	&quot;fmt&quot;
	&quot;math/rand&quot;
	&quot;sync&quot;
	&quot;time&quot;

	&quot;golang.org/x/exp/slices&quot;
)

const (
	numPhilo       = 5
	eatTimes       = 3
	numEatingPhilo = 2
)

type eatRequest struct {
	who            int         // Who is making the request
	finishedFnChan chan func() // When approves a response will be sent on this channel with a function to call when done
}

// simulateHost - the host must provide permission before a philosopher can eat
// Exits when channel closed
func simulateHost(requestChannel &lt;-chan eatRequest) {
	awaitRequest := requestChannel
	finishedChan := make(chan struct {
		who  int
		done chan struct{}
	})

	var whoEating []int // tracks who is currently eating

	for {
		select {
		case request, ok := &lt;-awaitRequest:
			if !ok {
				return // Closed channel means that we are done (finishedChan is guaranteed to be empty)
			}
			// Sanity check - confirm that philosopher is not being greedy! (should never happen)
			if slices.Index(whoEating, request.who) != -1 {
				panic(&quot;Multiple requests from same philosopher&quot;)
			}
			whoEating = append(whoEating, request.who) // New request always goes at the end
			fmt.Printf(&quot;%d started eating (currently eating %v)\n&quot;, request.who, whoEating)

			// Let philosopher know and provide means for them to tell us when done
			request.finishedFnChan &lt;- func() {
				d := make(chan struct{})
				finishedChan &lt;- struct {
					who  int
					done chan struct{}
				}{who: request.who, done: d}
				&lt;-d // Wait until request has been processed (ensure we should never have two active requests from one philosopher)
			}
		case fin := &lt;-finishedChan:
			idx := slices.Index(whoEating, fin.who)
			if idx == -1 {
				panic(&quot;philosopher stopped eating multiple times!&quot;)
			}
			whoEating = append(whoEating[:idx], whoEating[idx+1:]...) // delete the element
			fmt.Printf(&quot;%d completed eating (currently eating %v)\n&quot;, fin.who, whoEating)
			close(fin.done)
		}
		// There has been a change in the number of philosopher&#39;s eating
		if len(whoEating) &lt; numEatingPhilo {
			awaitRequest = requestChannel
		} else {
			awaitRequest = nil // Ignore new eat requests until a philosopher finishes (nil channel will never be selected)
		}
	}
}

// ChopS represents a single chopstick
type ChopS struct {
	mu  sync.Mutex
	idx int // Including the index can make debugging simpler
}

// philosopher simulates a Philosopher (brain in a vat!)
func philosopher(philNum int, leftCS, rightCS *ChopS, requestToEat chan&lt;- eatRequest) {
	for numEat := 0; numEat &lt; eatTimes; numEat++ {
		// once the philosopher intends to eat, lock the corresponding chopsticks
		for {
			leftCS.mu.Lock()
			// Attempt to get the right Chopstick - if someone else has it we replace the left chopstick and try
			// again (in order to avoid deadlocks)
			if rightCS.mu.TryLock() {
				break
			}
			leftCS.mu.Unlock()
		}

		// We have the chopsticks but need the hosts permission
		ffc := make(chan func()) // when accepted we will receive a function to call when done eating
		requestToEat &lt;- eatRequest{
			who:            philNum,
			finishedFnChan: ffc,
		}
		doneEating := &lt;-ffc

		fmt.Printf(&quot;philosopher %d starting to eat (%d feed)\n&quot;, philNum, numEat)
		time.Sleep(time.Millisecond * time.Duration(rand.Intn(200))) // Eating takes a random amount of time
		fmt.Printf(&quot;philosopher %d finished eating (%d feed)\n&quot;, philNum, numEat)

		rightCS.mu.Unlock()
		leftCS.mu.Unlock()
		doneEating() // Tell host that we have finished eating
	}
	fmt.Printf(&quot;philosopher %d is full\n&quot;, philNum)
}

func main() {
	CSticks := make([]*ChopS, numPhilo)
	for i := 0; i &lt; numPhilo; i++ {
		CSticks[i] = &amp;ChopS{idx: i}
	}

	requestChannel := make(chan eatRequest)

	var wg sync.WaitGroup
	wg.Add(numPhilo)
	for i := 0; i &lt; numPhilo; i++ {
		go func(philNo int) {
			philosopher(philNo, CSticks[philNo-1], CSticks[philNo%numPhilo], requestChannel)
			wg.Done()
		}(i + 1)
	}

	go func() {
		wg.Wait()
		close(requestChannel)
	}()

	simulateHost(requestChannel)
}

huangapple
  • 本文由 发表于 2022年8月4日 05:33:46
  • 转载请务必保留本文链接:https://go.coder-hub.com/73228045.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定