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

标签 go concurrency goroutine

对于大学,我必须用 Java 实现银行转账模拟。完成之后,我想在 Go 中实现它,因为我听说了很多关于 Go 的并发能力并且想尝试一下。

我有两个聚会,foo 和 bar。每一方都有一份银行账户 list ,其中包含余额和用于识别的号码。 foo 的每个账户都应该向 bar 的一个账户转账一定的金额。这些转账应分成较小且可疑性较小的转账,重复转账一个单位,直到转账全部金额。同时,bar 将相同的金额转回给 foo,因此 foo 和 bar 的账户在开始和结束时的总和应该相同。

这是我的帐户结构:

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

以下是通过 channel 向每个帐户执行大量小额支付的函数/方法:

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

或者丢失:

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

所以我认为(以我的Java思维)问题可能是Account.Listen():也许Balance被Goroutine A读取,然后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

这很奇怪。互斥锁似乎很有帮助,因为它在大多数情况下都有效,而当它不起作用时,它只会关闭一个。我真的不明白同步在其他什么地方可能会出现问题。

更新:当我按如下方式实现帐户时,无法防止数据争用警告:

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

我真的不明白我做错了什么。

最佳答案

由于这是家庭作业(并且感谢您这么说),因此这里有一个线索。

I really don't get at what other place synchronization might be an issue.

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

真正的问题是您运行 Execute goroutine,并假设它们的工作立即完成:

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 可能还没有完成。您需要等待他们结束才能确保传输完成。你能自己找到办法做到这一点吗?

关于Go:使用 Goroutine 进行银行转账模拟,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/43015259/

相关文章:

go - golang 中的 map[T]struct{} 和 map[T]bool

go - 为 struct field 找到无效字段,需要为关系定义外键或需要实现 Valuer/Scanner 接口(interface)

Go net/http 在高负载下内存泄漏

java - 为什么 while 循环后 boolean 变量仍然为 true?

java - MyBatis SelectList 输出 CopyOnWriteArrayList

iphone - 为什么我更愿意为每个新线程或 NSOperation 创建一个 NSManagedObjectContext 而不是在主线程上调用核心数据?

loops - 去审核-循环变量我被func文字捕获

go - 如何循环创建 channel ?

go - 如何在 gRPC 中识别断开连接的客户端?

go - 如何等待一组goroutines?