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

转到:使用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条)

报告相同问题?

悬赏问题

  • ¥15 装 pytorch 的时候出了好多问题,遇到这种情况怎么处理?
  • ¥20 IOS游览器某宝手机网页版自动立即购买JavaScript脚本
  • ¥15 手机接入宽带网线,如何释放宽带全部速度
  • ¥30 关于#r语言#的问题:如何对R语言中mfgarch包中构建的garch-midas模型进行样本内长期波动率预测和样本外长期波动率预测
  • ¥15 ETLCloud 处理json多层级问题
  • ¥15 matlab中使用gurobi时报错
  • ¥15 这个主板怎么能扩出一两个sata口
  • ¥15 不是,这到底错哪儿了😭
  • ¥15 2020长安杯与连接网探
  • ¥15 关于#matlab#的问题:在模糊控制器中选出线路信息,在simulink中根据线路信息生成速度时间目标曲线(初速度为20m/s,15秒后减为0的速度时间图像)我想问线路信息是什么