In concurrent programming, concurrent access to shared resources can lead to all sorts of problems such as data race. In order to eliminate unexpected behaviour, concurrent access to the shared resource must be protected. This protected section is called the critical section. The critical section must not be executed by more than one process at a time.


In this example, we are going to ensure that the integrity of the state of a bank account is not compromised. We will do this by using two general synchronisation mechanisms which are semaphore and mutex. Both of these mechanisms are used to protect the critical section. They implement mutexes, limit access to multiple resources, solve the readers-writers problem so on.



Given that the semaphore can allow multiple access to a shared resource, we will limit it with 1. This is called binary semaphore and literally same as using a buffered channel with the capacity of 1.


Bank skeleton


This is the bare bones of our bank account. We will update it as we use different examples.


package bank

type Account struct {
fund float64
}

func (a *Account) Balance() float64 {
return a.fund
}

func (a *Account) Credit(amount float64) {
a.fund += amount
}

func (a *Account) Withdraw(amount float64) {
a.fund -= amount
}

This is the main piece of code we will be using to run tests and it will remain intact.


package main

import (
"time"

"internal/bank"
)

func main() {
account := &bank.Account{}

account.Balance("-")

for k, v := range map[int]string{10: "A", 20: "B", 30: "C"} {
go account.Balance(v)
go account.Credit(float64(k), v)
go account.Withdraw(float64(k), v)
}

time.Sleep(1 * time.Second)
account.Balance("+")
}

One read and one write at a time


Ensuring that only one goroutine accesses the funds at a time. Each goroutine will have an exclusive access to the funds so if one is reading then the other one will wait or vice versa. This is considered as a thread safe operation.


Option 1: Binary semaphore/buffered channels


We are not directly implementing a semaphore here but if you want to create and use a semaphore package, see example right at the bottom.


package bank

import (
"fmt"
"time"
)

var semaphore = make(chan struct{}, 1)

type Account struct {
fund float64
}

func (a *Account) Balance(who string) float64 {
semaphore <- struct{}{}
fmt.Println(who, "Bal", a.fund, "@", time.Now().UTC())
<-semaphore

return a.fund
}

func (a *Account) Credit(amount float64, who string) {
semaphore <- struct{}{}
fmt.Println(who, "Cre", amount, "@", time.Now().UTC())
a.fund += amount
<-semaphore
}

func (a *Account) Withdraw(amount float64, who string) {
semaphore <- struct{}{}
fmt.Println(who, "Wit", amount, "@", time.Now().UTC())
a.fund -= amount
<-semaphore
}

- Bal  0 @ 2020-04-13 14:01:33.650511 +0000 UTC
A Bal 0 @ 2020-04-13 14:01:33.650590 +0000 UTC
B Bal 0 @ 2020-04-13 14:01:33.650662 +0000 UTC
A Cre 10 @ 2020-04-13 14:01:33.650686 +0000 UTC +10
A Wit 10 @ 2020-04-13 14:01:33.650717 +0000 UTC 0
B Cre 20 @ 2020-04-13 14:01:33.650741 +0000 UTC +20
B Wit 20 @ 2020-04-13 14:01:33.650767 +0000 UTC 0
C Bal 0 @ 2020-04-13 14:01:33.650781 +0000 UTC
C Cre 30 @ 2020-04-13 14:01:33.650791 +0000 UTC +30
C Wit 30 @ 2020-04-13 14:01:33.650799 +0000 UTC 0
+ Bal 0 @ 2020-04-13 14:01:34.654512 +0000 UTC

- Bal 0 @ 2020-04-13 14:02:19.709515 +0000 UTC
B Bal 0 @ 2020-04-13 14:02:19.709617 +0000 UTC
C Cre 30 @ 2020-04-13 14:02:19.709702 +0000 UTC +30
B Cre 20 @ 2020-04-13 14:02:19.709728 +0000 UTC +50
B Wit 20 @ 2020-04-13 14:02:19.709746 +0000 UTC +30
C Bal 30 @ 2020-04-13 14:02:19.709786 +0000 UTC
A Bal 30 @ 2020-04-13 14:02:19.709801 +0000 UTC
C Wit 30 @ 2020-04-13 14:02:19.709815 +0000 UTC 0
A Cre 10 @ 2020-04-13 14:02:19.709833 +0000 UTC +10
A Wit 10 @ 2020-04-13 14:02:19.709847 +0000 UTC 0
+ Bal 0 @ 2020-04-13 14:02:20.711135 +0000 UTC

Option 2: Sync.Mutex


package bank

import (
"fmt"
"sync"
"time"
)

type Account struct {
fund float64
mux sync.Mutex
}

func (a *Account) Balance(who string) float64 {
a.mux.Lock()
defer a.mux.Unlock()

fmt.Println(who, "Bal", a.fund, "@", time.Now().UTC())

return a.fund
}

func (a *Account) Credit(amount float64, who string) {
a.mux.Lock()
defer a.mux.Unlock()

fmt.Println(who, "Cre", amount, "@", time.Now().UTC())
a.fund += amount
}

func (a *Account) Withdraw(amount float64, who string) {
a.mux.Lock()
defer a.mux.Unlock()

fmt.Println(who, "Wit", amount, "@", time.Now().UTC())
a.fund -= amount
}

- Bal  0 @ 2020-04-13 13:47:32.016951 +0000 UTC
A Bal 0 @ 2020-04-13 13:47:32.017039 +0000 UTC
B Bal 0 @ 2020-04-13 13:47:32.017177 +0000 UTC
A Cre 10 @ 2020-04-13 13:47:32.017188 +0000 UTC +10
A Wit 10 @ 2020-04-13 13:47:32.017216 +0000 UTC 0
C Bal 0 @ 2020-04-13 13:47:32.017231 +0000 UTC
C Cre 30 @ 2020-04-13 13:47:32.017244 +0000 UTC +30
B Cre 20 @ 2020-04-13 13:47:32.017254 +0000 UTC +50
B Wit 20 @ 2020-04-13 13:47:32.017266 +0000 UTC +30
C Wit 30 @ 2020-04-13 13:47:32.017280 +0000 UTC 0
+ Bal 0 @ 2020-04-13 13:47:33.021482 +0000 UTC

- Bal 0 @ 2020-04-13 13:48:45.885885 +0000 UTC
A Bal 0 @ 2020-04-13 13:48:45.885986 +0000 UTC
B Cre 20 @ 2020-04-13 13:48:45.886052 +0000 UTC +20
A Wit 10 @ 2020-04-13 13:48:45.886085 +0000 UTC +10
B Bal 10 @ 2020-04-13 13:48:45.886114 +0000 UTC
C Bal 10 @ 2020-04-13 13:48:45.886130 +0000 UTC
B Wit 20 @ 2020-04-13 13:48:45.886142 +0000 UTC -10
C Cre 30 @ 2020-04-13 13:48:45.886157 +0000 UTC +20
C Wit 30 @ 2020-04-13 13:48:45.886169 +0000 UTC -10
A Cre 10 @ 2020-04-13 13:48:45.886183 +0000 UTC 0
+ Bal 0 @ 2020-04-13 13:48:46.891075 +0000 UTC

Many read and one write at a time


Allowing many goroutines access the funds (a shared variable/resource) at a time. However, only one can write to it and when this happens, reads are blocked as well. If no one is writing, many reads are allowed since it is considered as a conditionally safe operation. Again, as long as no one is writing at same time! This is good for high read traffic cases.


package bank

import "sync"

type Account struct {
fund float64
mux sync.RWMutex
}

func (a *Account) Balance() float64 {
a.mux.RLock()
defer a.mux.RUnlock()

return a.fund
}

func (a *Account) Credit(amount float64) {
a.mux.Lock()
defer a.mux.Unlock()

a.fund += amount
}

func (a *Account) Withdraw(amount float64) {
a.mux.Lock()
defer a.mux.Unlock()

a.fund -= amount
}

- Bal  0 @ 2020-04-13 13:53:44.060959 +0000 UTC
A Bal 0 @ 2020-04-13 13:53:44.061042 +0000 UTC
B Cre 20 @ 2020-04-13 13:53:44.061125 +0000 UTC +20
B Bal 20 @ 2020-04-13 13:53:44.061158 +0000 UTC
C Bal 20 @ 2020-04-13 13:53:44.061161 +0000 UTC
A Cre 10 @ 2020-04-13 13:53:44.061212 +0000 UTC +30
A Wit 10 @ 2020-04-13 13:53:44.061233 +0000 UTC +20
B Wit 20 @ 2020-04-13 13:53:44.061248 +0000 UTC 0
C Cre 30 @ 2020-04-13 13:53:44.061262 +0000 UTC +30
C Wit 30 @ 2020-04-13 13:53:44.061276 +0000 UTC 0
+ Bal 0 @ 2020-04-13 13:53:45.066051 +0000 UTC

- Bal 0 @ 2020-04-13 13:55:31.374249 +0000 UTC
B Bal 0 @ 2020-04-13 13:55:31.374342 +0000 UTC
A Bal 0 @ 2020-04-13 13:55:31.374336 +0000 UTC
A Cre 10 @ 2020-04-13 13:55:31.374470 +0000 UTC +10
C Bal 10 @ 2020-04-13 13:55:31.374482 +0000 UTC
B Cre 20 @ 2020-04-13 13:55:31.374515 +0000 UTC +30
B Wit 20 @ 2020-04-13 13:55:31.374543 +0000 UTC +10
A Wit 10 @ 2020-04-13 13:55:31.374563 +0000 UTC 0
C Cre 30 @ 2020-04-13 13:55:31.374577 +0000 UTC +30
C Wit 30 @ 2020-04-13 13:55:31.374590 +0000 UTC 0
+ Bal 0 @ 2020-04-13 13:55:32.378968 +0000 UTC

Semaphore package


This is what a semaphore package would look like.


package semaphore

type Semaphore chan struct {}

// New returns Semaphore channel. Use 1 for "mutual exclusion".
func New(n int) Semaphore {
return make(Semaphore, n)
}

// acquire `n` amount of resource slots in semaphore channel.
func (s Semaphore) Acquire(n int) {
for i := 0; i < n; i++ {
s <- struct{}{}
}
}

// release `n` amount of resource slots in semaphore channel.
func (s Semaphore) Release(n int) {
for i := 0; i < n; i++ {
<- s
}
}

Usage


If you wanted, you could use it with our bank example as shown below.


package bank

import (
"internal/semaphore"
)

const slot = 1

type locker struct {
semaphore.Semaphore
}

func newLocker() locker {
return locker{semaphore.New(slot)}
}

func (l locker) lock() {
l.Semaphore.Acquire(slot)
}

func (l locker) release() {
l.Semaphore.Release(slot)
}

package bank

import (
"fmt"
"time"
)

type Account struct {
fund float64
locker locker
}

func New() *Account {
return &Account{
fund: 0,
locker: newLocker(),
}
}

func (a *Account) Balance(who string) float64 {
a.locker.lock()
fmt.Println(who, "Bal", a.fund, "@", time.Now().UTC())
a.locker.release()

return a.fund
}

func (a *Account) Credit(amount float64, who string) {
a.locker.lock()
fmt.Println(who, "Cre", amount, "@", time.Now().UTC())
a.fund += amount
a.locker.release()
}

func (a *Account) Withdraw(amount float64, who string) {
a.locker.lock()
fmt.Println(who, "Wit", amount, "@", time.Now().UTC())
a.fund -= amount
a.locker.release()
}

Optionally if you want to use the package on its own then here is a simple example. In this case, resource slots are used in a flexible manner as opposed to our bank example above with 1 slot.


package main

import "fmt"

type (
// empty is a 0 byte struct.
empty struct {}
// semaphore is a channel with empty struct.
semaphore chan empty
)

func main() {
// Initialise semaphore channel with 10 resource slots.
s := make(semaphore, 10)

// Acquire 7 resource slots.
s.acquire(1)
s.acquire(2)
s.acquire(4)
fmt.Println("Available resource slots:", len(s))

// Release 2 resource slots.
s.release(2)
fmt.Println("Available resource slots:", len(s))
}

// acquire `n` amount of resource slots in semaphore channel.
func (s semaphore) acquire(n int) {
for i := 0; i < n; i++ {
s <- empty{}
}
}

// release `n` amount of resource slots in semaphore channel.
func (s semaphore) release(n int) {
for i := 0; i < n; i++ {
<- s
}
}

References