协程(coroutine)是Go语言中的轻量级线程实现,由Go运行时(runtime)管理。
在一个函数调用前加上go关键字,这次调用就会在一个新的goroutine中并发执行。当被调用的函数返回时,这个goroutine也自动结束。需要注意的是,如果这个函数有返回值,那么这个返回值会被丢弃。
func longWait() {
fmt.Println("Beginning longWait()")
time.Sleep(5 * 1e9)
fmt.Println("End of longWait()")
}
func shortWait() {
fmt.Println("Beginning shortWait()")
time.Sleep(2 * 1e9)
fmt.Println("End of shortWait()")
}
func main(){
fmt.Println("main begin")
time.Sleep(3 * 1e9)
go shortWait()
go longWait()
fmt.Println("main end")
}
执行上述代码,发现longWait没有执行到,shortWait全部执行完毕,这时候main函数结束就不会在执行longWait。如果我们想要让main()函数等待所有goroutine退出后再返回,但如何知道goroutine都退出了呢?这就引出了多个goroutine之间通信的问题。
chan
channel是引用类型,是CSP格式的个体实现,用于多个goroutine通讯,其内部实现了同步,确保并发安全。
当我们复制一个channel或用于函数参数传递时,我们只是拷贝了一个channel引用,因此,调用者或被调用者将引用一个channel对象,和其它的类型一样,channel的零值也是nil。
chan定义
make(chan type) //等坐于 make(chan type,0)
make(chan type,capacity) //capacity是容量
//当capacity=0时,channel是无缓冲阻塞读写的,当capacity>0时,channel是有缓冲非阻塞的,直到写满capacity元素才阻塞写入。
//channel通过操作符<-来接收和发数据
ch := make(chan int) //创建一个int类型管道
ch <- value //发送value到ch
<-ch //接收并且丢弃
num := <-ch //从ch中接收数据,并赋值给num
num, ok := <-ch //功能上和上面的一样,但是它还同时检查是否已经关闭或者为空
var ch1 chan int // 普通channel
var ch2 chan <- int // 只用于写int数据
var ch3 <-chan int // 只用于读int数据
channel作为一种原生类型,本身也可以通过channel进行传递,例如下面这个流式处理结构:
type PipeData struct {
value int
handler func(int) int
next chan int
}
func handle(queue chan *PipeData) {
for data := range queue {
data.next <- data.handler(data.value)
}
}
下面是用chan来解决并发的问题,chan相当于队列,先入先出,当有数据进入chan中后,num才能获取chan中的数据,主线程才能继续执行。如果没有数据写入chan,就会一直阻塞。
select
select()函数用来监控一组描述符,该机制常被用于实现高并发的socket服务器程序。Go语言直接在语言级别支持select关键字,用于处理异步IO问题,大致结构如下:
1.select+case是用于阻塞监听goroutine的,如果没有case,就单单一个select{},则为监听当前程序中的goroutine,此时注意,需要有真实的goroutine在跑,否则select{}会报panic。
2.select底下有多个可执行的case,则随机执行一个。
3.select常配合for循环来监听channel有没有故事发生。需要注意的是在这个场景下,break只是退出当前select而不会退出for,需要用break TIP / goto的方式。
4.无缓冲的通道,则传值后立马close,则会在close之前阻塞,有缓冲的通道则即使close了也会继续让接收后面的值。
func main() {
ch := make(chan int,1)
for i := 0; i < 10; i++{
select {
case ch <- i:
case x := <- ch:
fmt.Println(x)
}
}
}
如果管道没有指定大小就会产生一个死锁,运行后代码输出0,2,4,6,8。因为这个管道的缓冲值只有1
,那么同一时间只会有一个case
执行,这个channel
不是空的就是满的。
select的case语句中,都是对应一个I/O操作,准确的说是对应一个channel的I/O操作,那么到这里也应该可以理解为什么在code-1中,一个无缓冲的channel能在那段代码中产生一个deadlock
。当某个case得到执行后,就会退出select。
go中协程也会存在一些资源竞争,比如下面代码如果不加锁的话运行结果很难得到5000,加锁后可以得到想要的结果。
var lock sync.Mutex
type Money struct {
amount int
}
func (m *Money)Add(i int) {
m.amount += i
}
func (m *Money) Minute(i int) {
lock.Lock()
defer lock.Unlock()
if m.amount >= i {
m.amount = m.amount - i
}
}
func (m *Money) Get() int {
return m.amount
}
func main() {
m := new(Money)
m.Add(10000)
for i := 0; i < 1000; i++ {
go func() {
time.Sleep(500 * time.Millisecond)
m.Minute(5)
}()
}
time.Sleep(2 * time.Second)
fmt.Println(m.Get())
}