Go 中的哲学家就餐问题未通过单元测试

Posted

技术标签:

【中文标题】Go 中的哲学家就餐问题未通过单元测试【英文标题】:Dining philosophers problem in Go fails unit test 【发布时间】:2022-01-17 19:09:58 【问题描述】:

我正在学习围棋课程,其作业如下:

用下面的方法实现餐饮哲学家的问题 约束/修改。

应该有 5 个哲学家共用筷子,每对相邻的哲学家之间有一根筷子。

每个哲学家应该只吃 3 次(而不是像我们在讲座中那样无限循环)。

哲学家按任意顺序拿起筷子,而不是从最低的先拿起筷子(我们在讲座中做过)。

为了吃饭,哲学家必须获得在自己的 goroutine 中执行的主机的许可。

主持人允许不超过 2 位哲学家同时用餐。

每位哲学家都有编号,从 1 到 5。

当哲学家开始进食时(在它获得必要的 锁)它自己在一行上打印“开始吃”,其中 是哲学家的编号。

当哲学家吃完饭时(在它释放锁之前) 它自己在一行上打印“finishing eating”,其中 是哲学家的编号。

我的实现:

package main

import (
    "fmt"
    "io"
    "math/rand"
    "os"
    "sync"
    "time"
)

const (
    NumPhilosophers    = 5
    NumEatMaxTimes     = 3
    NumMaxAllowedToEat = 2
)

type chopstick struct sync.Mutex 

type philosopher struct 
    num int
    cs  []*chopstick


func setTable() []*philosopher 
    cs := make([]*chopstick, NumPhilosophers)
    for i := 0; i < NumPhilosophers; i++ 
        cs[i] = new(chopstick)
    
    ph := make([]*philosopher, NumPhilosophers)
    for i := 0; i < NumPhilosophers; i++ 
        ph[i] = &philosopheri + 1, []*chopstickcs[i], cs[(i+1)%NumPhilosophers]
    

    return ph


func (ph philosopher) eat(sem chan int, wg *sync.WaitGroup, w io.Writer) 
    for i := 0; i < NumEatMaxTimes; i++ 
        /* Ask host for permission to eat */
        sem <- 1
        /*
            Pick any of the left or right chopsticks.
            Notice how the methods on the Mutex can be called directly on a chopstick due to embedding.
        */
        firstCS := rand.Intn(2)
        secondCS := (firstCS + 1) % 2
        ph.cs[firstCS].Lock()
        ph.cs[secondCS].Lock()

        fmt.Fprintf(w, "Starting to eat %d\n", ph.num)
        x := rand.Intn(NumEatMaxTimes)
        time.Sleep(time.Duration(x) * time.Second)
        fmt.Fprintf(w, "Finishing eating %d\n", ph.num)

        ph.cs[secondCS].Unlock()
        ph.cs[firstCS].Unlock()
        <-sem
    
    wg.Done()


func main() 
    run(os.Stdout)


func run(w io.Writer) 
    var sem = make(chan int, NumMaxAllowedToEat)
    rand.Seed(time.Now().UnixNano())
    var wg sync.WaitGroup

    allPh := setTable()
    wg.Add(len(allPh))
    for _, ph := range allPh 
        go ph.eat(sem, &wg, w)
    
    wg.Wait()

单元测试:

func TestRun(t *testing.T) 
    var out bytes.Buffer
    run(&out)
    lines := strings.Split(strings.ReplaceAll(out.String(), "\r\n", "\n"), "\n")
    eating := make(map[int]bool)
    timesEaten := make(map[int]int)
    for _, line := range lines 
        if line == "" 
            continue
        
        fmt.Println(line)
        tokens := strings.Fields(line)

        i, err := strconv.Atoi(tokens[len(tokens)-1])
        if err != nil 
            t.Errorf("Bad line: %s", line)
        

        s := strings.ToLower(tokens[0])

        if s == "starting" 
            if len(eating) > (NumMaxAllowedToEat - 1) 
                t.Errorf("%v are eating at the same time", eating)
            
            _, ok := eating[i]
            if ok 
                t.Errorf("%d started before finishing", i)
            
            eating[i] = true
         else if s == "finishing" 
            _, ok := eating[i]
            if !ok 
                t.Errorf("%d finished without starting", i)
            

            delete(eating, i)

            timesEaten[i] = timesEaten[i] + 1
        
    

    for k, v := range timesEaten 
        if v > NumEatMaxTimes 
            t.Errorf("%d ate %d times", k, v)
        
    

    if len(timesEaten) != NumPhilosophers 
        t.Error("One or more didn't get to eat")
    

问题是,测试随机失败。下面是一个执行(添加了行号):

1. Starting to eat 5
2. Starting to eat 2
3. Finishing eating 2
4. Finishing eating 5
5. Starting to eat 3
6. Starting to eat 1
7. Finishing eating 1
8. Finishing eating 3
9. Starting to eat 2
10. Starting to eat 4
11. Finishing eating 4
12. Starting to eat 5
13. Finishing eating 2
14. Finishing eating 5
15. Starting to eat 3
16. Finishing eating 3
17. Starting to eat 1
18. Finishing eating 4
19. Finishing eating 1
20. Starting to eat 5
21. Finishing eating 5
22. Starting to eat 3
23. Finishing eating 3
24. Starting to eat 4
25. Starting to eat 2
26. Finishing eating 2
27. Starting to eat 1
28. Finishing eating 4
29. Finishing eating 1

--- FAIL: TestRun (12.01s)
    main_test.go:43: 4 finished without starting

Philosopher 4 已从第 10 行和第 24 行开始,并在第 11、18 和 28 行结束。第 28 行不匹配,因此测试正确地抱怨。但是,我很难找到错误。你能帮忙吗?

【问题讨论】:

问题没有在 28 日结束。它在 10 日开始,在 11 日结束,然后在 18 日再次结束,然后在 24 日再次开始。问题结束于18. 我承认你看到的输出似乎不可能,希望其他人能看到我所缺少的。我要指出您的代码不符合要求:您允许三个哲学家一次吃,而要求规定每个哲学家应该吃3次总共. 你试过go test -race .吗? @Adrian “你允许三个哲学家吃饭”我不这么认为;缓冲通道充当信号量,不允许超过 2 个哲学家同时吃饭。 @rustyx 我现在做了,它失败了,因为“在执行测试期间检测到种族”。如何检测和修复竞态条件?如果我运行go run -race main.go,它工作正常。 比赛检测器打印出比赛在源中的确切位置。 【参考方案1】:

回答我自己的问题,结果是byes.Buffer is not thread-safe。我最终使用go-fakeio 库进行测试,如下所示。

s, err := fakeio.Stderr().Stdout().Do(run)
if err != nil 
    t.Errorf("%v", err)

测试的其余部分保持不变。 main.run 函数不再需要 io.Writer,因为 fakeio 库替换了 stderr 和 stdout。

【讨论】:

以上是关于Go 中的哲学家就餐问题未通过单元测试的主要内容,如果未能解决你的问题,请参考以下文章

线程学习五:哲学家就餐问题

使用信号量 (BACI) 就餐哲学家

操作系统作业哲学家就餐问题

用java多线程编写哲学家就餐程序 利用多线程技术编写哲学家就餐程序,使之在运行时能演示产生死锁的情况,

哲学家就餐问题

死锁例证:哲学家就餐