在了解之前,要注意golang是并发语言而不是并行语言
并发和并行
- 并发是一次性做大量事情的能力(两个或多个事件在同一时间间隔发生)
- 并行同一时间执行多个任务的能力(两个或者多个事件在同一时刻发生)
举例说明:
每天早上10分钟我洗脸,刷牙,吃早饭等等很多事情,这就是并发。 我一边刷牙的同时在烧水做饭这就是并行。
技术层面来说:假如一个web网页中有视频播放和文件下载两个动作,当浏览器在单核的处理器下运行时, CPU核心会在这两个事件中来回切换,(同时)播放视频和下载,这就称为并发。并发进程在不同的时间点开始并有着重叠的执行周期。假如你的CPU是多核处理器,那么下载和播放会在不同的CPU核心同时执行,这就是并行。
goroutine
在go中,每一个并发执行的操作都称为goroutine,当一个程序启动时,只有一个goroutine来调用main函数,称它为主goroutine。新的goroutine通过go语法来创建。
f() // 调用f(); 等待它返回 go f() //新建一个调用f()的goroutine,不用等待。
调度模型
groutine能拥有强大的并发实现是通过GPM调度模型实现:
- G: 代表一个goroutine,它有自己的栈,instruction pointer和其他信息(正在等待的channel等等),用于调度。
- M: 代表内核级线程,一个M就是一个线程,goroutine就是跑在M之上的;M是一个很大的结构,里面维护小对象内存cache(mcache)、当前执行的goroutine、随机数发生器等等非常多的信息
- P: 全程processor,处理器,它的主要作用来执行goroutine,所以它维护了一个goroutine队列。里面存储了所有需要它来执行的goroutine。
- Sched: 代表调度器,它维护有存储M和G的队列以及调度器的一些状态信息等。
调度实现
- 有2个物理线程M,每一个M都拥有一个处理器P,每一个也都有一个正在运行的goroutine。
- P的数量可以通过GOMAXPROCS()来设置,它其实也就代表了真正的并发度,即有多少个goroutine可以同时运行。图中灰色的那些goroutine并没有运行,而是出于ready的就绪态,正在等待被调度。P维护着这个队列(称之为runqueue),
- Go语言里,启动一个goroutine很容易:go function 就行,所以每有一个go语句被执行,runqueue队列就在其末尾加入一个goroutine,在下一个调度点,就从runqueue中取出(如何决定取哪个goroutine?)一个goroutine执行。
如果一个线程阻塞会发生什么情况呢?如下图
从上图中可以看出,一个线程放弃了它的上下文让其他的线程可以运行它。M1可能仅仅为了让它处理图中系统调用而被创建出来,或者它可能来自一个线程池。这个处于系统调用中的线程将会保持在这个导致系统调用的goroutine上,因为从技术上来说,它仍然在执行,虽然阻塞在OS里了。
另一种情况是P所分配的任务G很快就执行完了(分配不均),这就导致了这个处理器P很忙,但是其他的P还有任务,此时如果global runqueue没有任务G了,那么P不得不从其他的P里拿一些G来执行。一般来说,如果P从其他的P那里要拿任务的话,一般就拿run queue的一半,这就确保了每个OS线程都能充分的使用,
使用goroutine
package main import ( "fmt" "time" ) func cal(a int , b int ) { c := a+b fmt.Printf("%d + %d = %d ",a,b,c) } func main() { for i :=0 ; i<10 ;i++{ go cal(i,i+1) //启动10个goroutine 来计算 } time.Sleep(time.Second * 2) // sleep作用是为了等待所有任务完成 }
GOMAXPROCS
设置goroutine运行的CPU数量,最新版本的go已经默认已经设置了。
num := runtime.NumCPU() //获取主机的逻辑CPU个数 runtime.GOMAXPROCS(num) //设置可同时执行的最大CPU数
也可以根据个人手动设置,例如
func a() { for i := 1; i < 10; i++ { fmt.Println("A:", i) } } func b() { for i := 1; i < 10; i++ { fmt.Println("B:", i) } } func main() { runtime.GOMAXPROCS(1) go a() go b() time.Sleep(time.Second)
上面GOMAXPROCS设置为1,当遇到两个go调度时,就会发生等待。如果设置为2就会并行执行(前提是你的cpu数量>=2),如下例子
func a() { for i := 1; i < 10; i++ { fmt.Println("A:", i) } } func b() { for i := 1; i < 10; i++ { fmt.Println("B:", i) } } func main() { runtime.GOMAXPROCS(2) go a() go b() time.Sleep(time.Second) }
在执行上面代码时细心的同学会发现,每一步最后都会睡眠一秒,才能打印结果,这是因为并发执行,goroutine还没来得及返回结果,主线程已经执行完了。
那么如果不会面有没有其他的方法?当然有。第一种便是采用sync.WaitGroup来实现
var wg sync.WaitGroup func hello(i int) { defer wg.Done() // goroutine结束就登记-1 fmt.Println("Hello Goroutine!", i) } func main() { for i := 0; i < 10; i++ { wg.Add(1) // 启动一个goroutine就登记+1 go hello(i) } wg.Wait() // 等待所有登记的goroutine都结束 }
详细用法详见sync包。另外一种便是channel
channel
channel是用来传递数据的一个数据结构,同map一样使用内置的make来创建。如
ch := make(chan int) // 无缓冲通道 ch1 := make(chan int, 10) //缓冲为10的通道
channel类型
定义格式
ChannelType = ( "chan" | "chan" "<-" | "<-" "chan" ) ElementType .
它表示三种类型的定义,可选的<-表示channel的方向。如果没有指定,即表示双向通道,既可以接收,也可以发送。
chan T // 可以接收和发送类型为 T 的数据 chan<- float64 // 只可以用来发送 float64 类型的数据 <-chan int // 只可以用来接收 int 类型的数据
<-总是最优先与最左边类型结合。如
chan<- chan int // 等价 chan<- (chan int) chan<- <-chan int // 等价 chan<- (<-chan int) <-chan <-chan int // 等价 <-chan (<-chan int) chan (<-chan int)
channel操作
常见三种操作,接收,发送和关闭
ch := make(chan int)
- 发送:ch <- 1 //将1发送到ch通道中
- 接收:x := <-ch // 从ch接收值并赋给x。也可以直接抛弃:<-ch
- 关闭:close(ch)
close时可以通过i, ok := <-c
可以查看Channel的状态,判断值是零值还是正常读取的值。
c := make(chan int, 10) close(c) i, ok := <-c fmt.Printf("%d, %t", i, ok) //0, false
无缓冲通道
需要注意的是,无缓冲通道上的发送操作将会阻塞,值到另一个goroutine在对立的通道上执行接收操作。这时值才算完成。相反,如果接收操作先执行,接收方goroutine将阻塞,直到另一个goroutine在同一个通道发送一个值。
package main import "fmt" func sum(s []int, c chan int) { sum := 0 for _, v := range s { sum += v } c <- sum // 把 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 中接收 fmt.Println(x, y, x+y) }
打印结果
-5 17 12
单向通道
当程序演进时,将大的函数拆分为多个更小的是很自然的,在当一个通道用作函数的行参时,它几乎总是被有意地限制不能发送或接收。为了将这种意图可以比避免误用,在go的类型系统提供了单向通道。仅仅导出发送或者接收操作。如类型chan <- int是一个只能发送的通道。反之 <- chan int是一个只能接收int类型通道。
func sum(out chan<- int) { for i := 0; i < 100; i++ { out <- i } close(out) } func squarer(out chan<- int, in <-chan int) { for i := range in { out <- i * i } close(out) } func printer(in <-chan int) { for i := range in { fmt.Println(i) } } func main() { ch1 := make(chan int) ch2 := make(chan int) go sum(ch1) go squarer(ch2, ch1) }
有缓冲通道
缓冲通道有一个元素队列,队列的最大长度在创建时通过make的容量参数来设置。
ch := make(chan string, 3)
一个空的缓冲通道
缓冲通道上的发送操作在队列的尾部插入一个元素,接收操作从队列的头部移除一个元素。如果通道满了,发送操作将会阻塞所在的goroutine直到另一个goroutine对它进行移除操作留出可用的空间。反过来,如果通道空了。执行接收操作的goroutine阻塞,直到另一个goroutine在通道上发送数据。
func main() { ch := make(chan string, 3) ch <- "a" ch <- "b" ch <- "c" fmt.Println("发送成功") x := <-ch // 打印a }
range
func main() { go func() { time.Sleep(1 * time.Hour) }() c := make(chan int) go func() { for i := 0; i < 10; i = i + 1 { c <- i } close(c) }() for i := range c { fmt.Println(i) } fmt.Println("Finished") }
如上面的例子,range c 产生的迭代值为channel中发送的值,它会一直迭代直到channel关闭。如果此时close(c)关掉。程序会一直阻塞在for....range c 这一行。
select
select
语句选择一组可能的send操作和receive操作去处理。它类似switch
,但是只是用来处理通讯(communication)操作。它的case
可以是send语句,也可以是receive语句,亦或者default
。receive
语句可以将值赋值给一个或者两个变量。它必须是一个receive操作。最多允许有一个default case
,它可以放在case列表的任何位置,尽管我们大部分会将它放在最后。
func fibonacci(c, quit chan int) { x, y := 0, 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) }
如果有同时多个case去处理,比如同时有多个channel可以接收数据,那么Go会伪随机的选择一个case处理(pseudo-random)。如果没有case需要处理,则会选择default
去处理,如果default case
存在的情况下。如果没有default case
,则select
语句会阻塞,直到某个case需要处理。
特别注意的是nil channel会一直被阻塞。如果没有default: nil chanel会一直阻塞。
最后列出channel的几种常用关系
小结:
在处理并发时会发生数据错乱的情况,这时候就会用到锁机制,如上面一开始介绍sync包。锁将会在sync包中描述。