goconcurrencydining-philosopher

Golang implementation of dining philosophers variant problem


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.

I implemented the following code:

package main

import (
    "fmt"
    "sync"
)

// 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 := <-pHost.requestChannel
            currEating := make([]int, 0, numPhilo)
            for tmpIdx, eating := range pHost.eatingPhilos {
                if eating {
                    currEating = append(currEating, tmpIdx)
                }
            }
            fmt.Printf("%v have been eating, clearing up %d\n", currEating, finished.idx)
            pHost.eatingPhilos[finished.idx] = false
        }

        select {
        case <-pHost.quitChannel:
            fmt.Println("stop hosting")
            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 < 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 <- pPhilo
        pPhilo.host.mu.Lock()
        pPhilo.host.eatingPhilos[pPhilo.idx] = true
        pPhilo.host.mu.Unlock()

        pPhilo.numEat++
        fmt.Printf("starting to eat %d for %d time\n", pPhilo.idx, pPhilo.numEat)
        fmt.Printf("finishing eating %d for %d time\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
}

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?


Solution

  • 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 (
        "fmt"
        "math/rand"
        "sync"
        "time"
    
        "golang.org/x/exp/slices"
    )
    
    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 <-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 := <-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("Multiple requests from same philosopher")
                }
                whoEating = append(whoEating, request.who) // New request always goes at the end
                fmt.Printf("%d started eating (currently eating %v)\n", request.who, whoEating)
    
                // Let philosopher know and provide means for them to tell us when done
                request.finishedFnChan <- func() {
                    d := make(chan struct{})
                    finishedChan <- struct {
                        who  int
                        done chan struct{}
                    }{who: request.who, done: d}
                    <-d // Wait until request has been processed (ensure we should never have two active requests from one philosopher)
                }
            case fin := <-finishedChan:
                idx := slices.Index(whoEating, fin.who)
                if idx == -1 {
                    panic("philosopher stopped eating multiple times!")
                }
                whoEating = append(whoEating[:idx], whoEating[idx+1:]...) // delete the element
                fmt.Printf("%d completed eating (currently eating %v)\n", fin.who, whoEating)
                close(fin.done)
            }
            // There has been a change in the number of philosopher's eating
            if len(whoEating) < 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<- eatRequest) {
        for numEat := 0; numEat < 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 <- eatRequest{
                who:            philNum,
                finishedFnChan: ffc,
            }
            doneEating := <-ffc
    
            fmt.Printf("philosopher %d starting to eat (%d feed)\n", philNum, numEat)
            time.Sleep(time.Millisecond * time.Duration(rand.Intn(200))) // Eating takes a random amount of time
            fmt.Printf("philosopher %d finished eating (%d feed)\n", philNum, numEat)
    
            rightCS.mu.Unlock()
            leftCS.mu.Unlock()
            doneEating() // Tell host that we have finished eating
        }
        fmt.Printf("philosopher %d is full\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)
    }