go语言的并发

Go语言是原生支持并发的,通过goroutine提供了并发执行接口。

并行和并发

  • 两个队列,一个coffee机器,是并发
  • 两个队列,两个coffee机器,是并行

go调度器

go的调度器包含:

  • S:调度器,维护M和G的队列及调度状态信息;
  • M:内核级线程,所有的goroutine都在M上运行;
  • P:processor处理器,执行goroutine;
  • G:一个goroutine协程。

板砖图形象地表示了三者的关系,老鼠gopher(M)推着手推车(P),里面装着砖头(G),一个手推车(P)中会有很多砖头。

上图中个,存在两个内核级线程M,每一个M都拥有一个处理器P,P管理着多个协程G。其中,蓝色的G为正在运行的goroutine,而灰色的G为处于ready状态等待被执行的goroutine,灰色goroutine组成的队列被称为runqueue,每当启动一个goroutine后,runqueue就会在队列的末尾加入该goroutine。
当时,当一个内核线程M被阻塞时,其处理器P如何处理其他G呢?

如图所示,左边为初始状态,线程M1上没有任何处理器,线程M0上存在处理器P,其中正在处理G0,并且拥有一个包含多个G的runqueue。当M0被阻塞时,其他所有任务都无法继续执行,那么处理器P会转而在M1上继续运行,这样并不会因为线程M0上的阻塞而影响到其他任务。

另外一种情况,如果两个线程的的协程数量不同呢?
如下图,左边为初始状态,共两个内核线程,但是由于分配不均匀,其中一个线程需要处理的runqueue为空,如果全局runqueue中没有其他goroutine,它就会从其他的处理器上取一些goroutine来执行,着确保了每个内核线程M的充分使用

goroutine

goroutine是从语言层面上支持并发的重要特性,是运行在操作系统中轻量级的coroutine。goroutine很像线程,但它并不是线程,比线程更轻量,仅比分配栈空间多一点点,而且初始栈空间很小。
通过在函数或方法前添加关键字go来创建一个并发执行单元,

1
go fuction(x, y, z)

  • go关键字是支持匿名函数;
  • 如果进程在其所有goroutine执行完毕前结束,所有goroutine将会被销毁;
  • 调用runtime.Goexit()可以结束该goroutine;
  • 调用runtime.Gosched()可以暂停该goroutine。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    package main
    import (
    "fmt"
    "time"
    )
    func say(s string) {
    for i := 0; i < 5; i++ {
    time.Sleep(100 * time.Millisecond)
    fmt.Println(s)
    }
    }
    func main() {
    go say("world")
    say("hello")
    }

需要注意
默认条件下,go所有的goroutines只能在一个线程里运行,也就是说制实现了并发。
为了发挥多核的并行处理,显示地调用runtime.GOMAXPROCS(n),告诉调度器同时使用多个线程。

##channel
channel是可以用来同步并发执行的goroutine
channel类似于Unix的管道,通过发送和接收来进行通信。

基本操作如下,

1
2
3
4
5
6
7
// 初始化整形、字符串和任意类型的channel
ci := make(chan int)
cs := make(chan string)
cf := make(chan interface{})
//发送和接收
cf <- v
v := <- cf

channel接收和发送数据都是阻塞的,除非另一端准备好接收或发送。

带缓存的channel类型

1
2
3
4
// value为缓存长度
// value == 0 无缓冲(阻塞)
// value != 0 缓冲(非阻塞,除非缓冲了value个元素)
ch := make(chan int , value)

##channel的Select操作
go语言提供了一个关键字select,可以用来监听channel上的数据。
select默认是阻塞的,只有监听的channel中有发送或接收进行时才会运行,如果有多个channel同时准备好,则随机选择一个执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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来设置超时时间,
通过设置 <- time.After(n * time.Second),会延迟n秒后执行该case下的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main
import (
    "fmt"
    "time"
)
func main() {
    c := make(chan int)
    q := make(chan int)
    go func() {
        select {
        case v := <- c:
            fmt.Println(v)
        case <- time.After(2 * time.Second):
            fmt.Println("timeout")
            q <- 1 
            break
        }
    }()
    <- q  // q默认为空,阻塞直到q中缓冲数据
}

参考

http://morsmachine.dk/go-schedulerhttp://eleme.io/blog/2014/goroutine-2/http://skoo.me/go/2013/11/29/golang-schedule/http://xhrwang.me/2014/12/31/golang-fundamentals-10-goroutine-channel.html
https://github.com/astaxie/build-web-application-with-golang/blob/master/zh/02.7.md