The main reason I can see is the missing concurrency planning.
The reason the codes got deadlock is because your timing of locking for each sticks is unclear. Topping up the fact that you randomizes the sticks and assign to 5 philosophers, there is a chance where a stick is getting locked multiple rounds at the same time, repeatedly. That’s why you occasionally get deadlock.
The way you’re currently doing is like picking 1 stick from a pair chopstick and combine with another stick from another pair and tell the philosopher to use that pair of chopstick to eat.
Aside from @Christophe_Meessen mentioning about not understanding the problem clearly, you always need to plan out your concurrency. This is important because you’re dealing with multiple processes. If one process is giving you problem(s), it will be magnified. Concurrency planning also helps you prioritizes what to control and simplify the processes to the minimum viable codes. Only then, you can have full power to ensure all processes will not get into deadlock and race conditions problems.
Planning Example
Here is an example of the planning (I refactored the plan 2 times before testing it on code) based on your listed requirements.
Plan 1
If you notice, in Plan 1, it serves as an overview of the program, testing whether I’m understanding the problem correctly or otherwise. It is cleared that we have at least 2 main process structures (
main
as “host”,
p
as philosopher process). The arrows (with some errors) indicate data exchanges between
main
and any
p
processes.
However, it is still unclear how would I approach the synchronization, so we need to improve the plan.
Plan 2
In Plan 2, a more improved version, we can now clearly see that
host
is actually controlling who can eat, offers a
request
function to either return
nil
to deny eating or a
chopstick
to allow eating.
Also, p
will check his/her fullness and inform back to host
that he/she will no longer eats. However, we did not take that into account in both Plan 1 and Plan 2, so we need to improve it again.
Plan 3
In Plan 3, it is very clear now with roles and synchronization. The table is synchronized using mutex locking in order to offer
request
function. It has clarity on when to lock and unlock during checking.
Also, main
setup a channel approach (c1
) after table for synchronizing with all philosophers regarding of their fullness before serving them. Hence, we understood that step 6 in main
is a for...select
mechanism for their waiting. Hence, at step 5, we are clear that each philosopher will take in c1
as a parameter in order to inform host of his/her fullness.
On p
side, it is clear that philosopher knows whether he/she can eat by checking the return value of the request
function. Without chopstick, repeat the request
again. Since we’re using c1
channel, it made it easy to infrom the host before completing the process.
With Plan 3, you can now know how-to and what-to code. If you notice:
- we do not need to manage chopsticks down to sticks level.
- chopsticks ownership is not needed.
However, we are still unclear with
contact switching
between main
, table
, and p
. (WHO is doing WHAT at WHEN)
- After eating, who is freeing the chopsticks.
Hence, we refactor the plan again.
Plan 4
This time, it has very clear contact switching, and explanation on WHO is doing WHAT at WHEN.
We can now clearly see that table
is responsible to setup and managing chopsticks
, offering contacting switching functions like Request
and Return
to p
. The mutex locking happens only at table
.
p
only needs to setup his/her fullness
limit, use Request
to get a chopstick from table, start eating, stop eating etc and Return
to free the chopstick back to table and check for its fullness
. If full
, then he/she informs host
by sending his/her name
to the fullness c1
channel.
main
(host) only needs to know the headcount
, setup table
, the fullness channel c1
, serve the philosophers (p1
, p2
, …), then process the waiting (for..select
) by checking the fullness report is the same has headcount
.
At any point, any time, you can use this map to explain WHO is doing WHAT at WHEN. This is the best indicator that you’re ready to code. You can continue to refactor your plan. However, Plan 4 is good enough for coding.
The codes differs from individual to individuals. However, all of them should agree to your concurrency plan. Please try it out yourself.
I’m hiding my tested source codes just in case. Please be self-disciplined. Welcome to Golang-Bridge by the way.
One coding Example
package main
import (
"fmt"
"sync"
)
const (
maxEating = 2
fullnessLimit = 2
)
// Chopsticks is an object structure that serves as a tool to eat
type Chopsticks struct {
mutex sync.RWMutex
ID int
isUsed bool
}
// NewChopsticks creates a Chopsticks object with the given id label.
func NewChopsticks(id int) *Chopsticks {
return &Chopsticks{
mutex: sync.RWMutex{},
ID: id,
isUsed: false,
}
}
// Use is to set the Chopsticks status to "In-Use"
func (c *Chopsticks) Use() {
c.mutex.Lock()
defer c.mutex.Unlock()
c.isUsed = true
}
// Free is to set the Chopsticks status to "Free"
func (c *Chopsticks) Free() {
c.mutex.Lock()
defer c.mutex.Unlock()
c.isUsed = false
}
// IsInUse is to check the Chopsticks status, currently "In-Use" or "Free".
func (c *Chopsticks) IsInUse() bool {
c.mutex.Lock()
defer c.mutex.Unlock()
return c.isUsed
}
// Table is the structure serving foods and chopsticks
type Table struct {
mutex sync.RWMutex
isEating uint
chopsticks []*Chopsticks
}
// NewTable creates the table object with person quantity.
func NewTable(quantity uint) *Table {
t := &Table{
mutex: sync.RWMutex{},
isEating: 0,
chopsticks: []*Chopsticks{},
}
for i := 0; i < int(quantity); i++ {
t.chopsticks = append(t.chopsticks, NewChopsticks(i))
}
return t
}
// RequestChopsticks is to allows a customer to eat using an available
// Chopsticks.
func (t *Table) RequestChopsticks() *Chopsticks {
t.mutex.Lock()
defer t.mutex.Unlock()
// table is full
if t.isEating >= maxEating {
return nil
}
// permit to eat. Scan for available chopsticks
c := t.seekChopsticks()
c.Use()
t.isEating++
return c
}
func (t *Table) seekChopsticks() *Chopsticks {
// NOTE: here, you can use random. I will use FIFO instead.
for _, c := range t.chopsticks {
if !c.IsInUse() {
return c
}
}
return nil
}
// ReturnChopsticks is to allow a customer to place back chopsticks when
// he/she is done eating
func (t *Table) ReturnChopsticks(c *Chopsticks) {
t.mutex.Lock()
defer t.mutex.Unlock()
t.isEating--
c.Free()
}
func philosopher(id int, fullness chan int, table *Table) {
eatCount := fullnessLimit
for {
chopsticks := table.RequestChopsticks()
if chopsticks == nil {
continue
}
// start eating
fmt.Printf("P%d: START eating with chopstick %d.\n",
id,
chopsticks.ID)
// eating
eatCount--
// stop eating
fmt.Printf("P%d: FINISH eating with chopstick %d.\n",
id,
chopsticks.ID)
table.ReturnChopsticks(chopsticks)
// check fullness
if eatCount == 0 {
fullness <- id
return
}
}
}
func main() {
headcount := 5
t := NewTable(uint(headcount))
c1 := make(chan int)
fullness := 0
for i := 0; i < headcount; i++ {
go philosopher(i, c1, t)
}
// Wait for fullness
for {
select {
case person := <-c1:
fullness++
fmt.Printf("Philosopher %d is full\n", person)
if fullness == headcount {
fmt.Printf("All are full.\n[ ENDED ]\n")
return
}
}
}
}
// Output:
// P4: START eating with chopstick 0.
// P4: FINISH eating with chopstick 0.
// P4: START eating with chopstick 0.
// P4: FINISH eating with chopstick 0.
// Philosopher 4 is full
// P0: START eating with chopstick 0.
// P0: FINISH eating with chopstick 0.
// P0: START eating with chopstick 0.
// P0: FINISH eating with chopstick 0.
// Philosopher 0 is full
// P3: START eating with chopstick 0.
// P2: START eating with chopstick 1.
// P2: FINISH eating with chopstick 1.
// P3: FINISH eating with chopstick 0.
// P3: START eating with chopstick 0.
// P3: FINISH eating with chopstick 0.
// Philosopher 3 is full
// P2: START eating with chopstick 1.
// P2: FINISH eating with chopstick 1.
// Philosopher 2 is full
// P1: START eating with chopstick 0.
// P1: FINISH eating with chopstick 0.
// P1: START eating with chopstick 0.
// P1: FINISH eating with chopstick 0.
// Philosopher 1 is full
// All are full.
// [ ENDED ]