golang在语言层面上就提供了对并发的支持,通过goroutine和实现goroutine间通信的channel。
进程,线程,协程与并行,并发 这篇文章对几个概念讲述得很好。
goroutine本质上就是协程,但它又不太一样:
1.go的运行时自带一个goroutine调度器来调度已创建的goroutine协程。而一般意义上的协程,对共同依赖的线程的控制权是在各自的代码中由开发者自己定义的,比如python和ES6,可以通过yield关键字,在执行某个异步任务后,把控制权交回给执行栈。等异步的结果返回,又把控制权交给异步函数处理返回,任务是在单个线程上调度的。
2.协程同一时刻只能有一个在跑,但是goroutine可以实现并行,就是说多个goroutine可以同时在多个进程跑。
3.goroutine之间的通信通过channel实现。
由于goroutine运行在同一个地址空间,访问共享内存时要遵循同步原则。
一个官方示例:
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关键字用于创建一个新的goroutine
//say("hello")在当前主goroutine中执行
//say("world")在另一个goroutine中执行
go say("world")
say("hello")
}
这个例子中time.Sleep用于挂起当前goroutine,time.Sleep是交出控制权的一种方式。。
如果把say("hello")注释掉,程序不会打印字符串,因为线程一直在当前主goroutine中,执行完main函数后就退出了,其他协程还没来得及执行。golang之并发编程这篇文章对此做了讨论。
如果把say("hello")换成time.Sleep(300 * time.Millisecond),程序会打印三次"world",因为主goroutine歇了300ms,这段时间交给了执行say("world")的goroutine。
channel
go提供了channel这个概念,顾名思义,作为信道,来处理goroutine之间的通信。在实现上,就是声明一个chan type,来存放要接收或发送的数据。
ch := make(chan int)
//箭头方向表示数据传输方向
ch <- v // Send v to channel ch.
v := <-ch // Receive from ch, and
// assign value to v.
引用A Tour of Go中示例:
import "fmt"
func sum(s []int, c chan int) {
sum := 0
for _, v := range s {
sum += v
}
//fmt.Println(sum)
c <- sum // send sum to 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 // receive from c
fmt.Println(x, y, x+y)
}
在这个最基本的例子中,我们可看到:
1.channel实例c被传给sum。sum内部通过c<-value文法将数据发送给c。
2.在主goroutine中,通过value := <-c 文法,接收其他协程送来的数据。
多次执行代码,会输出 '17 -5 12' 或 '-5 17 12'。可见goroutine在执行时,顺序方面是相当随机的。
接下来我们可以做一些试验来验证一些东西:
试验一:将主函数中获取channel数据的那行移个位置:
go sum(s[:len(s)/2], c)
x, y := <-c, <-c // receive from c
go sum(s[len(s)/2:], c)
改成这样之后,再运行代码,报错:
fatal error: all goroutines are asleep - deadlock!
这是因为执行到x, y := <-c, <-c这行代码时,程序要从channel c中读取两个数据x,y。可是执行完第一个go sum()之后channel c中只写入了一个数据x,而此时并没有其他goroutine在活动。也就是说,channel c中 第二个位置的数据永远不会到来,程序不可能一直这样等下去,go直接弹出错误,视之为死锁了。
不过如果我们在sum函数中添加一行,给channel c写入一个数值:
func sum(s []int, c chan int) {
...
c <- sum // send sum to c
c <- 1
}
再运行,程序正常执行,并打印出:17 1 18
因为channel c 中第一和第二个位置已经有数据,不会“等一个永远不会来的数据”,自然不会死锁。
试验二:在sum函数中添加一行time.Sleep(1000 * time.Millisecond)
func sum(s []int, c chan int) {
...
fmt.Println(sum)
time.Sleep(1000 * time.Millisecond)
fmt.Println(sum)
c <- sum // send sum to c
}
程序不会报错,只是main函数的fmt.Println延时了约1秒才打印预期的结果。也就是说,channel遵循同步的思想,只要程序中还有goroutine为结束,它就会一直等到预期的数据被写入,才执行并赋值给x,y。然后继续执行后边的代码。
buffered channel
上面的例子我们看到,channel是会阻塞的。对于开发者而言,channel就像一个管道、或者传送带,发送者在另一段塞入东西,接收者在这一端拿出来。如果接收者这边需要一次性拿到3个数据,只能等发送者那边塞完三个数据,再一次性拿出来。
我做了一翻试验,琢磨了好阵子,发现通过make(chan int)声明channel时,默认地,当发送者塞入一个数据后,goroutine会自动交出执行权,等其他goroutine拿出这个数据。
比如发送者goroutine是A,接收者goroutine是B。
在"A一次只塞入一个数据,B一次只拿出一个数据"的设定下,A和B轮番切换执行。
在"A一次只塞入一个数据,B一次拿出三个数据"的设定下,A第一次塞入数据后,切换到B,发现B要一次性拿三个数据,就切换到A,直到A塞满3个数据,再回到B拿出数据。
也就是说,goroutine会因为channel而阻塞。如果channel中没有位置放数据了,发送者会交出控制权,直到channel中有位置放数据才继续执行。如果channel中没有数据,接收者会交出执行权,直到channel中有东西可以拿,才继续往下执行。
我们可以在make一个channel的时候,传入第二个参数:channel可以额外缓存多少个数据:
chanMessage := make(chan string,2)
这时候,发送者在可以额外塞入2个字符串,才交出执行权。
遍历channel中的数据:
for i := range ch //i是channel ch中的每个数据
select语句:
在并发编程时,往往要顾及到多个协程的通信,go可以通过select语句管理多个channel的通信:
//主函数调用,给其他协程传送数据
func fibonacci(c, quit chan int) {
x, y := 0, 1
for {
select {
case c <- x: //如果channel c的数据被其他协程取走,继续放下一个数字
x, y = y, x+y
case <-quit: //如果其他协程返回一个quit值,打印"quit"
fmt.Println("quit")
return
default: //所有case都不满足,执行default
...
}
}
}
func main() {
//声明两个channel 实例:c用来传送数据,quit用来传输"结束通信"的标记
c := make(chan int)
quit := make(chan int)
//创建一个匿名函数goroutine,打印主函数传过来的数据
go func() {
for i := 0; i < 10; i++ {
fmt.Println(<-c)
}
//接收完10个数据后,回传一个quit值给主函数
quit <- 0
}()
fibonacci(c, quit)
}
mutex:
由于协程间共享内存资源,对资源的访问应该是互斥的,以避免冲突,go标准库sync包提供了一种数据结构--mutex。
sync.mutex提供了两个方法:Lock()和unlock()
我们可以在一个代码块前后分别执行这两个方法,让这段代码中的变量在执行期间遵循互斥原则:
import (
"fmt"
"sync"
...
)
type SafeCounter struct {
v map[string]int //存放计数值
mux sync.Mutex //mux是个固定的symc.Mutex
}
//该函数被调用时被锁定,一个时间点内只有一个goroutine可以访问c.v
func (c *SafeCounter) Inc(key string) {
c.mux.Lock()
c.v[key]++
c.mux.Unlock()
}
关闭信道
发送者可以执行下边语句关闭信道,表示不会再发送数据。
close(ch) //c是channel实例
接收者可以通过下面语句判断信道是否被关闭:
v, ok := <-ch
ok为false表示信道已被关闭。
>>>>>>>>>
初学这方面时看到一些不错的分享和讨论: