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.
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?