[golang] goroutine 和 channel

579 阅读4分钟

在 Go 中,应用程序并发处理的部分被称作 goroutines(协程),它可以进行更有效的并发运算。在协程和操作系统线程之间并无一对一的关系:协程是根据一个或多个线程的可用性,映射(多路复用,执行于)在他们之上的;协程调度器在 Go 运行时很好的完成了这个工作。

协程是轻量的,比线程更轻。它们痕迹非常不明显(使用少量的内存和资源):使用 4K 的栈内存就可以在堆中创建它们。因为创建非常廉价,必要的时候可以轻松创建并运行大量的协程(在同一个地址空间中 100,000 个连续的协程)。并且它们对栈进行了分割,从而动态的增加(或缩减)内存的使用;栈的管理是自动的,但不是由垃圾回收器管理的,而是在协程退出后自动释放。

任何 Go 程序都必须有的 main() 函数也可以看做是一个协程,尽管它并没有通过 go 来启动。协程可以在程序初始化的过程中运行(在 init() 函数中)。

在一个协程中,比如它需要进行非常密集的运算,可以在运算循环中周期的使用 runtime.Gosched():这会让出处理器,允许运行其他协程;它并不会使当前协程挂起,所以它会自动恢复执行。使用 Gosched() 可以使计算均匀分布,使通信不至于迟迟得不到响应。

使用 GOMAXPROCS

所有的协程都会共享同一个线程除非将 GOMAXPROCS 设置为一个大于 1 的数。当 GOMAXPROCS 大于 1 时,会有一个线程池管理许多的线程。GOMAXPROCS 等同于(并发的)线程数量,在一台核心数多于 1 个的机器上,会尽可能有等同于核心数的线程在并行运行。

goroutine

Goroutine 是 Go 中最基本的执行单元,每一个 Go 程序至少有一个 goroutine(main goroutine,当程序启动时,它会自动创建)。
image.png

var wg sync.WaitGroup
sayHello := func() {
    defer wg.Done()
    fmt.Println("hello")
}

wg.Add(1)
// fork
go sayHello()
// join
wg.Wait()

但在 Go 1.5 以前调度器仅使用单线程,也就是说只实现了并发。想要发挥多核处理器的并行,需要在的程序中显式调用 runtime.GOMAXPROCS(n) 告诉调度器同时使用多个线程。GOMAXPROCS 设置了同时运行逻辑代码的系统线程的最大数量,并返回之前的设置。如果 n < 1,不会改变当前设置。
在 Go 1.5 及以后,将标识并发系统线程个数的 runtime.GOMAXPROCS 的初始值由 1 改为了运行环境的 CPU 核数。

runtime goroutine

  • Goexit:退出当前执行的 goroutine,但是 defer 函数还会继续调用;
  • Gosched:让出当前 goroutine 的执行权限,调度器安排其他等待的任务运行,并在下次某个时候从该位置恢复执行;
  • NumCPU:返回 CPU 核数量;
  • NumGoroutine:返回正在执行和排队的任务总数;
  • GOMAXPROCS:用来设置可以并行计算的 CPU 核数的最大值,并返回之前的值。

channel

读写 channel

goroutine 是 Go 语言的基本调度单位,而 channel 则是它们之间的通信机制。操作符 <- 用来指定管道的方向(写或读)。如果未指定方向,则为双向管道。默认情况下,信道的写消息和读消息都是阻塞的。

// 创建一个双向 channel
ch := make(chan interface{})
// 写,往 nil channel 中发送数据会一致被阻塞着
ch <- x
// 读,从 nil channel 中接收数据会一致被阻塞着
x = <-ch
// 忽略接收到的值(合法)
<-ch
if <- ch != 1000{
    ...
}
writeStream := make(chan<- interface{})
readStream := make(<-chan interface{})

// invalid operation: cannot receive from send-only channel writeStream (variable of type chan<- interface{})
<-writeStream

// invalid operation: cannot send to receive-only type <-chan interface{}
readStream <- struct{}{}

关闭 channel

channel 支持 close 操作:

  • 对已关闭的 channel 进行任何写操作都将导致 panic
  • 可以对已关闭的 channel 进行读操作;
  • 如果 channel 中已经没有数据,将读取一个零值;
intStream := make(chan int)
close(intStream)
integer, ok := <-intStream
// (false): 0
fmt.Printf("(%v): %v", ok, integer)
intStream := make(chan int)
go func() {
    time.Sleep(5 * time.Second)
    defer close(intStream)
    for i := 1; i <= 5; i++ {
        intStream <- i
    }
}()

// 如果 intStream 不关闭,那么 range 函数就不会结束,从而接收到第 6 个值的时候就阻塞了。
// fatal error: all goroutines are asleep - deadlock!
for integer := range intStream {
    // 1 2 3 4 5
    fmt.Printf("%v ", integer)
}

带缓冲的 channel

带缓冲区的通道允许发送端的数据发送和接收端的数据获取处于异步状态,就是说发送端发送的数据可以放在缓冲区里面,可以等待接收端去获取数据,而不是立刻需要接收端去获取数据。
不过由于缓冲区的大小是有限的,所以还是必须有接收端来接收数据的,否则缓冲区一满,数据发送端就无法再发送数据了。
注意:如果通道不带缓冲,发送方会阻塞直到接收方从通道中接收了值。如果通道带缓冲,发送方则会阻塞直到发送的值被拷贝到缓冲区内;如果缓冲区已满,则意味着需要等待直到某个接收方获取到一个值。接收方在有值可以接收之前会一直阻塞。

ch := make(chan string, 3)
// 可以在无阻塞的情况下,向新创建的 channel 发送 3 个值
ch <- "A"
ch <- "B"
ch <- "C"

// fatal error: all goroutines are asleep - deadlock!
// ch <- "D"

// A
// 此刻,读取 1 个值,那么 channel 的缓冲空间将有剩余。
// 因此对该 channel 执行的写或读操作都不会发生阻塞。
// 通过这种方式,channel 的缓冲队列解耦了读和写的 goroutine。
fmt.Println(<-ch)

无缓冲是同步的,一旦有写入,则必须有读取,某则阻塞。

func sum(s []int, c chan int) {
    sum := 0
    for _, v := range s {
        sum += v
    }
    fmt.Printf("sum:")
    fmt.Printf("%#v\n", sum)
    c <- sum // 把 sum 发送到通道 c
    fmt.Println("after channel pro")
}

func main() {
    // 通道不带缓冲,表示是同步的,只能向通道 c 发送一个数据,只要这个数据没被接收,那么所有的发送就被阻塞
    c := make(chan int)

    go sum([]int{0, 0}, c) // a
    go sum([]int{1, 1}, c) // b
    go sum([]int{2, 2}, c) // c
    go sum([]int{3, 3}, c) // d

    /*
       a b c d 和 main 一起争夺 cpu 的,他们的执行顺序完全无序,甚至里面不同的语句都相互穿插。
       但无缓冲的等待是同步的,所以接下来 a b c d 都会执行,一直执行到 c <- sum 后,开始同步阻塞。
       因此 after channel pro 是打印不出来的,等要打印 after channel pro 的时候,main 就结束了。
    */

    fmt.Println("go start waiting...")
    time.Sleep(1000 * time.Millisecond)
    fmt.Println("go waited 1000 ms")

    // a b c d 都在管道门口等着
    aa := <-c
    bb := <-c
    fmt.Println(aa)
    fmt.Println(bb)
    x, y := <-c, <-c
    fmt.Println(x, y, x+y)
}

若修改成 make(chan int, 2),则缓冲区内是非阻塞的,可以正常打印 after channel pro
带缓冲的 channel 可被用作信号量,列如限制吞吐量。在此例中,进入的请求会被传递给 handle,它从 channel 中接收值,处理请求后将值发回该信道中,以便让该“信号量”准备迎接下一次请求。信道缓冲区的容量决定了同时调用 process 的数量上限。

var sem = make(chan int, MaxOutstanding)

func handle(r *Request) {
    sem <- 1   // 等待活动队列清空
    process(r) // 可能需要很长时间
    <-sem      // 完成,使下一个请求可以运行
}

func Serve(queue chan *Request) {
    for {
        req := <-queue
        go handle(req) // 无需等待 handle 结束
    }
}

然而,它却有个设计问题:尽管只有 MaxOutstanding 个 goroutine 能同时运行,但 Serve 还是为每个进入的请求都创建了新的 goroutine。其结果就是,若请求来得很快,该程序就会无限地消耗资源。为了弥补这种不足,可以通过修改 Serve 来限制创建 goroutine。

func Serve(queue chan *Request) {
    for req := range queue {
        sem <- 1
        go func() {
            process(req) // 这有 Bug
            <-sem
        }()
    }
}

Bug 出现在 Go 的 for 循环中,该循环变量在每次迭代时会被重用,因此 req 变量会在所有的 goroutine 间共享。因此需要确保 req 对于每个 goroutine 来说都是唯一的。

func Serve(queue chan *Request) {
    // for-range 语句来读取通道是更好的办法,因为这会自动检测通道是否关闭
    for req := range queue {
        sem <- 1
        go func(req *Request) {
            process(req)
            <-sem
        }(req)
    }
}

另一种解决方案就是创建新的变量。

reqTemp := req

下面这个案例并发地向三个镜像站点发出请求,三个镜像站点分散在不同的地理位置。它们分别将收到的响应发送到带缓冲 channel,最后接收者只接收第一个收到的响应,也就是最快的那个响应。因此 mirroredQuery 函数可能在另外两个响应慢的镜像站点响应之前就返回了结果。

func mirroredQuery() string {
    responses := make(chan string, 3)
    go func() { responses <- request("asia.gopl.io") }()
    go func() { responses <- request("europe.gopl.io") }()
    go func() { responses <- request("americas.gopl.io") }()
    // 仅仅返回最快的那个 response
    return <-responses
}

func request(hostname string) (response string) { /* ... */ }

如果使用了无缓冲的 channel,那么两个慢的 goroutine 将会因为没有被接收而被永远卡住。这种情况,称为 goroutine 泄漏。和垃圾变量不同,泄漏的 goroutine 并不会被自动回收,因此确保每个不再需要的 goroutine 能正常退出是非常重要的。

channel of channel

channel 可以被分配,并到处传递,这种特性通常被用来实现安全、并行的多路分解。
可以利用这个特性来实现一个简单的 RPC(Remote Procedure Call),以下为 Request 类型的大概定义。

type Request struct {
    args       []int
    f          func([]int) int
    resultChan chan int
}

客户端提供了一个函数及其实参,此外在请求对象中还有个接收应答的 channel。

func sum(a []int) (s int) {
    for _, v := range a {
        s += v
    }
    return
}

request := &Request{[]int{3, 4, 5}, sum, make(chan int)}
// 发送请求
clientRequests <- request
// 等待回应
fmt.Printf("answer: %d\n", <-request.resultChan)

服务端的 handler 函数。

func handle(queue chan *Request) {
    for req := range queue {
        req.resultChan <- req.f(req.args)
    }
}

channel pipeline

channel 也可以用于将多个 goroutine 连接在一起,一个 channel 的输出作为下一个 channel 的输入。这种串联的 channel 就是所谓的 channel pipeline。

  1. 第一个 goroutine 用于生成 0, 1, 2...,并通过第一个 channel 将其发送给第二个 goroutine;
  2. 第二个 goroutine 对收到的每个整数求平方,并通过第二个 channel 将其发送给第三个 goroutine;
  3. 第三个 goroutine 打印收到的每个整数;
func counter(out chan<- int) {
    for x := 0; x < 100; x++ {
        out <- x
    }
    close(out)
}

func squarer(out chan<- int, in <-chan int) {
    for v := range in {
        out <- v * v
    }
    close(out)
}

func printer(in <-chan int) {
    for v := range in {
        fmt.Println(v)
    }
}

func main() {
    naturals := make(chan int)
    squares := make(chan int)

    go counter(naturals)
    go squarer(squares, naturals)
    printer(squares)
}

select 多路复用

select 用于从一组可能的 channel 中选择处理(select 默认是阻塞的)。如果任意一个 channel 都可以处理,则从中随机(pseudo-random)选择一个处理。否则,如果又没有默认分支(default case),select 语句则会阻塞,直到某个 case 需要处理。select 的 case 可以是发送语句,也可以是接收语句,否则就是 default 语句(select 不再阻塞等待 channel,最多允许一个,位置不定)。

select {
case <-ch1:
    // ...
case x := <-ch2:
    // ...use x...
case ch3 <- y:
    // ...
default:
    // ...
}
func fibonacci(c, quit chan int) {
    x, y := 0, 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)
}

timeout

如果没有 case 需要处理,select 语句就会一直阻塞。这时候需要一个超时操作,用来处理超时的情况。
time.After 方法返回一个类型为 <-chan Time 的单向 channel,在指定时间后返回一个当前时间。

func main() {
    c1 := make(chan string, 1)

    go func() {
        time.Sleep(time.Second * 2)
        c1 <- "result 1"
    }()

    select {
    case res := <-c1:
        fmt.Println(res)
    case <-time.After(time.Second * 1):
        fmt.Println("timeout 1")
    }
}

下面的 select 语句会在 abort channel 中有值时,从其中接收值,无值时什么都不做。这是一个非阻塞的接收操作,反复地做这样的操作叫做“轮询channel”。

select {
case <-abort:
    fmt.Printf("Launch aborted!\n")
    return
default:
    // do nothing
}

timer 和 ticker

timer 是一个定时器,代表未来的一个单一事件。

// 类同于 time.Sleep
timer2 := time.NewTimer(time.Second)

go func() {
    <-timer2.C
    fmt.Println("Timer 2 expired")
}()

stop2 := timer2.Stop()
if stop2 {
    fmt.Println("Timer 2 stopped")
}

ticker 是一个定时触发的计时器。

ticker := time.NewTicker(500 * time.Millisecond)
done := make(chan bool)

go func() {
    for {
        select {
        case <-done:
            return
        case t := <-ticker.C:
            fmt.Println("Tick at", t)
        }
    }
}()

time.Sleep(1600 * time.Millisecond)
ticker.Stop()
done <- true
fmt.Println("Ticker stopped")

锁和通道

type Task struct {
    // some state
}

type Pool struct {
    Mu    sync.Mutex
    Tasks []*Task
}

func Worker(pool *Pool) {
    for {
        pool.Mu.Lock()
        // begin critical section:
        task := pool.Tasks[0]        // take the first task
        pool.Tasks = pool.Tasks[1:]  // update the pool of tasks
        // end critical section
        pool.Mu.Unlock()
        process(task)
    }
}

通道

func main() {
    pending, done := make(chan *Task), make(chan *Task)
    go sendWork(pending)     // put tasks with work on the channel
    for i := 0; i < N; i++ { // start N goroutines to do work
        go Worker(pending, done)
    }
    consumeWork(done) // continue with the processed tasks
}

func Worker(in, out chan *Task) {
    for {
        t := <-in
        process(t)
        out <- t
    }
}
  • 使用锁的情景:
    • 访问共享数据结构中的缓存信息
    • 保存应用程序上下文和状态信息数据
  • 使用通道的情景:
    • 与异步操作的结果进行交互
    • 分发任务
    • 传递数据所有权

惰性生成器的实现

import (
    "fmt"
)

type Any interface{}
type EvalFunc func(Any) (Any, Any)

func main() {
    evenFunc := func(state Any) (Any, Any) {
        os := state.(int)
        ns := os + 2
        return os, ns
    }

    even := BuildLazyIntEvaluator(evenFunc, 0)

    for i := 0; i < 10; i++ {
        fmt.Printf("%vth even: %v\n", i, even())
    }
}

func BuildLazyEvaluator(evalFunc EvalFunc, initState Any) func() Any {
    retValChan := make(chan Any)
    loopFunc := func() {
        var actState Any = initState
        var retVal Any
        for {
            retVal, actState = evalFunc(actState)
            retValChan <- retVal
        }
    }
    retFunc := func() Any {
        return <-retValChan
    }
    go loopFunc()
    return retFunc
}

func BuildLazyIntEvaluator(evalFunc EvalFunc, initState Any) func() int {
    ef := BuildLazyEvaluator(evalFunc, initState)
    return func() int {
        return ef().(int)
    }
}

实现 Futures 模式

func InverseProduct(a Matrix, b Matrix) {
    a_inv_future := InverseFuture(a) // start as a goroutine
    b_inv_future := InverseFuture(b) // start as a goroutine
    a_inv := <-a_inv_future
    b_inv := <-b_inv_future
    return Product(a_inv, b_inv)
}

func InverseFuture(a Matrix) chan Matrix {
    future := make(chan Matrix)
    go func() {
        future <- Inverse(a)
    }()
    return future
}

典型的客户端/服务器(C/S)模式

import "fmt"

type Request struct {
    a, b   int
    replyc chan int // reply channel inside the Request
}

type binOp func(a, b int) int

func run(op binOp, req *Request) {
    req.replyc <- op(req.a, req.b)
}

func server(op binOp, service chan *Request, quit chan bool) {
    for {
        select {
        case req := <-service:
            go run(op, req)
        case <-quit:
            return
        }
    }
}

func startServer(op binOp) (service chan *Request, quit chan bool) {
    service = make(chan *Request)
    quit = make(chan bool)
    go server(op, service, quit)
    return service, quit
}

func main() {
    adder, quit := startServer(func(a, b int) int { return a + b })
    const N = 100
    var reqs [N]Request
    for i := 0; i < N; i++ {
        req := &reqs[i]
        req.a = i
        req.b = i + N
        req.replyc = make(chan int)
        adder <- req
    }
    // checks:
    for i := N - 1; i >= 0; i-- { // doesn't matter what order
        if <-reqs[i].replyc != N+2*i {
            fmt.Println("fail at", i)
        } else {
            fmt.Println("Request ", i, " is ok!")
        }
    }
    quit <- true
    fmt.Println("done")
}

限制同时处理的请求数

const MAXREQS = 50

var sem = make(chan int, MAXREQS)

type Request struct {
    a, b   int
    replyc chan int
}

func process(r *Request) {
    // do something
}

func handle(r *Request) {
    sem <- 1 // doesn't matter what we put in it
    process(r)
    <-sem // one empty place in the buffer: the next request can start
}

func server(service chan *Request) {
    for {
        request := <-service
        go handle(request)
    }
}

func main() {
    service := make(chan *Request)
    go server(service)
}
import (
    "sync"
    "fmt"
    "testing"
)

const defaultMaxLimit = 8

// GoroutineLimiter goroutine concurrency limiter
type GoroutineLimiter struct {
    maxLimit int
    wg       *sync.WaitGroup
    tickets  chan struct{}
}

// NewGoroutineLimiter new a goroutine limiter
func NewGoroutineLimiter(maxLimit int) *GoroutineLimiter {
    if maxLimit <= 0 {
        maxLimit = defaultMaxLimit
    }
    limiter := &GoroutineLimiter{
        maxLimit: maxLimit,
        wg:       &sync.WaitGroup{},
        tickets:  make(chan struct{}, maxLimit),
    }
    return limiter
}

// Go go
func (l *GoroutineLimiter) Go(fn func()) {
    l.wg.Add(1)
    l.tickets <- struct{}{}

    go func() {
        defer func() {
            <-l.tickets
            defer l.wg.Done()
        }()
        fn()
    }()
}

// Wait wait all fun to finish
func (l *GoroutineLimiter) Wait() {
    l.wg.Wait()
    // actually, didn't need close channel manually
    // close this channel, prevent this limiter to be reused after called Wait(although it can be reused)
    // which means you MUST new an another limiter after when the Wait has been called
    close(l.tickets)
}

func TestConcurrencyLimiter(t *testing.T) {
    l := NewGoroutineLimiter(2)
    for i := 0; i < 8; i++ {
        ii := i
        l.Go(func() {
            fmt.Println("run......", ii)
        })
    }
    l.Wait()
}

链式协程

import (
    "flag"
    "fmt"
)

var ngoroutine = flag.Int("n", 100000, "how many goroutines")

func f(left, right chan int) { left <- 1 + <-right }

func main() {
    flag.Parse()
    leftmost := make(chan int)
    var left, right chan int = nil, leftmost
    for i := 0; i < *ngoroutine; i++ {
            left, right = right, make(chan int)
            go f(left, right)
    }
    right <- 1      // bang!
    x := <-leftmost // wait for completion
    fmt.Println(x)  // 100001, about 1.5 s
}

并行化大量数据的计算

func ParallelProcessData (in <-chan *Data, out chan<- *Data) {
    // make channels:
    preOut := make(chan *Data, 100)
    stepAOut := make(chan *Data, 100)
    stepBOut := make(chan *Data, 100)
    stepCOut := make(chan *Data, 100)
    // start parallel computations:
    go PreprocessData(in, preOut)
    go ProcessStepA(preOut,StepAOut)
    go ProcessStepB(StepAOut,StepBOut)
    go ProcessStepC(StepBOut,StepCOut)
    go PostProcessData(StepCOut,out)
}

使用通道并发访问对象

import (
    "fmt"
    "strconv"
)

type Person struct {
    Name   string
    salary float64
    chF    chan func()
}

func NewPerson(name string, salary float64) *Person {
    p := &Person{name, salary, make(chan func())}
    go p.backend()
    return p
}

func (p *Person) backend() {
    for f := range p.chF {
        f()
    }
}

// Set salary.
func (p *Person) SetSalary(sal float64) {
    p.chF <- func() { p.salary = sal }
}

// Retrieve salary.
func (p *Person) Salary() float64 {
    fChan := make(chan float64)
    p.chF <- func() { fChan <- p.salary }
    return <-fChan
}

func (p *Person) String() string {
    return "Person - name is: " + p.Name + " - salary is: " + strconv.FormatFloat(p.Salary(), 'f', 2, 64)
}

func main() {
    bs := NewPerson("Smith Bill", 2500.5)
    fmt.Println(bs)
    bs.SetSalary(4000.25)
    fmt.Println("Salary changed:")
    fmt.Println(bs)
}

参阅