英文:
Go: Bank Transfer Simulation using Goroutines
问题
为了大学的课程,我需要用Java实现一个银行转账模拟。完成后,我想用Go来实现,因为我听说Go有很强的并发能力,想试试看。
我有两个参与方,foo和bar。每个参与方都有一个带有余额和身份编号的银行账户列表。foo的每个账户都应该向bar的一个账户转账一定金额。这些转账应该被分成较小且不太可疑的转账,重复转账一单位,直到全部金额转移完成。与此同时,bar也向foo转账相同金额,以便foo和bar的账户余额在开始和结束时分别相等。
这是我的Account结构体:
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)
}
这是账户需要运行的接收付款的函数/方法(我将支出的付款实现为负数金额的付款):
func (account *Account) Listen(channel <-chan int) {
for amount := range channel {
account.Balance += amount
}
}
这是我的Transfer结构体:
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)
}
这是通过通道执行一系列微支付来执行付款的函数/方法:
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)
}
正如stdout所显示的,所有的转账在最后都完成了:
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()上:也许Goroutine A读取了Balance,然后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
这非常奇怪。互斥锁似乎有帮助,因为它大多数时候都有效,而且当它不起作用时,只有一个差错。我真的不明白还有哪些地方可能存在同步问题。
更新:当我按照以下方式实现Account时,我现在能够避免数据竞争警告:
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
我真的不明白我做错了什么。
英文:
For university, I had to implement a bank transfer simulation in Java. Having done that, I wanted to implement it in Go, because I heard a lot about Go's concurrency capabilities and wanted to try them out.
I have two parties, foo and bar. Each party has a list of bank accounts with a balance and a number for identification. Every of foo's accounts should transfer a certain amount to one of bar's accounts. Those transfers should be split up in smaller and less suspicious transfers, transferring one unit repeatedly until the whole amount was transferred. At the same time, bar is transferring the same amount back to foo, so that the sum of foo's and bar's accounts, respectively, should be the same at the beginning and at the end.
Here's my Account struct:
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)
}
This is the function/method the account has to run in order to receive payments (I implemented outgoing payments as payments of negative amounts):
func (account *Account) Listen(channel <-chan int) {
for amount := range channel {
account.Balance += amount
}
}
And here's my Transfer struct:
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)
}
Here's the function/method that performs the payment in a bunch of micro payments over a channel to each account:
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)
}
And, finally, here's the actual program:
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)
}
As the stdout shows, all the transfers have been done at the end:
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.-
But money is either created:
Start: foo: 225000, bar: 225000
End: foo: 225053, bar: 225053
Or lost:
Start: foo: 225000, bar: 225000
End: foo: 225053, bar: 225053
So I thought (with my Java mindset) that the problem might be Account.Listen(): maybe Balance is read by Goroutine A, then comes Goroutine B, executing Account.Listen() completely, then Goroutine A goes ahead doing the calculation with the old value. A mutex might fix it:
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()
}
}
Which works great... nine ouf of ten times. But then:
Start: foo: 225000, bar: 225000
End: foo: 225001, bar: 225001
This is very strange. The mutex seems to help, because it works most of the time, and when it doesn't work, it's only off by one. I really don't get at what other place synchronization might be an issue.
Update: I am no able to prevent data race warnings when I implement Account as follows:
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
}
And I also access the Balance at the end like this:
fooEndBalance += fooAccounts[i].GetBalance()
barEndBalance += barAccounts[i].GetBalance()
As I said, the data race detecter now stays silent, but I still got some errors in roughly every 10th run:
Start: foo: 100000, bar: 100000
End: foo: 99999, bar: 99999
I really don't get what I'm doing wrong.
答案1
得分: 2
由于这是作业(感谢你说明),这里有一个提示。
每当你遇到这个问题时,使用Go数据竞争检测器。它对你的代码有一些意见。
[编辑]
另一个问题:
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可能还没有完成。在确保转账完成之前,你需要等待它们完成。你能自己找到一种方法来做到这一点吗?
英文:
Since this is homework (and thanks for saying so), here is a clue.
> I really don't get at what other place synchronization might be an issue.
Whenever you run into this question, use the Go data race detector. It has a few things to say about your code.
[Edit]
Another problem:
fmt.Printf("Start: foo: %4d, bar: %4d\n", fooStartBalance, fooStartBalance)
fmt.Printf(" End: foo: %4d, bar: %4d\n", fooEndBalance, fooEndBalance)
You print foo twice, instead of foo and bar.
The real problem is that you run your Execute goroutines, and assume that their work is finished immediately:
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)
Here, you consider the job done and move on to printing the result:
fooEndBalance := 0
barEndBalance := 0
...
However, the goroutines may not be done at this point. You need to wait for them to be over before being sure that the transfer is done. Can you find a way to do that by yourself?
答案2
得分: 0
谢谢,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")
}
}
英文:
Thanks, Zoyd, you helped me to point out the issue. The issue is that the status is reported without having waited for both Listen methods. Here's what I'm doing now:
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
}
It looks quite clumsy to me. I try to improve on that, but the issue now seems to be gone.
Edit: I now tried to simplify the code a bit. It's now working and the data race detector doesn't complain anymore, even though I don't use methods to access the balance.
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")
}
}
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论