• Original address: Concurrent Programming
  • By StefanNilsson
  • Translation from: The Gold Project
  • This article is permalink: github.com/xitu/gold-m…
  • Translator: kobehaha
  • Proofreader: Joyking7 Alfred-Zhong

Concurrent programming

bouncing balls

  • 1. Multithreaded execution
  • 2. Channels
  • 3. The synchronous
  • 4. A deadlock
  • 5. Data competition
  • 6. The mutex
  • 7. Detect data contention
  • 8. Select the id
  • 9. The most basic concurrent instance
  • 10. Parallel computing

This article will introduce concurrent programming using the Go language as an example, including the following

  • Concurrent execution of threads (Goroutines)
  • Basic synchronization techniques (Channels and locks)
  • The basic concurrency mode in Go
  • Deadlocks and data contention
  • Parallel computing

To get started, you need to know how to write a basic Go program. If you are already familiar with C/C++, Java or Python, A Tour of Go will help you. You can also check out Go for C++ programmers or Go for Java programmers.

1. Multithreaded execution

Goroutine is a scheduling mechanism for GO. Go uses the Go declaration to start a new thread of execution with the Goroutine scheduling mechanism. It executes the program in the newly created Goroutine. In a single program, all Goroutines share the same address space.

Goroutine is lighter and less expensive than allocating stack space. The stack space is initialized to be small, and memory needs to be expanded by applying and freeing heap space. Goroutines are internally multiplexed across multiple operating system threads. If a goroutine blocks an operating system thread, for example, while it is waiting for input, other goroutines in that thread will migrate to other threads in order to keep running, and you don’t need to worry about these details.

The following program will print “Hello from main Goroutine “. Whether to print “Hello from another Goroutine “depends on which of the two goroutines finishes first.

func main() {

    go fmt.Println("Hello from another goroutine")
    fmt.Println("Hello from main goroutine"}}}}}}}}}}Copy the code

goroutine1.go

The next program “Hello from main Goroutine “and “Hello from another Goroutine” may print in any order. But there is a possibility that the second Goroutine will run so slowly that it won’t print until the program is over.

func main() {
    go fmt.Println("Hello from another goroutine")
    fmt.Println("Hello from main goroutine") time.sleep (time.second) // Wait 1 Second for the other goroutine to complete}Copy the code

goroutine2.go

For a more practical example, let’s define a function that uses concurrency to defer events.

// After the specified time expires, the text is printed to the standard output // This is not blocked in any way func Publish(text string, delay time.duration) {gofunc() {
        time.Sleep(delay)
        fmt.Println("BREAKING NEWS:", text)}() // Note the parentheses. We must call the anonymous function}Copy the code

publish1.go

You might call the Publish function in the following way

func main() {
    Publish("A goroutine starts a new thread of execution.", 5*time.Second)
    fmt.Println("Let's hope the news will be published before I leave.") // Wait for the message to be published time.sleep (10 * time.second) fmt.println ("Ten seconds later: I’m leaving now.")}Copy the code

publish1.go

The program will most likely print three lines in the following order, with five seconds between each line of output.

$go run publish1. Go Let's hope the news will be published before I leave. BREAKING news: A goroutine starts A new thread of execution. Ten seconds later: I'm leaving now.Copy the code

In general, it is not possible for threads to sleep and wait for each other. In the next section, we’ll look at one of Go’s synchronization mechanisms, Channels. We then demonstrate how to use a channel to make one Goruntine wait for another.

2. Channels

Sushi conveyor belt

Sushi conveyor

A channel is a Go construct that provides a mechanism for two Goroutines to synchronously execute and communicate data by passing values for specific element types. The <- identifier indicates the direction of the channel, receive or send. If no direction is specified. So a channel is two-way.

Chan Sushi // can be used to receive and send the value chan<- of type Sushifloat64 // Can only be used for sendingfloatValue of type 64 <-chan int // can only be used to receive int valuesCopy the code

Channels is a reference type allocated by make

Wc := make(chan *Work, 10) wc := make(chan *Work, 10Copy the code

To send values over a channel, you can use <- as a binary operator. A value is received through a channel, which can be used as a unary operator.

IC < -3 // Send 3 work to channel := <-wc // receive pointer from channel to workCopy the code

If the channel is unbuffered, the sender will block until a receiver receives the value from it. If buffered, only if the value is copied to the buffer and the buffer is full will the sender block until there is a receiver to receive it. The receiver blocks until a value can be received in the channel.

Shut down

The function of close is to ensure that no value can be sent to the channel. A channel can still receive values after it is closed. The receive operation returns zero without blocking. The multivalue receive operation returns an additional Boolean value indicating whether the value was sent or not.

ch := make(chan string)
go func() {
    ch <- "Hello!"Close (ch)}() fmt.println (<-ch) // Print"Hello!"Println(<-ch) // Print null values without blocking""Println(<-ch) // Print again""V, ok := <-ch // v is"", the value of OK isfalseCopy the code

The for statement accompanied by the range clause reads continuously the values sent through the channel until the channel is closed

func main() {
    var ch <-chan Sushi = Producer()
    for s := range ch {
        fmt.Println("Consumed", s)
    }
}

func Producer() <-chan Sushi {
    ch := make(chan Sushi)
    go func() {
        ch <- Sushi("Haoluwani")  // Ebi nigiri
        ch <- Sushi("Tuna and childhood grip") // Toro nigiri
        close(ch)
    }()
    return ch
}Copy the code

sushi.go

3. The synchronous

In the next example, the Publish function returns a channel that broadcasts the text sent as a message.

Func Publish(text string, delay time.duration) (// Publish will print text to the standard output after the time has expired.wait <-chan struct{}) {
    ch := make(chan struct{})
    go func() {
        time.Sleep(delay)
        fmt.Println("BREAKING NEWS:", text)
        close(ch) // broadcast – a closed channel sends a zero value forever
    }()
    return ch
}Copy the code

publish2.go

Note that we use an empty structure for channel: struct{}. This indicates that the channel is used only for signals, not for transmitting data.

You might use this function like this

func main() {
    wait := Publish("Channels let goroutines communicate.", 5*time.Second)
    fmt.Println("Waiting for the news...")
    <-wait
    fmt.Println("The news is out, time to leave.")}Copy the code

publish2.go

The program will print the following three lines of information in the given order. The last line appears immediately after the message is sent

$ go run publish2.go
Waiting for the news...
BREAKING NEWS: Channels let goroutines communicate.
The news is out, time to leave.Copy the code

4. A deadlock

traffic jam

Let’s introduce a bug in the Publish function.

func Publish(text string, delay time.Duration) (wait <-chan struct{}) {
    ch := make(chan struct{})
    go func() {
        time.Sleep(delay)
        fmt.Println("BREAKING NEWS:", text)
        **//close(ch)**
    }()
    return ch
}Copy the code

The Goroutine enabled by Publish prints important information and exits, leaving the main goroutine to wait.

func main() {
    wait := Publish("Channels let goroutines communicate.", 5*time.Second)
    fmt.Println("Waiting for the news...")
    **<-wait**
    fmt.Println("The news is out, time to leave.")}Copy the code

In some cases, the program will not make any progress, a situation known as deadlock.

Deadlock is a situation in which threads are waiting for each other but no deadlock can continue

At run time, Go has good support for runtime deadlock detection. At some point, however, the Goroutine fails to make any progress, and the Go program provides a detailed error message. Here is our crash log:

Waiting for the news...
BREAKING NEWS: Channels letgoroutines communicate. fatal error: all goroutines are asleep - deadlock! goroutine 1 [chan receive]: main.main() ... /goroutineStop.go:11 +0xf6 goroutine 2 [syscall]: created by runtime.main ... /go/src/pkg/runtime/proc.c:225 goroutine 4 [timer goroutine (idle)]: created by addtimer ... /go/src/pkg/runtime/ztime_linux_amd64.c:73Copy the code

In most cases, it’s easy to figure out what’s causing a deadlock in a Go program. Then there’s how to fix it.

5. Data competition

Deadlocks may sound bad, but what really spells disaster for concurrent programming is data contention. They are fairly common and difficult to debug.

A data race occurs when two threads concurrently access the same variable while at least one of the accesses is being written.

Data competition is irregular. For example, print the number 1 and try to figure out how it happened — one possible explanation is behind the code.

func race() {
    wait := make(chan struct{})
    n := 0
    go func() {**n++** // one operation: read, grow, write close() {**n++** /waitN++)} () * * * * / / another conflict access < - wait FMT. Println (n) / / output: not sure}Copy the code

datarace.go

Two goroutines, G1 and G2, in the course of competition, we don’t know in what order they execute. The following is just one of many possible outcomes.

  • g1nRead the value of a variable0
  • g2nRead the value of a variable0
  • g1Increase its value from0into1
  • g1Let’s take the value of that1Assigned ton
  • g2Increase its value from0to1
  • g2Let’s take the value of that1Assigned ton
  • This program will print the value of n, which has a value of1

The term “data race” is somewhat misleading, not only because its order of execution cannot be set, but there is no guarantee of what will happen next. Compilers and hardware often reorder code for better performance. If you look closely at a running thread, you’ll probably see more detail.

mid action

The only way to avoid data contention is to synchronize all the variable data shared between threads. There are several ways to do this. In Go, the most likely use is channel or lock. Lower-level operations can use the Sync and Sync /atomic packages, which are not discussed here.

In Go, the preferred way to handle concurrent data access is to use a channel that passes data from one Goroutine to another. There’s a classic saying :” Don’t pass data through shared memory; Instead, share memory by passing data.”

func sharingIsCaring() {
    ch := make(chan int)
    go func() {n := 0 // Local variables can only be visible to the current goroutine n++ ch < -n // data is passed through goroutine}() n := <-ch //... Accept n++ fmt.println (n) safely from another goroutine // output: 2}Copy the code

datarace.go

Channel plays a dual role in this code. It acts as a synchronization point, passing data between different Goroutines. Sending goroutines will wait for other Goroutines to receive data, and receiving goroutines will wait for other Goroutines to send data.

Go Memory Model – When one Goroutine reads a variable and another goroutine writes the same variable, the process is actually quite complicated, but as long as you use a channel to share data between different Goroutines, then the operation is safe.

6. The mutex

lock

Sometimes it is more convenient to synchronize data through direct locking than using a channel. To do this, the Go standard library provides a Mutex sync.Mutex.

For this type of lock to work correctly, all operations on shared data (both read and write) must occur while a Goroutine holds the lock. This is crucial. A single error in the Goroutine is enough to break the program and cause data competition.

Therefore, you need to design a custom data structure for the API and ensure that all synchronization is performed internally. In this example, we have built a secure and easy to use concurrent data structure, AtomicInt, which stores a single integer, and any Goroutines can safely access numbers through Add and Value methods.

AtomicInt is a data structure that holds an int and supports concurrency. // It is initialized to 0.typeAtomicInt struct {mu sync.Mutex // Only one goroutine can hold the lock at a time. N int} // Add n to the AtomicInt as a single atomic operation. Func (a *AtomicInt) Add(n Int) {a.m. lock () // Wait for the lock to be released and then acquire. A.n += a.m.u.u.lock () // Release the lock. Func (a *AtomicInt) Value() int {A.A.M. u.lock () n := A.A.A.M. u.lock ()return n
}

func lockItUp() {
    wait := make(chan struct{})
    var n AtomicInt
    go func() {
        n.Add(1) // one access
        close(wait)} () n.a. dd (1) / / another concurrent access to < - wait FMT. Println (n.V alue ()) / / Output: 2}Copy the code

datarace.go

7. Detect data contention

Competition is sometimes hard to detect. When I run this program with a data race, it prints 55555. Try again, you might get a different result. Sync. WaitGroup is part of the GO standard library; It waits for a series of Goroutines to finish executing.

func race() {
    var wg sync.WaitGroup
    wg.Add(5)
    for i := 0; i < 5; **i++** {
        go func() {** FMT.Print(I)** // local variable is shared by 6 goroutines wg.done ()}()} wg.wait ()Copy the code

raceClosure.go

A reasonable explanation for output 55555 is that the goroutine that executes i++ has been executed five times before the other goroutines print. In fact, the updated I is seen to be random to the other Goroutines.

A very simple solution is to start another Goroutine by using local variables as parameters.

func correct() {
    var wg sync.WaitGroup
    wg.Add(5)
    fori := 0; i < 5; I++ {go func(n int) {// local variable. fmt.Print(n) wg.Done() }(i) } wg.Wait() fmt.Println() }Copy the code

raceClosure.go

This code is correct, he prints the desired result, 24031. Recall that in different Goroutines, programs are executed out of order.

We can still use closures to avoid data contention. But we need to be aware that we need to have different variables in each goroutine.

func alsoCorrect() {
    var wg sync.WaitGroup
    wg.Add(5)
    fori := 0; i < 5; I ++ {n := I // Creates a separate variable go for each closurefunc() {
            fmt.Print(n)
            wg.Done()
        }()
    }
    wg.Wait()
    fmt.Println()
}Copy the code

raceClosure.go

7. Automatic race detection

In general, it is impossible to automatically discover all data competitions. But Go(since version 1.1) offers a powerful Data Race Detector.

The tool is simple to use: just add -race to the go command. Running the above program will automatically check and print the following output.

$ go run -race raceClosure.go Data race: ================== WARNING: DATA RACE Read at 0x00c420074168 by goroutine 6: main.race.func1() .. /raceClosure.go:22 +0x3f Previous write at 0x00c420074168 by main goroutine: main.race() .. /raceClosure.go:20 +0x1bd main.main() .. /raceClosure.go:10 +0x2f Goroutine 6 (running) created at: main.race() .. /raceClosure.go:24 +0x193 main.main() .. /raceClosure.go:10 +0x2f ================== 12355 Correct: 01234 Also correct: 01234 Found 1 data race(s)exit status 66Copy the code

The tool found a data race on line 20, where one goroutine was writing to a variable, and line 22 where another goroutine was reading the variable asynchronously.

Note that this tool can only find data races that occur during actual execution.

8. A Select statement

In Go concurrent programming, the last one is the SELECT statement. It picks out a list of communication operations that can be performed. If any communication operation can be performed, one is randomly selected and the associated statement is executed. Otherwise, if there is no default execution statement, it blocks until any of the communication operations can be executed.

Here’s an example of how to use SELECT to randomly generate numbers.

Func RandomBits() <-chan int {ch := make(chan int) gofunc() {
        for {
            select {
            caseCh < -0: // There is no related operation statementcase ch <- 1:
            }
        }
    }()
    return ch
}Copy the code

randBits.go

Simpler, where select is used to set the timeout. This code can only print news or time-out messages, depending on which of the two receive statements can execute.

select {
case news := <-NewsAgency:
    fmt.Println(news)
case <-time.After(time.Minute):
    fmt.Println("Time out: no news in one minute.")}Copy the code

Time.After is part of the GO standard library; He waits for a certain time to pass, and then sends the current time to the returning channel.

9. The most basic concurrent instance

couples

Take some time to understand this example carefully. When you fully understand it, you’ll have a thorough understanding of the concurrency mechanisms inside Go.

The program demonstrates that a single channel simultaneously sends and receives data from multiple Goroutines. It also shows how a SELECT statement can be selected for execution from multiple communication operations.

func main() {
    people := []string{"Anna"."Bob"."Cody"."Dave"."Eva"} match := make(chan string, 1) wargaming := new(sync.waitgroup)for _, name := range people {
        wg.Add(1)
        go Seek(name, match, wg)
    }
    wg.Wait()
    select {
    case name := <-match:
        fmt.Printf("No one received %s message.\n", name) default: Func Seek(name string, match chan string, match chan string, wg *sync.WaitGroup) { select {case peer := <-match:
        fmt.Printf("%s received a message from %s.\n", name, peer)
    caseMatch <- name: // wait for others to receive the message} wg.done ()}Copy the code

matching.go

Example output:

$ go run matching.go
Anna received a message from Eva.
Cody received a message from Bob.
No one received Dave’s message.Copy the code

10. Parallel computing

CPUs

Concurrent applications divide a large computation into smaller cells, each of which works separately.

Distributed computing on multiple cpus is not just a science, it’s an art.

  • The execution time for each cell is approximately 100us to 1ms. If these units are too small, allocation problems and the overhead of managing submodules may increase. If these units are too large, the entire computing system may be blocked by a small time-consuming operation. Many factors can affect computing speed, such as scheduling, program terminals, and memory layout (note that the number of units of work is independent of the number of cpus).

  • Minimize the amount of data shared. Concurrent writes can be very performance consuming, especially when multiple Goroutines are executed on different cpus. The performance impact of shared data read operations is not significant.

  • The rational organization of data is an efficient way. Loading and storing data will be much faster if the data is kept in the cache. Again, this is very important for write operations.

The following example shows how to allocate multiple time calculations to multiple available cpus. This is the code we want to optimize.

type Vector []float64

// Convolve computes w = u * v, whereW [k] = σ U [I]*v[J], I + j = k. // Feed feed: len(u) > 0, len(v) > 0. func Convolve(u, v Vector) Vector { n := len(u) + len(v) - 1 w := make(Vector, n)for k := 0; k < n; k++ {
        w[k] = mul(u, v, k)
    }
    returnW} σ u[I]*v[j], I + j = k.func mul(u, v Vector, k int)float64 {
    var res float64
    n := min(k+1, len(u))
    j := min(k, len(v)-1)
    for i := k - j; i < n; i, j = i+1, j-1 {
        res += u[i] * v[j]
    }
    return res
}Copy the code

The idea is simple: identify units of work that are the right size, and then run each unit of work in a separate Goroutine. This is the concurrent version of Convolve.

func Convolve(u, v Vector) Vector { n := len(u) + len(v) - 1 w := make(Vector, Size := Max (1, 1000000/n) var wargsync.waitgroupfor i, j := 0, size; i < n; i, j = j, j+size {
        ifWg.add (1) go func(I, j int) {for k := i; k < j; k++ {
                w[k] = mul(u, v, k)
            }
            wg.Done()
        }(i, j)
    }
    wg.Wait()
    return w
}Copy the code

convolution.go

When cells are defined, it is usually best to leave scheduling to program execution and the operating system. However, in the Go1.* version, you need to specify the number of Goroutines.

func init() {numCPU := runtime.numcpu () runtime.gomaxprocs (numcpu) // Try to use all available cpus}Copy the code

Stefan Nilsson


Diggings translation project is a community for translating quality Internet technical articles from diggings English sharing articles. The content covers Android, iOS, React, front end, back end, product, design and other fields. If you want to see more high-quality translations, please continue to pay attention to The Jingjin Translation Project, official weibo, zhihu column.