This is a creation in Article, where the information may have evolved or changed.
English Original: "Sane Concurrency with Go"
Glyph Lefkowitz recently wrote an introductory article in which he detailed some of the challenges of developing high-concurrency software, and if you develop software but do not read this article, then I suggest you read one. This is a very good article, modern software engineering should have a wealth of wisdom. Extracted from multiple tidbits, but if I ventured to summarize the main points of view, the idea is that the combination of preemptive multitasking and general shared state leads to the complexity of the software development process that is not manageable, and developers may prefer to keep some of their sanity to avoid this kind of non-manageable complexity. Preemptive scheduling is good for real parallel tasks, but when mutable states are shared through multiple concurrent threads, explicit multitasking is more pleasing. |
Mo Tian Translated 6 months ago 1 Person top top translation of good Oh! |
Despite the collaborative multitasking, your code can still be complex, and it just has the opportunity to maintain a manageable amount of complexity. When control transfer is clear a code reader has at least some visible indications that things may be out of track. There is no clear mark that each new stage is a potential mine: "If this operation is not atomic, what happens at the end?" "Then the space between each command becomes an endless space black hole, and the awful heisenbugs appears Over the past year, although work on Heka (a high-performance data, log and indicator processing engine) has mostly been developed using the go language. One of the highlights of go is that the language itself has some very useful concurrency primitives. But what about the concurrency of go, which needs to be observed by encouraging code footage that supports local reasoning. |
Mo Tian Translated 6 months ago 1 Person top top translation of good Oh! |
Not all the facts are good. All Goroutine access the same shared memory space, the state defaults to variable, but the Go Scheduler does not guarantee the accuracy of the context selection process. In a single-core setup, the run time of go goes to the "implicit collaborative work" category, and the list of asynchronous program models frequently mentioned in glyph is selected in 4. When GorOutine can run in parallel in multicore systems, the world is hard to get. go can't protect you, but it doesn't mean you can't take steps to protect yourself. By using some of the primitives provided by go in the process of writing code, you can minimize the abnormal behavior generated by the associated preemption schedule. Take a look at the following glyph example of the Go interface in the "account Translation" code snippet (ignoring which floating-point numbers are not easy to end up storing fixed-point decimals) func transfer (amount float64, payer, payee *account, server someservertype) error { if payer. Balance () < amount { Return errors. New ("Insufficient funds") } log. Printf ("%s has sufficient funds", payer) Payee. Deposit (amount) log. Printf ("%s received payment", payee) payer. Withdraw (amount) log. Printf ("%s made payment", payer) server. Updatebalances (payer, pAyee) // Assume this is magic and always works. return nil } |
Mo Tian Translated 6 months ago 0 Person Top top translation of good Oh! |
This is obviously not safe, if called from multiple goroutine, because they may get the same result from the deposit schedule concurrently, and then request more of the cancelled call's deposit variables together. It is best that the dangerous part of the code is not executed by multiple goroutine. This feature is implemented in this way: type transfer struct { payer *account payee *account amount float64 } var xferchan = make (Chan *transfer) var errchan = make (chan Error) func init () { go Transferloop () } func transferloop () { for xfer := range xferChan { if xfer.payer.balance < xfer.amount { errchan <- errors. New("Insufficient funds") continue } log. Printf ("%s has sufficient funds", xfer.payer) xfer.payee.deposit (Xfer.amount) log. Printf ("%s received payment", xfer.payee) xfer.payer.withdraw (Xfer.amount) log. Printf ("%s made payment", xfer.payer) errChan <- nil } } &nbSp; func transfer (amount float64, payer, payee *account, server someservertype) error { xfer := &transfer{ payer: payer, payee: payee, amount: amount, } xferChan <- xfer err := <-errchan if err == nil { server. Updatebalances (Payer, payee) // still magic. } return err } There's more code here, but we eliminate concurrency problems by implementing a trivial event loop. When the code is first executed, it activates a goroutine run loop. A forwarding request is passed into a newly created channel for this purpose. The result is returned to the outside of the loop via an erroneous channel. Because the channels are not buffered, they are locked, and through the transfer function regardless of how many concurrent forwarding requests are entered, they are continuously serviced through a single run event loop. |
Mo Tian Translated 6 months ago 0 Person Top top translation of good Oh! |
The code above looks a little awkward, maybe. A mutex (mutex) may be a better choice for such a simple scenario, but what I'm trying to prove is that you can apply an isolated state operation to a go routine. Even slightly awkward, it's good enough for most needs, and it works, even using the simplest account structure: type account struct { balance float64 } func (A *account) balance () float64 { return a.balance } func (a *account) Deposit (amount float64) { log. Printf ("depositing: %f", amount) a.balance += amount } func (a *account) withdraw (amount float64) { log. Printf ("withdrawing: %f", amount) a.balance -= amount } But such a clumsy account implementation would seem naïve. It may be more effective to provide some protection for the account structure itself by not letting any recall operations greater than the current balance. What happens if we turn the recall function into something like this?: Func (a *account) withdraw (amount float64) {If amount > a.balance {log. PRINTLN ("insufficient funds") return} log. Printf ("Withdrawing:%f", amount) a.balance-= amount}
|
Leoxu Translated 6 months ago 0 Person Top top translation of good Oh! |
Unfortunately, this code suffers the same problem as our original Transfer implementation. Concurrent execution or unfortunate context switching means that we may end up with a negative balance. Fortunately, the idea of an internal event loop is also very good, even better, because the event loop Goroutine can be well coupled with each individual account structure instance. Here's an example to illustrate this point: Type account struct {balance float64 Deltachan chan float64 Balancechan chan float64 Errchan Chan Error} func newaccount (Balance float64) (a *account) { a = &Account{ balance: balance, deltachan: make (Chan float64), balancechan: make (Chan float64), errchan: make ( Chan error), } go a.run () return } func (A *account) balance () float64 { return <-a.balancechan } func (a *account) Deposit (amount float64) error { a.deltaChan <- amount return <-a.errchan } func (A *account) withdraw (Amount float64) error { a.deltaChan <- -amount return <-a.errChan } func (A *account) applydelta (Amount float64) error { newBalance := a.balance + amount if newBalance < 0 { return errors. New ("Insufficient funds") } a.balance = newBalance return nil } func (A *account) run () { var delta float64 for { select { case delta = <-a.deltachan : a.errchan <- a.applydelta (Delta) case a.balancechan <- a.balance: // do nothing, we ' Ve accomplished our goal w/ the channel put. } } } This API is slightly different, and the Deposit and withdraw methods now return errors. Instead of processing their requests directly, they put the adjustment of the account balance into Deltachan and access the Deltachan in the event loop when the Run method runs. Similarly, the Balance method continuously requests data in the event loop through blocking until it receives a value through Balancechan. |
Zhao Liang-Blue Sky Translated 6 months ago 0 Person Top top translation of good Oh! |
The key point to note is that the above code, all of the structure of internal data is worth direct access and modification is the event loop triggered by the *within* code to complete. If public API calls are performing well and interacting with the data using only the given channels, then regardless of the number of concurrent calls to public methods, we know that only one of them will be processed at any given time. Our time loop code is much easier to infer. The core of the model is the design of Heke. When Heka starts, it reads the configuration file and launches each plug-in in its own go routine. With the clock signal, shutdown notification, and other control signals, the data is fed into the plug-in via the channel. This encourages plug-in authors to implement plug-in functionality using a schema of event loop types like the one described above. Again, go won't protect you. It is entirely possible to write a Heka plug-in (or any architecture) that is loosely coupled with its internal data management and subject-matter conditions. But there are some small places to be aware of, and the free application of Go controversy detectors, which you can write code whose behavior can be predicted, even in the façade code of preemptive scheduling. |
Leoxu Translated 6 months ago 0 Person Top top translation of good Oh! |
All translations in this article are for learning and communication purposes only, please be sure to indicate the translator, source, and link to this article.
Our translation work in accordance with the CC agreement, if our work has violated your rights and interests, please contact us promptly