This is a creation in Article, where the information may have evolved or changed.
Background
During this time, the basic tool library of some services was reconstructed, mainly the decoupling Pub-sub changed to asynchronous system [Eventbus], and the timer was adjusted simply [clock]. It was thought that the business had been drastically simplified, resulting in May, one of the services because of broadcast events, which led to deadlocks. After analysis, it is found that a very basic problem has been caused and deserves a smooth stroke.
The cause of the problem is basically this:
There is a service object that, through RPC, provides multiple public services externally and can reverse-push messages to clients. which
- There are multiple RPC methods, called by the client, and some require data protection, and a lock is called.
- The service can receive a service-side network Disconnect command, calling the Servernetreset method to reset the network proxy's private property at any time, and also calls the lock.
- The service object subscribes to events that, once triggered, actively invoke the Notify method to push the message to the client. To avoid this operation, the network proxy is emptied by the Servernetreset method, so the private method push of the Notify call also calls the lock.
As a result of a business refactoring, there is a method that Foo invokes the Notify push message in some cases, and because Foo has object-private data maintenance, the lock is used directly. As a result, a deadlock condition occurs.
I believe there are a lot of things like this, but we didn't find them.
Finally, we improve the solution is: Network reset, message push these two operations, through the message of serial execution, no longer lock.
Usage Scenarios
When learning go, a lot of data mentions: "Multi-Channel (Chan), less use of locks." For long-term custom synchronous programming, direct calls between methods, the understanding of which is not deep, many people more to the Chan as a signal transmission. Because asynchronous calls involve event definitions, subscribing to the publishing system, and delaying the return, it is far from straightforward to invoke the method directly. Therefore, a project with tens of thousands of lines of code will use a large number of locks to protect object properties.
If you want to adopt a channel without a lock, you have to weigh the three aspects of "development efficiency", "operational efficiency" and "resource consumption". In simple terms:
Basic ToolPak objects, one-way references, and recommended locks.
By locking the internal properties of the object, it is most efficient to synchronously invoke the public method provided by the object directly. If the object runs a sample that does not need to be "interrelated" with other instances, but is simply referenced, then the lock is completely fine and the simplest.
For example, we do a support concurrency counter, there is no reference to third-party objects, this time, only need to use a lock. Like what:
//counter Counter is a multi-thread safe counterstypeCounterstruct{mut sync. MutexcurrnumInt64 //Current quantityMaxnumInt64 //Maximum quantity}//addone based on the original internal count, +1. func (c *counter) addone () int {New: = Atomic. AddInt64 (&c.currnum,1) C.mut.lock ()ifC.maxnum <New{C.maxnum =New}c.mut.unlock ()return int(C.currnum)}//decone based on the original internal count,-1. func (c *counter) decone () int {return int(Atomic. AddInt64 (&c.currnum,-1))}//current Gets the current internal count result. func (c *counter) current () int {return int(Atomic. LoadInt64 (&c.currnum))}the maximum count for the//maxnum counter during the life cycle. func (c *counter) maxnum() int {return int(Atomic. LoadInt64 (&c.maxnum))}//newcounter Counter Constructor func newcounter() *Counter {return&counter{}}
The business object, especially between the clusters mentioned in DDD, takes precedence over decoupling the message framework.
Because of the complexity of reference between objects, the most easy to understand is the battle scene of World of Warcraft: A number of players on both sides to cooperate with each other, constantly cast attacks, auxiliary skills, the process of some heroes use props, and some were attacked lead to death. If you take a synchronous call, object A calls the object B,b call C,c after execution completes, it is very complicated to need to tell a again under a certain condition. At this time, we consider the ECS framework, the basic is the PUB-SUB system support.
For the use of pubsub, different system interface slightly different, we are also more familiar with, here is not an example.
In complex business objects, it is recommended to use an asynchronous message
If an instance has more than one public method + Private method, which is similar to the background of the previous issue, it has both command-driven and internal message framework driven by the external UI. In consideration of concurrency, it is recommended to use a serial asynchronous approach when locking is introduced. all business methods are not external, the object only creates, accepts the message, destroys three external public methods. All messages have only one entry, so they don't have to be locked. The code structure is simple:
typeMessage1struct{}typeMessage2struct{}typeAstruct{Close Int32 //Whether the object is closed flagMsgbufChan Interface{}//Message buffering} func Newa() *A {A: = &a{msgbuf: Make(Chan Interface{},Ten),}GoA.receive ()returnA func (a *a) Post(message interface {}){ifAtomic. LoadInt32 (&a.Close) ==1{a.msgbuf <-message}} func (a *a) receive() {//Defer for simple fault isolationdefer func() {ifERR: =Recover(); Err! =Nil{log. PRINTLN (Err)}} ()//Execute message processing forMessage: =RangeA.msgbuf {Switchmsg: = message. (type) { CaseMessage1:a.foo1 (msg) CaseMessage2:a.foo2 (MSG)}}} func (a *a) foo1(message Message1) {} func (a *a) foo2(message Message2) {} func (a *a) Close() {ifAtomic.compareandswapint32 (&a.Close,0,1) {//do other thing}}//...
In particular, even as a consumer, subscribing to events in the Pub-sub system only passes the post as the function handle of the event response, so that even if the complex system does not occur because the multi-method executes the internal properties of the operation, it needs to lock protection, which brings the negative problems.
Of course, the use of asynchronous message objects requires basic skills and additional work:
- Clear architecture. For example, the RPC service object is one, but the method in which it is called is parallel. The reason is that rpc/server.go Line481 used the co-process:
goservice.call(serversendingmtypereqargvreplyvcodec)
- The earlier frame is not as intuitive as synchronous invocation, the construction is slow, the benefit should be reflected in maintenance.
- Some message replies can be cumbersome, requiring future, Promise, callback, and the Go standard library does not have native support.
For the future, Promise, callback mode, it is very simple, essentially, the asynchronous framework provides for the value object between business objects. Go native Waitgroup, cond, Chan have provided good native support and are easily extensible.
In addition, the Actor model from theory to practice for 40 years is also very good, compared to Scala, Java and so on, go memory advantage is very obvious, also very good. Because these two parts involve more design patterns of use, content, there is time to open another said.