并发

Go中的协程goroutine

goroutine是go语言的核心特性之一,可以很方便的使用go关键字来启动一个新的协程执行方法.

func main() {
    go say("world")
    say("hello")
}

使用Channel来传递数据

信道使用make创建

c := make(chan int)

使用操作符<-来给值,箭头代表数据流的方向

传递数据时默认同步,不用添加显示的锁,多个go程可以用同一个chan,按顺序取用,见如下

package main

import "fmt"

func sum(s []int, c chan int) {
    sum := 0
    for _, v := range s {
        sum += v
    }
    c <- sum // 将和送入 c
}

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

    c := make(chan int)
    go sum(s[:len(s)/2], c)
    go sum(s[len(s)/2:], c)
    x, y := <-c, <-c // 从 c 中接收
    //x := <-c
    fmt.Println(x,y)
}
输出
-5 17

如果将以上x,y := <-c,<-c改为 x := <-c并尝试输出x会得到-5
这是因为上面的chan是没有缓冲限制(buffered)的,所以在第一次的go协程中,会阻塞,直到计算出sum并将值放入信道,之后才会进行第二次go协程进行前半部的和计算.

带缓冲的信道

以下会报错

func main() {
    ch := make(chan int,2)
    ch <- 1
    ch <- 12
    ch <- 11
    fmt.Println(<-ch)
    fmt.Println(<-ch)
    fmt.Println(<-ch)
}

输出报错
fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan send]:
main.main()
    /tmp/sandbox078539248/prog.go:9 +0xa0

ch := make(chan int,3)即可,是否可以这样理解,Channel是一个装数据块的管道,每次go操作可以从中取一份数据块.上面的错误示例,没有用协程处理,没有将前面的数据块取出就又往里面塞数据,而容量又是有限的,所以进入死锁,一直在等待前面的被取出.而在go协程中对通道的每一次处理,可以理解为对通道中数据块的异步取用.

通过rangeclose处理信道

通过for i:= range c依次从信道中取值
通过close来关闭一个信道

func fibonacci(n int, c chan int) {
    x, y := 0, 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)
    }
}

输出
0
1
1
2
3
5
8
13
21
34

神奇的select语句

select的用法类似switch,本质是对channel的监听,官方tour上的说法是
select 语句使一个 Go 程可以等待多个通信操作,select 会阻塞到某个分支可以继续执行为止,这时就会执行该分支。当多个分支都准备好时会随机选择一个执行

先来看一个select的常规操作

ch1 := make (chan int, 1)
ch2 := make (chan int, 1)

...

select {
case <-ch1:
    fmt.Println("ch1 pop one element")
case <-ch2:
    fmt.Println("ch2 pop one element")
}

其中seleect监听了ch1和ch2两个channel,如果有数据流,就会执行相应的case,select会一直阻塞到直到响应其中一条case

通过这个特性,可以用select来实现超时机制,见下

timeout := make (chan bool, 1)
go func() {
    time.Sleep(1e9) // sleep one second
    timeout <- true
}()
ch := make (chan int)
select {
case <- ch:
case <- timeout:
    fmt.Println("timeout!")
}

如果timeout先触发,则意味着ch超时仍未执行

另外,select中也有default,当case无法实现时,会执行default语句,常用于管道已满,仍执行push操作时,见下

ch := make (chan int, 1)
ch <- 1
select {
case ch <- 2:
default:
    fmt.Println("channel is full !")
}

最后,个人理解,selectgo协程对channel是一种双向监听,和<-c还是c<-有关
参照官方tour上的示例.

package main

import "fmt"

func fibonacci(c, quit chan int) {
    x, y := 0, 1
    for {
        select {
        case c <- x:
            x, y = y, x+y
            fmt.Println("ss:", x)
        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("aa",<-c)
        }
        quit <- 1
    }()
    fibonacci(c, quit)
}
输出
aa 0
ss: 1
ss: 1
aa 1
aa 1
ss: 2
ss: 3
aa 2
aa 3
ss: 5
ss: 8
aa 5
aa 8
ss: 13
ss: 21
aa 13
aa 21
ss: 34
ss: 55
aa 34
quit

在示例中,飞出一个go func,输出<-c,如果不调用fibonacci方法,go协程中的10次遍历永远不会输出,只有在fibonacci中通过select做了c<-x操作,才触发了协程中的fmt.Println("aa",<-c)操作.是否可以这样理解,在go协程中,对chan做 <-chan 操作,如果chan为空,会一直挂起等待chan 被执行chan<-操作.试着验证下

func main() {
    c := make(chan int)
    quit := make(chan int)


    go func() {
        for i := 0; i < 10; i++ {
            fmt.Println("aa",<-c)
        }
        quit <- 1
        fmt.Println("finish")
    }()
    c<-3
    //fibonacci(c, quit)
    fmt.Println("finish2")
}

aa 3
finish2

果然,执行了一次c<-3操作后,go func中输出了一次aa 3,同时注意到finish仍未输出,说明go func仍然在等待剩下的9次 c<- 操作

试着改下

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

输出
j 0
aa 0
j 1
j 2
aa 1
aa 2
j 3
j 4
aa 3
aa 4
j 5
j 6
aa 5
aa 6
j 7
j 8
aa 7
aa 8
j 9
finish2

发现j==9后,给完C,直接就往下走了,完全没给go func最后的机会,试着再改下

func main() {
    c := make(chan int)
    go func() {
        for i := 0; i < 10; i++ {
            fmt.Println("aa",<-c)
        }
        fmt.Println("finish")
    }()
    
    //fibonacci(c, quit)
    for j :=0; j<10; j++ {
        fmt.Println("j",j)
        c<-j
        time.Sleep(1)
    }
    fmt.Println("finish2")
}

输出
j 0
aa 0
j 1
aa 1
j 2
aa 2
j 3
aa 3
j 4
aa 4
j 5
aa 5
j 6
aa 6
j 7
aa 7
j 8
aa 8
j 9
aa 9
finish
finish2

这下圆满了..同时发现go协程的特点,在for循环的每次遍历前会出让资源执行协程,通过time.Sleep()也可以主动让出资源.现在再看官方示例,应该也是在case 赋值后,出让了资源到协程中,执行了输出操作,才会出现先输出 aa 0再输出ss: 1的现象

补充一个官方select default的例子

package main

import (
    "fmt"
    "time"
)

func main() {
    tick := time.Tick(100 * time.Millisecond)
    boom := time.After(500 * time.Millisecond)
    for {
        select {
        case <-tick:
            fmt.Println("tick.")
        case <-boom:
            fmt.Println("BOOM!")
            return
        default:
            fmt.Println("    .")
            time.Sleep(50 * time.Millisecond)
        }
    }
}
输出
    .
    .
tick.
    .
    .
tick.
    .
    .
tick.
    .
    .
tick.
    .
    .
tick.
BOOM!
  • tick := time.Tick(100 * time.Millisecond)代表,每100毫秒,生成一个时间块放到管道tick中.
  • boom := time.After(500 * time.Millisecond)代表,500毫秒后生成一个时间块放到管道boom中.

使用互斥锁防止多个go协程访问修改同一个chan值

Go 标准库中提供了 sync.Mutex 互斥锁类型及其两个方法:

Lock
Unlock

我们可以通过在代码前调用 Lock 方法,在代码后调用 Unlock 方法来保证一段代码的互斥执行。参见 Inc 方法。

我们也可以用 defer 语句来保证互斥锁一定会被解锁。参见 Value 方法。defer的意义,大概在于return之后不方便执行unlock操作.相当于延迟操作

package main

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

// SafeCounter 的并发使用是安全的。
type SafeCounter struct {
    v   map[string]int
    mux sync.Mutex
}

// Inc 增加给定 key 的计数器的值。
func (c *SafeCounter) Inc(key string) {
    c.mux.Lock()
    // Lock 之后同一时刻只有一个 goroutine 能访问 c.v
    c.v[key]++
    c.mux.Unlock()
}

// Value 返回给定 key 的计数器的当前值。
func (c *SafeCounter) Value(key string) int {
    c.mux.Lock()
    // Lock 之后同一时刻只有一个 goroutine 能访问 c.v
    defer c.mux.Unlock()
    return c.v[key]
}

func main() {
    c := SafeCounter{v: make(map[string]int)}
    for i := 0; i < 1000; i++ {
        go c.Inc("somekey")
    }

    time.Sleep(time.Second)
    fmt.Println(c.Value("somekey"))
}

输出
1000

1000个协程对c做自加操作.并未产生异常,注意,如果不做time.Sleep(time.Second)释放资源,1000个go方法是没有机会执行的.

参考

golang的select典型用法

A Tour of Go

标签: go, golang, c, 并发

添加新评论