How does golang gracefully implement concurrent choreography tasks

Time:2022-5-13

The article continues to be updated. Wechat searches “Wu Qinqiang’s late night canteen”

Business scenario

When doing task development, you will encounter the following scenarios:

Scenario 1: when calling a third-party interface, you need to call different interfaces for data assembly.
Scenario 2: an application home page may rely on many services. That involves the interface that needs to request multiple services at the same time when loading the page. This step is often called by the back-end to assemble the data and then return it to the front-end, that is, the so-called BFF (back end for front end) layer.

For the above two scenarios, assuming that serial call is selected without strong dependency, the total time consumption is:

time=s1+s2+....sn

According to the millions of promising young people in modern times, they have greeted your ancestors for 18 generations for such a long time.

For great KPIs, we often choose to call these dependent interfaces concurrently. Then the total time is:

time=max(s1,s2,s3.....,sn)

Of course, when you start stacking business, you can serialize it first. When the people above are worried, you can show your unique skill.

In this way, the PPT at the end of the year can be added with a strong daily account: improve XXX performance for an interface of the business, and indirectly generate XXX value.

Of course, the premise of all this is that being a boss doesn’t understand technology, and being a technology “understands” you.

To get back to business, you might write this if you change it to concurrent calls,

package main

import (
    "fmt"
    "sync"
    "time"
)

func main() {
    var wg sync.WaitGroup
    wg.Add(2)

    var userInfo *User
    var productList []Product
    
    go func() {
        defer wg.Done()
        userInfo, _ = getUser()
    }()

    go func() {
        defer wg.Done()
        productList, _ = getProductList()
    }()
    wg.Wait()
    fmt. Printf ("user information:% + V \ n", userinfo)
    fmt. Printf ("product information:% + V \ n", productlist)
}


/********User services**********/

type User struct {
    Name string
    Age  uint8
}

func getUser() (*User, error) {
    time.Sleep(500 * time.Millisecond)
    var u User
    u.Name = "wuqinqiang"
    u.Age = 18
    return &u, nil
}

/********Goods and services**********/

type Product struct {
    Title string
    Price uint32
}

func getProductList() ([]Product, error) {
    time.Sleep(400 * time.Millisecond)
    var list []Product
    list = append(list, Product{
        Title: "SHib",
        Price: 10,
    })
    return list, nil
}

Let’s ignore other problems. In terms of implementation, how many services do you need and how many will you openG, usesync.WaitGroupThe characteristics of,
Achieve the effect of arranging tasks concurrently.

It seems that the problem is not big.

But with the code996With the increase of business scenarios, you will find that many modules have similar functions, but the corresponding business scenarios are different.

So can we draw out a set of tools for this business scenario and hand over the specific business implementation to the business party.

Arrangement.

use

Based on the principle of not repeatedly building wheels, I searched for open source projects and finally fell in love with themgo-zeroA tool insidemapreduce
From the file name, we can see what it is, and we can do it by ourselvesGoogleThis noun.

It’s easy to use. Let’s transform the above code through it:

package main

import (
    "fmt"
    "github.com/tal-tech/go-zero/core/mr"
    "time"
)

func main() {
    var userInfo *User
    var productList []Product
    _ = mr.Finish(func() (err error) {
        userInfo, err = getUser()
        return err
    }, func() (err error) {
        productList, err = getProductList()
        return err
    })
    fmt. Printf ("user information:% + V \ n", userinfo)
    fmt. Printf ("product information:% + V \ n", productlist)
}
User information: & {Name: wuqinqiang age: 18}
Product information: [{Title: Shib price: 10}]

Is it much more comfortable.

But one more thing to note here is that suppose one of the services you call is wrong and youreturn errIf there is a corresponding error, other invoked services will be cancelled.
For example, we modify getproductlist to directly respond to errors.

func getProductList() ([]Product, error) {
    return nil, errors.New("test error")
}
//Print
User information: < nil >
Product information: []

Then, even the user information will be empty when printing. Because of a service error, the user service request is cancelled.

In general, when requesting service errors, we will have a minimum operation. A service error cannot affect the results of other requests.
Therefore, the specific processing depends on the business scenario.

Source code

Now that it’s used, catch up with the source code.

func Finish(fns ...func() error) error {
    if len(fns) == 0 {
        return nil
    }

    return MapReduceVoid(func(source chan<- interface{}) {
        for _, fn := range fns {
            source <- fn
        }
    }, func(item interface{}, writer Writer, cancel func(error)) {
        fn := item.(func() error)
        if err := fn(); err != nil {
            cancel(err)
        }
    }, func(pipe <-chan interface{}, cancel func(error)) {
        drain(pipe)
    }, WithWorkers(len(fns)))
}
func MapReduceVoid(generator GenerateFunc, mapper MapperFunc, reducer VoidReducerFunc, opts ...Option) error {
    _, err := MapReduce(generator, mapper, func(input <-chan interface{}, writer Writer, cancel func(error)) {
        reducer(input, cancel)
        drain(input)
        // We need to write a placeholder to let MapReduce to continue on reducer done,
        // otherwise, all goroutines are waiting. The placeholder will be discarded by MapReduce.
        writer.Write(lang.Placeholder)
    }, opts...)
    return err
}

aboutMapReduceVoidFunction, mainly to view three closure parameters.

  • firstGenerateFuncUsed for production data.
  • MapperFuncRead the produced data and process it.
  • VoidReducerFuncIt means wrong heremapperThe data after is aggregated and returned. So this closure almost 0 works here.
func MapReduce(generate GenerateFunc, mapper MapperFunc, reducer ReducerFunc, opts ...Option) (interface{}, error) {
    source := buildSource(generate) 
    return MapReduceWithSource(source, mapper, reducer, opts...)
}

func buildSource(generate GenerateFunc) chan interface{} {
    Source: = make (Chan interface {}) // create unbuffered channel
    threading.GoSafe(func() {
        defer close(source)
        Generate (source) // start production data
    })

    Return source // returns an unbuffered channel
}

buildSourceFunction returns an unbuffered channel. And open aGfunctiongenerate(source), plug data into the unbuffered channel. thisgenerate(source)No, it was the beginningFinishThe first closure parameter passed.

return MapReduceVoid(func(source chan<- interface{}) {
    //That's it
        for _, fn := range fns {
            source <- fn
        }
    })

Then viewMapReduceWithSourceFunction,

func MapReduceWithSource(source <-chan interface{}, mapper MapperFunc, reducer ReducerFunc,
    opts ...Option) (interface{}, error) {
    options := buildOptions(opts...)
    //Task execution end notification signal
    output := make(chan interface{})
    //Write the data processed by mapper to the collector
    collector := make(chan interface{}, options.workers)
    //Cancel operation signal
    done := syncx.NewDoneChan()
    writer := newGuardedWriter(output, done.Done())
    var closeOnce sync.Once
    var retErr errorx.AtomicError
    finish := func() {
        closeOnce.Do(func() {
            done.Close()
            close(output)
        })
    }
    cancel := once(func(err error) {
        if err != nil {
            retErr.Set(err)
        } else {
            retErr.Set(ErrCancelWithNil)
        }

        drain(source)
        finish()
    })

    go func() {
        defer func() {
            if r := recover(); r != nil {
                cancel(fmt.Errorf("%v", r))
            } else {
                finish()
            }
        }()
        reducer(collector, writer, cancel)
        drain(collector)
    }()
    //Actually get data from the generator channel and execute mapper
    go executeMappers(func(item interface{}, w Writer) {
        mapper(item, w, cancel)
    }, source, collector, done.Done(), options.workers)

    value, ok := <-output
    if err := retErr.Load(); err != nil {
        return nil, err
    } else if ok {
        return value, nil
    } else {
        return nil, ErrReduceNoOutput
    }
}

This code is quite long. Let’s talk about the core point. We see using aGcallexecuteMappersmethod.

go executeMappers(func(item interface{}, w Writer) {
        mapper(item, w, cancel)
    }, source, collector, done.Done(), options.workers)
func executeMappers(mapper MapFunc, input <-chan interface{}, collector chan<- interface{},
    done <-chan lang.PlaceholderType, workers int) {
    var wg sync.WaitGroup
    defer func() {
        //Wait until all tasks are completed
        wg.Wait()
        //Close channel
        close(collector)
    }()
   //Create a worker pool based on the specified number
    pool := make(chan lang.PlaceholderType, workers) 
    writer := newGuardedWriter(collector, done)
    for {
        select {
        case <-done:
            return
        case pool <- lang.Placeholder:
            //Fetch data from the unbuffered channel returned by buildsource()
            item, ok := <-input 
            //When the channel is closed, it ends
            if !ok {
                <-pool
                return
            }

            wg.Add(1)
            // better to safely run caller defined method
            threading.GoSafe(func() {
                defer func() {
                    wg.Done()
                    <-pool
                }()
                //Where the closure function actually runs
               // func(item interface{}, w Writer) {
               //    mapper(item, w, cancel)
               //    }
                mapper(item, writer)
            })
        }
    }
}

The specific logic has been noted, and the code is easy to understand.

onceexecuteMappersFunction return, closecollectorChannel, then executereducerNo more blocking.

go func() {
        defer func() {
            if r := recover(); r != nil {
                cancel(fmt.Errorf("%v", r))
            } else {
                finish()
            }
        }()
        reducer(collector, writer, cancel)
        //Here
        drain(collector)
    }()

therereducer(collector, writer, cancel)It’s actually fromMapReduceVoidThe third closure function passed.

func MapReduceVoid(generator GenerateFunc, mapper MapperFunc, reducer VoidReducerFunc, opts ...Option) error {
    _, err := MapReduce(generator, mapper, func(input <-chan interface{}, writer Writer, cancel func(error)) {
        reducer(input, cancel)
        //Here
        drain(input)
        // We need to write a placeholder to let MapReduce to continue on reducer done,
        // otherwise, all goroutines are waiting. The placeholder will be discarded by MapReduce.
        writer.Write(lang.Placeholder)
    }, opts...)
    return err
}

Then the closure function executes againreducer(input, cancel), herereducerThat’s what we explained at the beginningVoidReducerFunc, fromFrom finish()

How does golang gracefully implement concurrent choreography tasks

Wait, see the three places abovedrain(input)Did you?

// drain drains the channel.
func drain(channel <-chan interface{}) {
    // drain the channel
    for range channel {
    }
}

It’s actually an evacuationchannelBut all three places are for the same onechannel, it also puzzles me.

There is a more important point.

go func() {
        defer func() {
            if r := recover(); r != nil {
                cancel(fmt.Errorf("%v", r))
            } else {
                finish()
            }
        }()
        reducer(collector, writer, cancel)
        drain(collector)
    }()

The above code, if executedreducerwriterWrite triggerpanic, thendrain(collector)It will get stuck directly.

However, the author has fixed this problem and directly putdrain(collector)Put intodefer
How does golang gracefully implement concurrent choreography tasks

Specific issues [1].

Here, aboutFinishThe source code of is over. Interested can see other source code.

love itgo-zeroSome tools in, but some tools often used are not independent,
Depending on other packages, you need to install the whole package when you want to use only one of them.

appendix

[1]
https://github.com/tal-tech/g…

Recommended Today

How to modify OEM computer configuration information

There will be some OEM information during computer installation, including manufacturer’s brand, contact information, etc. some friends may need to modify and customize the information they want. Here is a method.  1. Before making changes, first let’s take a look at the OEM information of our computer system, my computer – > right click – […]