文章目录
- 写在前面
- 关于并发
- Go协程与通道
- 在Go中使用协程
- 使用channel在协程间通信
- 使用select来切换协程操作
写在前面
提及Go,大家都会人云亦云一句“Go支持高并发,适合使用高并发的场景”,事实也确实如此,Go学习笔记系列也终于到了该介绍下最著名的Go并发的时候了,没有介绍并发的Go文章是没有灵魂的哈哈^^
关于并发
提及并发,很容易联想到另外一个概念:并行。它们两个的区别是:
- 并发主要由切换时间片来实现多个任务“同时”运行
- 并行是直接通过多核实现多个任务同时运行
一个并发程序可以只在一个处理器或内核上运行多个任务,但是某一时间点只有一个任务在运行;如果运行在多处理器或多核上,就可以在同一时间点有多个任务在运行,才能实现真正的并行,因此,并发程序可以是运行在单处理器(单核)上也可以是运行在多处理器(多核)上。而在Go中,可以设置核数,让并发程序在多核心上真正并行运行,充分发挥多核计算机的能力
Go协程与通道
在其它编程语言中,实现并发程序往往是使用多线程的技术。在一个进程中有多个线程,它们共享同一个内存地址空间。然而使用多线程难以做到准确,尤其是内存中数据共享的问题,它们会被多线程以无法预知的方式进行操作,导致一些无法重现或者随机的结果。多线程解决这个问题的方式是同步不同的线程,对数据加锁
但是,这会带来更高的复杂度,更容易使代码出错以及更低的性能,所以这个经典的方法明显不再适合现代多核/多处理器编程
在Go中,使用协程(goroutines)来实现并发程序:
- 协程是轻量级的,比线程要更轻,只需要使用少量的内存和资源(每个实例4-5k左右的内存栈),因此在必要时可以创建大量的协程,实现高并发
- 协程与操作系统线程之间没有一对一的关系,协程是根据一个或多个线程的可用性,映射(多路复用)在它们之上的
- Go多个协程之间使用通道(channel)来同步(它不通过共享内存来通信,而是通过通信来共享内存),当然Go中也提供了
包可以进行传统的加锁同步操作,但并不推荐使用sync
在Go中使用协程
在Go中使用协程是通过关键字go调用一个函数或者方法来实现的:
import (
"fmt"
"time"
)
func main(){
go Goroutine()
time.Sleep(3 * time.Second)
}
func Goroutine(){
fmt.Println("start a goroutine")
}
根据上面的代码,我们定义了一个
Goroutine
的函数,并在主程序中使用
go
关键字去调用该函数,从而启动一个协程去执行
Goroutine
这个函数。在程序中使用
time.Sleep(3*time.Second)
是为了让主程序延时3s再结束,否则主程序启动完一个协程后立即退出,我们将没法看到协程函数中打印的信息
新版本的Go(应该是1.8之后)当我们启动多个协程时,Go将会自动启动多个核心来并行运行,而在老版本的Go里需要我们手动设置,手动设置多核心的操作如下:
import (
"fmt"
"runtime"
"time"
)
func main(){
num := runtime.NumCPU()
runtime.GOMAXPROCS(num) //新版本会自动设置
for i := 0; i < 10; i++ {
go Goroutine(i)
}
time.Sleep(3 * time.Second)
}
func Goroutine(){
fmt.Println("start a goroutine")
}
使用channel在协程间通信
通道(channel)是一种特殊的类型,可以理解为发送某种其它类型数据的管道,用于在协程之间通信。数据利用通道进行传递,在任何时间,通道中的数据只能被一个协程进行访问,因此不会发生数据竞争
- channel通过
进行创建,使用make
来关闭close
如果只是声明channel,未初始化,它的值为var ch1 chan string ch1 = make(chan string) //创建一个用于传递string类型的通道
上面两行代码也可以简写为nil
ch1 := make(chan string)
- 通道的操作符
<-
- 往通道发送数据:
表示把变量ch <- int1
发送到通道int1
中ch
- 从通道接收数据:
表示变量int2 <- ch
从通道int2
中接收数据(如果ch
没有事先声明过,则要用int2
)。直接使用int2 := <- ch
也可以,也表示从通道中取值,然后该值会被丢弃<-ch
上面的代码创建了一个布尔型的channel,主程序启动协程后就一直阻塞在func main(){ c := make(chan bool) go func() { //使用匿名函数,闭包,所以可以获取到外层的channel变量 fmt.Println("go go go") c <- true }() <-c //阻塞,直到从通道取出数据 }
那里等待从通道中取出数据,协程中当打印完数据后,就往通道中发送<-c
。主程序此时方从通道中取出数据,退出程序。从而不需要手动让主程序睡眠等待协程完成true
- 往通道发送数据:
- 大多数情况下channel默认都是阻塞的:从channel取数据一端会阻塞等待channel有数据可以取出才往下执行(如上一段代码中所示);往channel发送数据一端需要一直阻塞等到数据从channel取出才往下执行。如果把上面那段代码中往channel中读取数据的位置调换一下,程序依旧会正常输出
func main(){ c := make(chan bool) go func() { //使用匿名函数,闭包,所以可以获取到外层的channel变量 fmt.Println("go go go") <-c }() c <- true //这里把数据传入通道后也会阻塞知道通道中数据被取出 }
- 根据需要,channel也可以被设置为有缓存的,有缓存的channel在通道被填满之前不会阻塞(异步)。上面的程序,如果设置为有缓存的channel,那么主程序往通道中发送数据之后就直接退出了
func main(){ c := make(chan bool, 1) go func() { //使用匿名函数,闭包,所以可以获取到外层的channel变量 fmt.Println("go go go") <-c }() c <- true //这里往通道发完数据就直接退出了 }
这里make(chan type, buf)
是通道可以同时容纳的元素的个数,如果容量大于 0,通道就是异步的了:缓冲满载(发送)或变空(接收)之前通信不会阻塞,元素会按照发送的顺序被接收buf
- 使用
来操作channelfor-range
可以用在通道上,以便从通道中获取值:for-range
它从指定的通道中读取数据直到通道关闭才能执行下面的代码,因此程序必须在某个地方for v := range ch { fmt.Println(v) }
该通道,否则程序将死锁close
func main(){ c:=make(chan bool) go func(){ fmt.Println("gogogo") c <- true close(c) }() for v := range c{ fmt.Println(v) } }
此外,关于channel还需要注意:
- channel可以设置为单向(只读或只写)或双向通道(能读能写),默认是双向的
- channel是引用类型
- 一个channel只能传递一种类型的数据
使用select来切换协程操作
使用
select
可以从不同的并发执行的协程中获取值,它和
switch
语句很类似。
select
可以用来监听进入通道的数据,也可以向通道发送数据
select {
case u:= <- ch1:
...
case v:= <- ch2:
...
...
default: // no value ready to be received
...
}
select
的功能其实就是处理列出的多个通信中的一个
-
语句是可选的,default
是不允许的,任何一个fallthrough
中执行了case
或者break
语句,return
就结束了select
- 如果所有
的通道都阻塞了,会等待直到其中一个可以处理case
- 如果有多个
的通道可以处理,会case
选择一个处理随机
- 如果没有通道操作可以处理并且写了
语句,它就会执行default
语句default
- 在
中使用发送操作并且有select
可以确保发送不被阻塞!如果没有default
,default
就会一直阻塞select
-
也可以设置超时处理select
下面的代码是一个类似生产者-消费者的模式,包括了两个通道和三个协程,其中协程
goroutine1
和
goroutine2
分别往通道
ch1
和
ch2
中写入数据,协程
goroutine3
则通过
select
分别从两个通道中读出数据并输出
func main() {
ch1 := make(chan int)
ch2 := make(chan int)
go goroutine1(ch1)
go goroutine2(ch2)
go goroutine3(ch1, ch2)
time.Sleep(1e9)
}
func goroutine1(ch chan int) {
for i := 0; ; i++ {
ch <- i * 2
}
}
func goroutine2(ch chan int) {
for i := 0; ; i++ {
ch <- i + 5
}
}
func goroutine3(ch1, ch2 chan int) {
for {
select {
case v := <-ch1:
fmt.Printf("Received on channel 1: %d\n", v)
case v := <-ch2:
fmt.Printf("Received on channel 2: %d\n", v)
}
}
}