dongren1011
2017-03-25 10:28
浏览 63
已采纳

转到:使用Goroutines进行银行转账模拟

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
", 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
", fooStartBalance, fooStartBalance)
    fmt.Printf("  End: foo: %4d, bar: %4d
", 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.

  • 写回答
  • 好问题 提建议
  • 追加酬金
  • 关注问题
  • 邀请回答

2条回答 默认 最新

  • drux41001 2017-03-25 11:15
    最佳回答

    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
    ", fooStartBalance, fooStartBalance)
    fmt.Printf("  End: foo: %4d, bar: %4d
    ", 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
    ", 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?

    评论
    解决 无用
    打赏 举报
查看更多回答(1条)

相关推荐 更多相似问题