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

时间:2017-03-25 10:28:42

标签: go concurrency goroutine

对于大学,我不得不用Java实现银行转账模拟。完成后,我想在Go中实现它,因为我听到了很多关于Go的并发功能并希望尝试它们。

我有两个派对,foo和bar。每一方都有一个银行账户清单,其中包含余额和一个用于识别的号码。 foo的每个帐户都应该将一定金额转移到其中一个帐户。这些转移应分成较小且不太可疑的转移,重复转移一个单位,直到转移全部金额。同时,bar将相同的金额转回foo,因此foo和bar的帐户的总和应分别在开头和结尾处相同。

这是我的帐户结构:

S (S (plus k j))

这是帐户为了接收付款而必须运行的功能/方法(我将付款作为负数付款):

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
    }
}

以下是通过渠道向每个帐户进行一系列小额付款的功能/方法:

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)
}

但要么创造了资金:

 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

所以我认为(用我的Java心态)问题可能是Account.Listen():也许平衡由Goroutine A读取,然后是Goroutine B,完全执行Account.Listen(),然后Goroutine A继续做用旧值计算。互斥锁可能会修复它:

Start: foo: 225000, bar: 225000
  End: foo: 225053, bar: 225053

哪个效果很好......十次九次。但那时:

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
}

正如我所说,数据竞争检测器现在保持沉默,但我在大约每10次运行中仍然有一些错误:

fooEndBalance += fooAccounts[i].GetBalance()
barEndBalance += barAccounts[i].GetBalance()

我真的不知道自己做错了什么。

2 个答案:

答案 0 :(得分:2)

由于这是作业(并且感谢你这么说),这里有一个线索。

  

我真的不知道其他地方的同步可能是个问题。

每当遇到此问题时,请使用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。

真正的问题是你运行你的执行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
...

但是,此时可能无法完成goroutine。在确定传输完成之前,您需要等待它们结束。你能找到一种自己做的方法吗?

答案 1 :(得分: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")
    }
}