Skip to content

Latest commit

 

History

History
242 lines (171 loc) · 7.66 KB

File metadata and controls

242 lines (171 loc) · 7.66 KB

Concurrency

It is said that Go is the C language of 21st century. I think there are two reasons: first, Go is a simple language; second, concurrency is a hot topic in today's world, and Go supports this feature at the language level.

goroutine

goroutines and concurrency are built into the core design of Go. They're similar to threads but work differently. More than a dozen goroutines maybe only have 5 or 6 underlying threads. Go also gives you full support to share memory in your goroutines. One goroutine usually uses 4~5 KB stack memory. Therefore, it's not hard to run thousands of goroutines in one computer. A goroutine is more lightweight, more efficient, and more convenient than system threads.

goroutines run on the thread manager at runtime in Go. We use keyword go to create a new goroutine, which is a function at the underlying level ( main() is a goroutine ).

go hello(a, b, c)

Let's see an example.

package main

import (
	"fmt"
	"runtime"
)

func say(s string) {
	for i := 0; i < 5; i++ {
    	runtime.Gosched()
    	fmt.Println(s)
	}
}

func main() {
	go say("world") // create a new goroutine
	say("hello") // current goroutine
}

Output:

hello
world
hello
world
hello
world
hello
world
hello

We see that it's very easy to use concurrency in Go by using the keyword go. In the above example, these two goroutines share some memory, but we would better off following the design recipe: Don't use shared data to communicate, use communication to share data.

runtime.Gosched() means let the CPU execute other goroutines, and come back at some point.

The scheduler only uses one thread to run all goroutines, which means it only implements concurrency. If you want to use more CPU cores in order to use parallel processing, you have to call runtime.GOMAXPROCS(n) to set the number of cores you want to use. If n<1, it changes nothing. This function may be removed in the future, see more details about parallel processing and concurrency in this article.

channels

goroutines are running in the same memory address space, so you have to maintain synchronization when you want to access shared memory. How do you communicate between different goroutines? Go uses a very good communication mechanism called channel. channel is like a two-way pipeline in Unix shell: use channel to send or receive data. The only data type that can be used in channels is the type channel and keyword chan. Be aware that you have to use make to create a new channel.

ci := make(chan int)
cs := make(chan string)
cf := make(chan interface{})

channel uses the operator <- to send or receive data.

ch <- v    // send v to channel ch.
v := <-ch  // receive data from ch, and assign to v

Let's see more examples.

package main

import "fmt"

func sum(a []int, c chan int) {
	total := 0
	for _, v := range a {
    total += v
	}
	c <- total  // send total to c
}

func main() {
	a := []int{7, 2, 8, -9, 4, 0}

	c := make(chan int)
	go sum(a[:len(a)/2], c)
	go sum(a[len(a)/2:], c)
	x, y := <-c, <-c  // receive from c

	fmt.Println(x, y, x + y)
}

Sending and receiving data in channels blocks by default, so it's much easier to use synchronous goroutines. What I mean by block is that the goroutine will not continue when it receives data from an empty channel (value := <-ch), until other goroutines send data to this channel. On the other hand, the goroutine will not continue when it sends data to channel (ch<-5) until this data is received.

Buffered channels

I introduced non-buffered channels above, and Go also has buffered channels that can store more than one element. For example, ch := make(chan bool, 4), here we create a channel that can store 4 boolean elements. So in this channel, we are able to send 4 elements into it without blocking, but the goroutine will be blocked when you try to send a fifth element and no goroutine receives it.

ch := make(chan type, n)

n == 0 ! non-buffer(block)
n > 0 ! buffer(non-block until n elements in the channel)

You can try the following code on your computer and change some values.

package main

import "fmt"

func main() {
	c := make(chan int, 2)  // change 2 to 1 will have runtime error, but 3 is fine
	c <- 1
	c <- 2
	fmt.Println(<-c)
	fmt.Println(<-c)
}

Range and Close

We can use range to operate on buffer channels as in slice and map.

package main

import (
	"fmt"
)

func fibonacci(n int, c chan int) {
	x, y := 1, 1
	for i := 0; i < n; i++ {
    	c <- x
    	x, y = y, x + y
	}
	close(c)
}

func main() {
	c := make(chan int, 10)
	go fibonacci(cap(c), c)
	for i := range c {
	    fmt.Println(i)
	}
}

for i := range c will not stop reading data from channel until the channel is closed. We use the keyword close to close the channel in above example. It's impossible to send or receive data on a closed channel, you can use v, ok := <-ch to test if a channel is closed. If ok returns false, it means the there is no data in that channel and it was closed.

Remember to always close channel in producers, not in consumers, or it's very easy to get into panic status.

Another thing you need to remember is that channels are unlike files, and you don't have to close them frequently, unless you are sure the channel is completely useless, or you want to exit range loops.

Select

In the above examples, we only use one channel, but how can we deal with more than one channel? Go has a keyword called select to listen to many channels.

select is blocking by default, and it continues to execute only when one of channels has data to send or receive. If several channels are ready to use at the same time, select chooses which to execute randomly.

package main

import "fmt"

func fibonacci(c, quit chan int) {
	x, y := 1, 1
	for {
    	select {
    	case c <- x:
        	x, y = y, x + y
    	case <-quit:
    	fmt.Println("quit")
        	return
    	}
	}
}

func main() {
	c := make(chan int)
	quit := make(chan int)
	go func() {
    	for i := 0; i < 10; i++ {
        	fmt.Println(<-c)
    	}
    	quit <- 0
	}()
	fibonacci(c, quit)
}

select has default as well, just like switch. When all the channels are not ready to use, it executes default (doesn't wait for channel anymore).

select {
case i := <-c:
	// use i
default:
	// executes here when c is blocked
}

Timeout

Sometimes a goroutine is blocked, but how can we avoid this to prevent the whole program blocking? We can set a timeout in select to do this.

func main() {
	c := make(chan int)
	o := make(chan bool)
	go func() {
    	for {
        	select {
            	case v := <- c:
               		println(v)
            	case <- time.After(5 * time.Second):
                	println("timeout")
                	o <- true
                	break
        	}
    	}
	}()
	<- o
}

Runtime goroutine

The package runtime has some functions to deal with goroutines.

  • runtime.Goexit()

    Exits the current goroutine, but defer functions will be executed as usual.

  • runtime.Gosched()

    Lets the scheduler executes other goroutines, and comes back at some point.

  • runtime.NumCPU() int

    Returns number of CPU cores

  • runtime.NumGoroutine() int

    Returns number of goroutines

  • runtime.GOMAXPROCS(n int) int

    Set how many CPU cores that you want to use

Links