Go:使用 Goroutines 进行银行转账模拟
Go: Bank Transfer Simulation using Goroutines
为了上大学,我必须在 Java 中实施银行转账模拟。完成之后,我想在 Go 中实现它,因为我听说了很多关于 Go 的并发能力并想尝试一下。
我有两个派对,foo 和 bar。每一方都有一份银行账户清单,其中包含余额和用于识别的号码。 foo 的每个账户都应该向 bar 的一个账户转账一定数额。这些转账应该分成更小、更不可疑的转账,重复转账一个单位,直到转完全部金额。同时,bar 将相同的金额转回给 foo,因此 foo 和 bar 的账户总和在开始和结束时应该是相同的。
这是我的帐户结构:
type Account struct {
Owner string
Number int
Balance int
}
func NewAccount(owner string, number int, balance int) *Account {
account := &Account{Owner: owner, Number: number, Balance: balance}
return account
}
func (account Account) String() string {
return fmt.Sprintf("%s-%04d", account.Owner, account.Number)
}
这是 function/method 帐户必须 运行 才能接收付款(我将付款实施为负数付款):
func (account *Account) Listen(channel <-chan int) {
for amount := range channel {
account.Balance += amount
}
}
这是我的传输结构:
type Transfer struct {
Source *Account
Target *Account
Amount int
}
func NewTransfer(source *Account, target *Account, amount int) *Transfer {
transfer := Transfer{Source: source, Target: target, Amount: amount}
return &transfer
}
func (transfer Transfer) String() string {
return fmt.Sprintf("Transfer from [%s] to [%s] with amount CHF %4d.-",
transfer.Source, transfer.Target, transfer.Amount)
}
这里是 function/method,它通过一个渠道向每个帐户执行一系列小额支付:
func (transfer Transfer) Execute(status chan<- string) {
const PAYMENT = 1
sourceChannel := make(chan int)
targetChannel := make(chan int)
go transfer.Source.Listen(sourceChannel)
go transfer.Target.Listen(targetChannel)
for paid := 0; paid < transfer.Amount; paid += PAYMENT {
sourceChannel <- -PAYMENT
targetChannel <- +PAYMENT
}
close(sourceChannel)
close(targetChannel)
status <- fmt.Sprintf("transfer done: %s", transfer)
}
最后,这是实际的程序:
func main() {
const ACCOUNTS = 25
const TRANSFERS = ACCOUNTS * 2
const AMOUNT = 5000
const BALANCE = 9000
fooStartBalance := 0
barStartBalance := 0
fooAccounts := [ACCOUNTS]*Account{}
barAccounts := [ACCOUNTS]*Account{}
for i := 0; i < ACCOUNTS; i++ {
fooAccounts[i] = NewAccount("foo", i + 1, BALANCE)
fooStartBalance += fooAccounts[i].Balance
barAccounts[i] = NewAccount("bar", i + 1, BALANCE)
barStartBalance += barAccounts[i].Balance
}
fooToBarTransfers := [ACCOUNTS]*Transfer{}
barToFooTransfers := [ACCOUNTS]*Transfer{}
for i := 0; i < ACCOUNTS; i++ {
fooToBarTransfers[i] = NewTransfer(fooAccounts[i], barAccounts[i], AMOUNT)
barToFooTransfers[i] = NewTransfer(barAccounts[i], fooAccounts[i], AMOUNT)
}
status := make(chan string)
for i := 0; i < ACCOUNTS; i++ {
go fooToBarTransfers[i].Execute(status)
go barToFooTransfers[i].Execute(status)
}
for i := 0; i < TRANSFERS; i++ {
fmt.Printf("%2d. %s\n", i + 1, <-status)
}
close(status)
fooEndBalance := 0
barEndBalance := 0
for i := 0; i < ACCOUNTS; i++ {
fooEndBalance += fooAccounts[i].Balance
barEndBalance += barAccounts[i].Balance
}
fmt.Printf("Start: foo: %4d, bar: %4d\n", fooStartBalance, fooStartBalance)
fmt.Printf(" End: foo: %4d, bar: %4d\n", fooEndBalance, fooEndBalance)
}
如标准输出所示,所有传输已在最后完成:
1. transfer done: Transfer from [bar-0011] to [foo-0011] with amount CHF 5000.-
[other 48 transfers omitted]
50. transfer done: Transfer from [bar-0013] to [foo-0013] with amount CHF 5000.-
但是钱要么被创造出来:
Start: foo: 225000, bar: 225000
End: foo: 225053, bar: 225053
或丢失:
Start: foo: 225000, bar: 225000
End: foo: 225053, bar: 225053
所以我认为(以我的 Java 心态)问题可能是 Account.Listen():也许 Balance 被 Goroutine A 读取,然后是 Goroutine B,执行 Account.Listen () 完成,然后 Goroutine A 继续使用旧值进行计算。互斥体可能会修复它:
type Account struct {
Owner string
Number int
Balance int
Mutex sync.Mutex
}
func (account *Account) Listen(channel <-chan int) {
for amount := range channel {
account.Mutex.Lock()
account.Balance += amount
account.Mutex.Unlock()
}
}
效果很好...十次九次。但是然后:
Start: foo: 225000, bar: 225000
End: foo: 225001, bar: 225001
这很奇怪。互斥量似乎有帮助,因为它大部分时间都在工作,当它不起作用时,它只差一个。我真的不知道其他什么地方同步可能是个问题。
更新:当我按如下方式实施帐户时,我无法阻止数据竞争警告:
type Account struct {
sync.Mutex
Owner string
Number int
Balance int
}
func NewAccount(owner string, number int, balance int) *Account {
account := &Account{Owner: owner, Number: number, Balance: balance}
return account
}
func (account *Account) String() string {
return fmt.Sprintf("%s-%04d", account.Owner, account.Number)
}
func (account *Account) Listen(channel <-chan int) {
for amount := range channel {
account.Lock()
account.Balance += amount
account.Unlock()
}
}
func (account *Account) GetBalance() int {
account.Lock()
newBalance := account.Balance
defer account.Unlock()
return newBalance
}
最后我也像这样访问余额:
fooEndBalance += fooAccounts[i].GetBalance()
barEndBalance += barAccounts[i].GetBalance()
正如我所说,数据竞争检测器现在保持沉默,但我仍然大约每 10 次就会出现一些错误 运行:
Start: foo: 100000, bar: 100000
End: foo: 99999, bar: 99999
我真的不明白我做错了什么。
由于这是作业(感谢您这么说),这里有一个线索。
I really don't get at what other place synchronization might be an issue.
每当你运行进入这个问题时,使用Go data race detector。它对您的代码有几点要说。
[编辑]
另一个问题:
fmt.Printf("Start: foo: %4d, bar: %4d\n", fooStartBalance, fooStartBalance)
fmt.Printf(" End: foo: %4d, bar: %4d\n", fooEndBalance, fooEndBalance)
你打印了两次 foo,而不是 foo 和 bar。
真正的问题是你 运行 你的 Execute goroutines,并假设他们的工作立即完成:
for i := 0; i < ACCOUNTS; i++ {
go fooToBarTransfers[i].Execute(status)
go barToFooTransfers[i].Execute(status)
}
for i := 0; i < TRANSFERS; i++ {
fmt.Printf("%2d. %s\n", i+1, <-status)
}
close(status)
到这里,您认为工作已经完成并继续打印结果:
fooEndBalance := 0
barEndBalance := 0
...
然而,goroutines 可能不会在此时完成。在确保传输完成之前,您需要等待它们结束。你能自己找到办法吗?
谢谢 Zoyd,你帮我指出了这个问题。问题是在没有等待两种 Listen 方法的情况下报告状态。这是我现在正在做的事情:
func (transfer Transfer) Execute(status chan<- string) {
const PAYMENT = 1
sourceChannel := make(chan int)
targetChannel := make(chan int)
sourceControlChannel := make (chan bool) // new
targetControlChannel := make (chan bool) // new
go transfer.Source.Listen(sourceChannel, sourceControlChannel)
go transfer.Target.Listen(targetChannel, targetControlChannel)
for paid := 0; paid < transfer.Amount; paid += PAYMENT {
sourceChannel <- -PAYMENT
targetChannel <- +PAYMENT
}
close(sourceChannel)
close(targetChannel)
// new condition
if <- sourceControlChannel && <- targetControlChannel {
status <- fmt.Sprintf("transfer done" )
}
}
func (account *Account) Listen(channel <-chan int, control chan<- bool) {
for amount := range channel {
account.Lock()
account.Balance += amount
account.Unlock()
}
control <- true // new
}
我觉得很笨拙。我尝试对此进行改进,但现在问题似乎已经消失了。
编辑:我现在试着稍微简化一下代码。它现在正在工作,数据竞争检测器不再抱怨,即使我没有使用方法来访问余额。
package main
import (
"fmt"
"math"
"sync"
)
type account struct {
owner string
number int
sync.Mutex
balance int
}
func (acc *account) listen(transfers <-chan int, control chan<- bool) {
for amount := range transfers {
acc.Lock()
acc.balance += amount
acc.Unlock()
}
control <- true
}
type transfer struct {
source *account
target *account
amount int
}
func (trans transfer) execute(status chan<- string) {
const PAYMENT = 1
sourceChannel := make(chan int)
targetChannel := make(chan int)
controlChannel := make (chan bool)
go trans.source.listen(sourceChannel, controlChannel)
go trans.target.listen(targetChannel, controlChannel)
for paid := 0; paid < trans.amount; paid += PAYMENT {
sourceChannel <- -PAYMENT
targetChannel <- +PAYMENT
}
close(sourceChannel)
close(targetChannel)
if <- controlChannel && <- controlChannel {
status <- "transfer done"
}
}
func main() {
const ACCOUNTS = 10
const TRANSFERS = ACCOUNTS * 2
const AMOUNT = 100
const BALANCE = 1000
fooBalance := 0
barBalance := 0
foo := [ACCOUNTS]*account{}
bar := [ACCOUNTS]*account{}
for i := 0; i < ACCOUNTS; i++ {
foo[i] = &account{owner: "foo", number: i, balance: BALANCE}
bar[i] = &account{owner: "bar", number: i, balance: BALANCE}
fooBalance += foo[i].balance
barBalance += bar[i].balance
}
fooToBar := [ACCOUNTS]*transfer{}
barToFoo := [ACCOUNTS]*transfer{}
for i := 0; i < ACCOUNTS; i++ {
fooToBar[i] = &transfer{source: foo[i], target: bar[i], amount: AMOUNT}
barToFoo[i] = &transfer{source: bar[i], target: foo[i], amount: AMOUNT}
}
status := make(chan string)
for i := 0; i < ACCOUNTS; i++ {
go fooToBar[i].execute(status)
go barToFoo[i].execute(status)
}
for i := 0; i < TRANSFERS; i++ {
fmt.Printf("%d. %s\n", i + 1, <-status)
}
close(status)
for i := 0; i < ACCOUNTS; i++ {
fooBalance -= foo[i].balance
barBalance -= bar[i].balance
}
if (fooBalance != 0 || barBalance != 0) {
difference := math.Abs(float64(fooBalance)) + math.Abs(float64(barBalance))
fmt.Println("Error: difference detected: ", difference)
} else {
fmt.Println("Success: no difference detected")
}
}
为了上大学,我必须在 Java 中实施银行转账模拟。完成之后,我想在 Go 中实现它,因为我听说了很多关于 Go 的并发能力并想尝试一下。
我有两个派对,foo 和 bar。每一方都有一份银行账户清单,其中包含余额和用于识别的号码。 foo 的每个账户都应该向 bar 的一个账户转账一定数额。这些转账应该分成更小、更不可疑的转账,重复转账一个单位,直到转完全部金额。同时,bar 将相同的金额转回给 foo,因此 foo 和 bar 的账户总和在开始和结束时应该是相同的。
这是我的帐户结构:
type Account struct {
Owner string
Number int
Balance int
}
func NewAccount(owner string, number int, balance int) *Account {
account := &Account{Owner: owner, Number: number, Balance: balance}
return account
}
func (account Account) String() string {
return fmt.Sprintf("%s-%04d", account.Owner, account.Number)
}
这是 function/method 帐户必须 运行 才能接收付款(我将付款实施为负数付款):
func (account *Account) Listen(channel <-chan int) {
for amount := range channel {
account.Balance += amount
}
}
这是我的传输结构:
type Transfer struct {
Source *Account
Target *Account
Amount int
}
func NewTransfer(source *Account, target *Account, amount int) *Transfer {
transfer := Transfer{Source: source, Target: target, Amount: amount}
return &transfer
}
func (transfer Transfer) String() string {
return fmt.Sprintf("Transfer from [%s] to [%s] with amount CHF %4d.-",
transfer.Source, transfer.Target, transfer.Amount)
}
这里是 function/method,它通过一个渠道向每个帐户执行一系列小额支付:
func (transfer Transfer) Execute(status chan<- string) {
const PAYMENT = 1
sourceChannel := make(chan int)
targetChannel := make(chan int)
go transfer.Source.Listen(sourceChannel)
go transfer.Target.Listen(targetChannel)
for paid := 0; paid < transfer.Amount; paid += PAYMENT {
sourceChannel <- -PAYMENT
targetChannel <- +PAYMENT
}
close(sourceChannel)
close(targetChannel)
status <- fmt.Sprintf("transfer done: %s", transfer)
}
最后,这是实际的程序:
func main() {
const ACCOUNTS = 25
const TRANSFERS = ACCOUNTS * 2
const AMOUNT = 5000
const BALANCE = 9000
fooStartBalance := 0
barStartBalance := 0
fooAccounts := [ACCOUNTS]*Account{}
barAccounts := [ACCOUNTS]*Account{}
for i := 0; i < ACCOUNTS; i++ {
fooAccounts[i] = NewAccount("foo", i + 1, BALANCE)
fooStartBalance += fooAccounts[i].Balance
barAccounts[i] = NewAccount("bar", i + 1, BALANCE)
barStartBalance += barAccounts[i].Balance
}
fooToBarTransfers := [ACCOUNTS]*Transfer{}
barToFooTransfers := [ACCOUNTS]*Transfer{}
for i := 0; i < ACCOUNTS; i++ {
fooToBarTransfers[i] = NewTransfer(fooAccounts[i], barAccounts[i], AMOUNT)
barToFooTransfers[i] = NewTransfer(barAccounts[i], fooAccounts[i], AMOUNT)
}
status := make(chan string)
for i := 0; i < ACCOUNTS; i++ {
go fooToBarTransfers[i].Execute(status)
go barToFooTransfers[i].Execute(status)
}
for i := 0; i < TRANSFERS; i++ {
fmt.Printf("%2d. %s\n", i + 1, <-status)
}
close(status)
fooEndBalance := 0
barEndBalance := 0
for i := 0; i < ACCOUNTS; i++ {
fooEndBalance += fooAccounts[i].Balance
barEndBalance += barAccounts[i].Balance
}
fmt.Printf("Start: foo: %4d, bar: %4d\n", fooStartBalance, fooStartBalance)
fmt.Printf(" End: foo: %4d, bar: %4d\n", fooEndBalance, fooEndBalance)
}
如标准输出所示,所有传输已在最后完成:
1. transfer done: Transfer from [bar-0011] to [foo-0011] with amount CHF 5000.-
[other 48 transfers omitted]
50. transfer done: Transfer from [bar-0013] to [foo-0013] with amount CHF 5000.-
但是钱要么被创造出来:
Start: foo: 225000, bar: 225000
End: foo: 225053, bar: 225053
或丢失:
Start: foo: 225000, bar: 225000
End: foo: 225053, bar: 225053
所以我认为(以我的 Java 心态)问题可能是 Account.Listen():也许 Balance 被 Goroutine A 读取,然后是 Goroutine B,执行 Account.Listen () 完成,然后 Goroutine A 继续使用旧值进行计算。互斥体可能会修复它:
type Account struct {
Owner string
Number int
Balance int
Mutex sync.Mutex
}
func (account *Account) Listen(channel <-chan int) {
for amount := range channel {
account.Mutex.Lock()
account.Balance += amount
account.Mutex.Unlock()
}
}
效果很好...十次九次。但是然后:
Start: foo: 225000, bar: 225000
End: foo: 225001, bar: 225001
这很奇怪。互斥量似乎有帮助,因为它大部分时间都在工作,当它不起作用时,它只差一个。我真的不知道其他什么地方同步可能是个问题。
更新:当我按如下方式实施帐户时,我无法阻止数据竞争警告:
type Account struct {
sync.Mutex
Owner string
Number int
Balance int
}
func NewAccount(owner string, number int, balance int) *Account {
account := &Account{Owner: owner, Number: number, Balance: balance}
return account
}
func (account *Account) String() string {
return fmt.Sprintf("%s-%04d", account.Owner, account.Number)
}
func (account *Account) Listen(channel <-chan int) {
for amount := range channel {
account.Lock()
account.Balance += amount
account.Unlock()
}
}
func (account *Account) GetBalance() int {
account.Lock()
newBalance := account.Balance
defer account.Unlock()
return newBalance
}
最后我也像这样访问余额:
fooEndBalance += fooAccounts[i].GetBalance()
barEndBalance += barAccounts[i].GetBalance()
正如我所说,数据竞争检测器现在保持沉默,但我仍然大约每 10 次就会出现一些错误 运行:
Start: foo: 100000, bar: 100000
End: foo: 99999, bar: 99999
我真的不明白我做错了什么。
由于这是作业(感谢您这么说),这里有一个线索。
I really don't get at what other place synchronization might be an issue.
每当你运行进入这个问题时,使用Go data race detector。它对您的代码有几点要说。
[编辑]
另一个问题:
fmt.Printf("Start: foo: %4d, bar: %4d\n", fooStartBalance, fooStartBalance)
fmt.Printf(" End: foo: %4d, bar: %4d\n", fooEndBalance, fooEndBalance)
你打印了两次 foo,而不是 foo 和 bar。
真正的问题是你 运行 你的 Execute goroutines,并假设他们的工作立即完成:
for i := 0; i < ACCOUNTS; i++ {
go fooToBarTransfers[i].Execute(status)
go barToFooTransfers[i].Execute(status)
}
for i := 0; i < TRANSFERS; i++ {
fmt.Printf("%2d. %s\n", i+1, <-status)
}
close(status)
到这里,您认为工作已经完成并继续打印结果:
fooEndBalance := 0
barEndBalance := 0
...
然而,goroutines 可能不会在此时完成。在确保传输完成之前,您需要等待它们结束。你能自己找到办法吗?
谢谢 Zoyd,你帮我指出了这个问题。问题是在没有等待两种 Listen 方法的情况下报告状态。这是我现在正在做的事情:
func (transfer Transfer) Execute(status chan<- string) {
const PAYMENT = 1
sourceChannel := make(chan int)
targetChannel := make(chan int)
sourceControlChannel := make (chan bool) // new
targetControlChannel := make (chan bool) // new
go transfer.Source.Listen(sourceChannel, sourceControlChannel)
go transfer.Target.Listen(targetChannel, targetControlChannel)
for paid := 0; paid < transfer.Amount; paid += PAYMENT {
sourceChannel <- -PAYMENT
targetChannel <- +PAYMENT
}
close(sourceChannel)
close(targetChannel)
// new condition
if <- sourceControlChannel && <- targetControlChannel {
status <- fmt.Sprintf("transfer done" )
}
}
func (account *Account) Listen(channel <-chan int, control chan<- bool) {
for amount := range channel {
account.Lock()
account.Balance += amount
account.Unlock()
}
control <- true // new
}
我觉得很笨拙。我尝试对此进行改进,但现在问题似乎已经消失了。
编辑:我现在试着稍微简化一下代码。它现在正在工作,数据竞争检测器不再抱怨,即使我没有使用方法来访问余额。
package main
import (
"fmt"
"math"
"sync"
)
type account struct {
owner string
number int
sync.Mutex
balance int
}
func (acc *account) listen(transfers <-chan int, control chan<- bool) {
for amount := range transfers {
acc.Lock()
acc.balance += amount
acc.Unlock()
}
control <- true
}
type transfer struct {
source *account
target *account
amount int
}
func (trans transfer) execute(status chan<- string) {
const PAYMENT = 1
sourceChannel := make(chan int)
targetChannel := make(chan int)
controlChannel := make (chan bool)
go trans.source.listen(sourceChannel, controlChannel)
go trans.target.listen(targetChannel, controlChannel)
for paid := 0; paid < trans.amount; paid += PAYMENT {
sourceChannel <- -PAYMENT
targetChannel <- +PAYMENT
}
close(sourceChannel)
close(targetChannel)
if <- controlChannel && <- controlChannel {
status <- "transfer done"
}
}
func main() {
const ACCOUNTS = 10
const TRANSFERS = ACCOUNTS * 2
const AMOUNT = 100
const BALANCE = 1000
fooBalance := 0
barBalance := 0
foo := [ACCOUNTS]*account{}
bar := [ACCOUNTS]*account{}
for i := 0; i < ACCOUNTS; i++ {
foo[i] = &account{owner: "foo", number: i, balance: BALANCE}
bar[i] = &account{owner: "bar", number: i, balance: BALANCE}
fooBalance += foo[i].balance
barBalance += bar[i].balance
}
fooToBar := [ACCOUNTS]*transfer{}
barToFoo := [ACCOUNTS]*transfer{}
for i := 0; i < ACCOUNTS; i++ {
fooToBar[i] = &transfer{source: foo[i], target: bar[i], amount: AMOUNT}
barToFoo[i] = &transfer{source: bar[i], target: foo[i], amount: AMOUNT}
}
status := make(chan string)
for i := 0; i < ACCOUNTS; i++ {
go fooToBar[i].execute(status)
go barToFoo[i].execute(status)
}
for i := 0; i < TRANSFERS; i++ {
fmt.Printf("%d. %s\n", i + 1, <-status)
}
close(status)
for i := 0; i < ACCOUNTS; i++ {
fooBalance -= foo[i].balance
barBalance -= bar[i].balance
}
if (fooBalance != 0 || barBalance != 0) {
difference := math.Abs(float64(fooBalance)) + math.Abs(float64(barBalance))
fmt.Println("Error: difference detected: ", difference)
} else {
fmt.Println("Success: no difference detected")
}
}