[toc]

Original is not easy, more dry goods, public concern: Qiya cloud storage

Concurrency classic scene

One particularly classic scenario in Go concurrent programming is when objects are created concurrently. General pseudocodes are as follows:

if ( /* If the object does not exist */) {
    // Create an object
}
Copy the code

In a concurrent environment, multiple Goroutines make the same judgments in a short period of time: they all decide that the object does not exist, and they all behave in the same way. Each Goroutine grinds and creates. If this is not controlled, it will lead to program logic problems.

Causes objects to be created multiple times and may be replaced and discarded constantly.

How to solve it?

Lock mutex

The simplest method: lock mutually exclusive. Make sure that the judge and create actions are atomic operations, so that there is no time window for concurrent errors, and the above problems will not exist.

lock ...
{
    if ( /* If the object does not exist */) {
        // Create an object
    }
}
unlock ...
Copy the code

Lock is not terrible, lock conflict is terrible, if every time to grab a lock, that performance is not worth it.

Once

The concurrent library in Go has another implementation for this: the sync.once object. This is a very compact implementation, with extremely minimal object implementation code. This library conveniently implements Go’s single-instance design pattern.

Another way to think about it, we don’t have to make judgments, and we need a library that provides semantics that are executed once, so that’s what we want.

That’s right, it calls Once.Do to create the object without the business even having to make extra judgments, as follows:

once.Do(/* Create an object */)
Copy the code

Yes, it’s that simple, the above call is guaranteed to be correct in the context of concurrency. So what semantics does Sync.once guarantee externally?

Highlight: Ensure that the passed function is executed only once.

One thing to think about here is: do you execute once for the library or for the instance?

Highlight: The semantics of executing once are bound to the concrete once variable.

How to understand? Here’s an example:

var once1 sync.Once
var once2 sync.Once

once1.Do( f1 )
once2.Do( f2 )
Copy the code

Ensure that f1 and F2 are executed once each.

The singleton pattern

The singleton pattern is arguably the simplest of all design patterns. How does Go implement creating only one object?

Very simple, with the help of the sync.once structure. Here’s a complete example:

// Global variables (we only want to create one)
var s *SomeObject
// Define a once variable
var once sync.Once
// You want to create only one singleton
func GetInstance(a) *SomeObject {
    once.Do(func(a){
        // Create an object and assign a pointer to the global variable
        s = &SomeObject{}
    })
    return s
}
Copy the code

Thus, we implement the singleton pattern, with only one object returned per call to the GetInstance function. So how does sync.once work?

Once the implementation of the

The following is the full source code implementation of Once instance, very short, but also very interesting.

package sync
import (
    "sync/atomic"
)
type Once struct {
    done uint32
    m    Mutex
}

func (o *Once) Do(f func(a)) {
    // Why is cas not used here?
    if atomic.LoadUint32(&o.done) == 0 {
        o.doSlow(f)
    }
}

func (o *Once) doSlow(f func(a)) {
    o.m.Lock()
    defer o.m.Unlock()
    if o.done == 0 {
        // Question to consider: why defer?
        defer atomic.StoreUint32(&o.done, 1)
        f()
    }
}
Copy the code

The sync.once implementation above is very brief, but there are two things worth thinking about.

  1. whyOnce.DoIt’s useless insidecasJudgment? Isn’t atomic manipulation faster?
  2. whyOnce.doSlowwithdeferTo add the count instead of doing it directly?

Ponder: Why not use cas atomic judgment?

What is CAS?

Go there is atomic.Com pareAndSwapUint32 can realize the function of the cas. Cas stands for Compare And Swap, wrapping judgment And assignment into an atomic operation. Let’s take a look at the pseudo-code implementation of CAS:

func cas(p : pointer to int, old : int.new : int) bool {
    // *p does not equal old, returns false
    if*p ! = old {return false
    }
    // *p equals old, and returns true
    *p = new
    return true
}
Copy the code

The above is the pseudo-code implementation of CAS, which guarantees that the above logic is atomic. Consider why Once can’t be implemented as follows:

if atomic.CompareAndSwapUint32(&o.done, 0.1) {
    f()
}
Copy the code

At first glance, it seems possible to implement the Once semantics, right?

This code looks like it will assign O.tone ==1 when O.tone == 0, and then execute f(). If o.one == 1, it will not enter the branch.

So why didn’t you do it with atoms? Isn’t cas atomic operation the best performance?

Fine execution, while guaranteed to execute only once, has a fatal flaw: there is no guarantee that f() has not completed when O.Done ==1. Golang’s standard library also addresses this.

// Do guarantees that when it returns, f has finished. // This implementation would not implement that guarantee: // given two simultaneous calls, the winner of the cas would // call f, and the second would return immediately, without // waiting for the first’s call to f to complete.

When O.tone is judged to be 0, it is immediately set to 1, and then the f() function is executed.

Once not only ensures that it is executed only Once, but also that it is completed when other users see o.Done ==1 causing Once.Do to return.

Is this semantics important?

It’s very important. It’s logical correctness. For example, we use Once.Do to create a unique global variable object. If you reply to the user that Once.

So what’s the solution? The solution is very simple, two ideas:

  1. Thermal path: atomic reado.doneTo ensure that the race conditions are correct;
  2. Since it doesn’t workcasAtomic operation, then use locking mechanism to ensure atomicity. ifo.done == 0, then take the slow path, note:All the following logic is in one big lock
    1. To perform firstf()Functions;
    2. And then set it upo.done1;

The first time might be when the locks are mutually exclusive, it might be slow. Because to grab the lock, but as long as the execution once, will not walk into the lock logic. It’s all atomic reading, and it’s very fast.

Now that we’re talking about locks, let’s look at a deadlock example. Once uses internal locks to ensure critical sections of code, so do not use nested, otherwise it will deadlock. As follows:

once1.Do( func(a){
    once1.Do( func(a){
        /* something */})})Copy the code

The above code deadlocks on calls to once1.m.lock ().

Highlight: Don’tsync.OnceUse complex, keep it simple, nesting is easy to deadlock.

Think: WhydoSlowdeferTo add the count instead off()And then directly?

Once. DoSlow operates entirely within the lock, so this code is serialized. If o.tone is 0, f has not been performed. Register a defer function defer atomic.storeuint32 (&o.tone, 1) and run f().

func (o *Once) doSlow(f func(a)) {
    o.m.Lock()
    defer o.m.Unlock()
    if o.done == 0 {
        // Question to consider: why defer?
        defer atomic.StoreUint32(&o.done, 1)
        f()
    }
}
Copy the code

Why defer here to ensure that o.Day is assigned to 1? Atomic.StoreUint32(&O.Tone, 1) after f().

Not good! Panic exceptions cannot be handled. Here’s an example:

If you do not defer, there will be no O.tone increment when a panic occurs during f() execution (the process is recovered by the outer layer), but f() has already been executed, which is a semantic violation.

As we said earlier, the function that defer registered will be executed even if a panic occurs inside f(), so the semantics outside of Once are guaranteed: o.Tone must be non-zero if it has been executed Once.

However, if f() causes panic for some reason, it may not complete, and in that case, it will no longer execute once. Do because it has already been executed Once. The business takes this responsibility on itself, and the framework has done its best.

Once the semantics of the

The semantics provided by Once are summarized here:

  1. Once.DoEnsure that the semantics are invoked only once, regardlessf()Internal execution is complete (panic);
  2. onlyf()Execution completed,Once.DoWill return, otherwise block waitf()The first execution of the

Lock grab brief demonstration:

General operation after:

Original is not easy, more dry goods, public concern: Qiya cloud storage

conclusion

  1. OnceForeign offerf()Semantics that are invoked only once;
  2. Once.DoAnd then, by convention,f()Must have been executed once and only once. If the execution is not complete, the wait is blockedf()The first execution of the
  3. OnceThe semantics that are executed once are bound to the instance, multipleOnceFor instance, each instance has a chance;
  4. Internal useLocking mechanismTo ensure the atomicity of the logic, execute firstf()And then seto.doneIdentity;
  5. OncedeferMechanism to ensurepanicThe scene can also be guaranteedo.doneThe identity bit is set.
  6. OnceExample must pay attention to, do not nest, internal lock, misuse of words easy deadlock;

Original is not easy, more dry goods, public concern: Qiya cloud storage