【问题标题】:Go: Bank Transfer Simulation using GoroutinesGo:使用 Goroutines 模拟银行转账
【发布时间】:2017-03-25 10:28:41
【问题描述】:

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

我有两个聚会,foo 和 bar。每一方都有一份银行账户清单,其中包含余额和用于识别的号码。 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
    }
}

这是我的 Transfer 结构:

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

或丢失:

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

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

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

【问题讨论】:

    标签: go concurrency goroutine


    【解决方案1】:

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

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

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

    但是,goroutines 可能不会在此时完成。您需要等待它们结束,然后才能确保传输完成。你能自己想办法吗?

    【讨论】:

    • 谢谢,我不知道那个工具 经过 10 年的发展,现在工具似乎已经相当成熟了。当我在向状态通道发送日志消息时输出整个传输对象时,会发生一次数据竞争。我可以避免这种情况。另一个问题似乎是最后的求和。但我不明白为什么这会是个问题,因为那时所有频道都已经关闭了。
    • 现在我创建了一个 GetBalance() 函数/方法,它使用一个互斥锁来访问平衡。数据竞争检测器现在不再检测到任何数据竞争。但它们仍然会发生。
    • 感谢您指出日志记录问题。但我不只是假设 goroutine 的工作立即完成。他们一完成就通过状态通道写回(发生 TRANSFERS 次)。然后他们必须完成,对吧?
    • 当消息传输被写入状态通道时,您知道最后一个传输消息被写入和接收,而不是被处理。尝试获取互斥体允许调度程序移动到另一个线程,例如回到编写传输的例程,然后愉快地继续前进。但是,实际上并没有完成转移。请注意,我不确定这是您面临的 问题;我确实认为这是您代码中的一个问题。
    【解决方案2】:

    谢谢,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")
        }
    }
    

    【讨论】:

      猜你喜欢
      • 2014-06-20
      • 1970-01-01
      • 2014-08-27
      • 1970-01-01
      • 2017-01-30
      • 1970-01-01
      • 2019-07-22
      • 2020-10-26
      • 1970-01-01
      相关资源
      最近更新 更多