使用mutex和semaphore创建银行的例子

167 阅读9分钟

在并发编程中,对共享资源的并发访问会导致各种问题,如数据竞赛。为了消除意外行为,对共享资源的并发访问必须受到保护。这个被保护的部分被称为关键部分。关键部分绝不能被一个以上的进程同时执行。

在这个例子中,我们要确保一个银行账户状态的完整性不被破坏。我们将通过使用两种通用的同步机制来实现这一点,这两种机制是semaphoremutex。这两种机制都是用来保护关键部分的。它们实现了互斥,限制了对多种资源的访问,解决了读者和写者的问题,等等。

  • 突变器:Mutex通过允许单个线程独占访问(相互排斥)来保护关键部分。

  • 傀儡(Semaphore):semaphore(一个变量或一个抽象数据类型)通过允许N个线程独占访问(互斥)来保护一个关键部分。

鉴于信号灯可以允许对共享资源进行多次访问,我们将把它限制为1。这被称为二进制信号灯,与使用容量为1的缓冲通道的情况相同。

银行骨架

这是我们的银行账户的基本框架。当我们使用不同的例子时,我们会更新它。

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
}

这是我们用来运行测试的主要代码,它将保持原样。

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("+")
}

一次一个读和一个写

确保每次只有一个goroutine访问资金。每个goroutine都有对资金的独占访问权,所以如果一个正在读取,另一个就会等待,反之亦然。这被认为是一个线程安全操作。

选项1:二进制信号/缓冲通道

我们在这里没有直接实现信号灯,但是如果你想创建和使用信号灯包,请看底部的例子。

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

选项2:同步.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

每次有许多读和一个写

允许许多goroutines在同一时间访问资金(一个共享的变量/资源)。然而,只有一个人可以写入,当这种情况发生时,读取也被阻断。如果没有人在写,则允许许多读,因为它被认为是一个有条件的安全操作。同样,只要没有人同时在写!这对高读流量的情况是很好的。

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

拴马桩包

这就是一个信号包的样子。

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
	}
}

使用方法

如果你愿意,你可以把它和我们的银行例子一起使用,如下所示。

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()
}

如果你想单独使用这个包的话,这里有一个简单的例子。在这种情况下,资源槽被灵活地使用,而不是像我们上面的银行例子那样只有一个槽。

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
	}
}