Go语言的核心Routine-Channel
前言
Go语言通过routine,提供了并发编程的支持。
Routine特性
(1) goroutine是Go语言运行库的功能,不是操作系统提供的功能,goroutine不是用线程实现的。
例:启动一个routine
go + 函数名即可启动一个goroutine
package main import ( "fmt" ) func p() { fmt.PrintLn("hello py") } func main() { go p() }
(2) goroutine就是一段代码,一个函数入口,以及在堆上为其分配的一个堆栈。所以它非常廉价,我们可以很轻松的创建上万个goroutine,但它们并不是被操作系统所调度执行。
(3) 除了被系统调用阻塞的线程外,Go运行库最多会启动$GOMAXPROCS个线程来运行goroutine。
例:使用GOMAXPROCS(设置CPU数)
package main import ( "runtime" "time" ) var _ = runtime.GOMAXPROCS(3) var a, b int func u1() { a = 1 b = 2 } func u2() { a = 3 b = 4 } func p() { println(a) println(b) } func main() { go u1() go u2() go p() time.Sleep(1 * time.Second) }
(4) goroutine是协作式调度的,如果goroutine会执行很长时间,而且不是通过等待读取或写入channel的数据来同步的话,就需要主动调用Gosched()来让出CPU。
例:Gosched()让出CPU
package main import ( "fmt" "runtime" ) func say(s string) { for i := 0; i < 2; i++ { runtime.Gosched() fmt.Println(s) } } func main() { go say("world") say("hello") }
(5) 和所有其他并发框架里的协程一样,goroutine里所谓“无锁”的优点只在单线程下有效,如果$GOMAXPROCS > 1并且协程间需要通信,Go运行库会负责加锁保护数据,这也是为什么sieve.go这样的例子在多CPU多线程时反而更慢的原因。
(6)
Web等服务端程序要处理的请求从本质上来讲是并行处理的问题,每个请求基本独立,互不依赖,几乎没有数据交互,这不是一个并发编程的模型,而并发编程框架只是解决了其语义表述的复杂性,并不是从根本上提高处理的效率,也许是并发连接和并发编程的英文都是concurrent吧,很容易产生“并发编程框架和coroutine可以高效处理大量并发连接”的误解。
(7) Go语言运行库封装了异步IO,所以可以写出貌似并发数很多的服务端,可即使我们通过调整$GOMAXPROCS来充分利用多核CPU并行处理,其效率也不如我们利用IO事件驱动设计的、按照事务类型划分好合适比例的线程池。在响应时间上,协作式调度是硬伤。
(8) goroutine最大的价值是其实现了并发协程和实际并行执行的线程的映射以及动态扩展,随着其运行库的不断发展和完善,其性能一定会越来越好,尤其是在CPU核数越来越多的未来,终有一天我们会为了代码的简洁和可维护性而放弃那一点点性能的差别。
Routine通信:Channel
routine之间是通过Channel进行通信的;Channel是进程内的通信,不支持挂进程的。
语法
// 声明方式,在此ElemType是指此管道所传递的类型 var chanName chan ElemType // 声明一个传递类型为int的管道 var ch chan int // 声明一个map,元素是bool型的channel var m map[string] chan bool // 定义语法,定义需要使用内置函数make()即可,下面这行代码是声明+定义一个整型管道 ch := make(chan int) // 事先定义好管道的size,下面这行代码定义管道的size为100 ch := make(chan int, 100) // 由管道中读写数据,<-操作符是与最左边的chan优先结合的 // 向管道中写入一个数据,在此需要注意:向管道中写入数据通常会导致程序阻塞,直到有 // 其他goroutine从这个管道中读取数据 ch<- value // 读取数据,注意:如果管道中没有数据,那么从管道中读取数据会导致程序阻塞,直到有数据 value := <-ch // 单向管道 var ch1 chan<- float64 // 只能向里面写入float64的数据,不能读取 var ch2 <-chan int // 只能读取int型数据 // 关闭channel,直接调用close()即可 close(ch) // 判断ch是否关闭,判断ok的值,如果是false,则说明已经关闭(关闭的话读取是不会阻塞的) x, ok := <-ch
什么样的场景使用管道?
首先我们知道Channel可以在routine中进行通信,如下例:
package main import "fmt" func print() { fmt.Println("Hello world") } func main() { for i := 0; i < 10; i++ { go print() } }
上面的代码意思大致是:使用协程来并行输出10次 “Hello world”, 但是大家运行上面代码的时候,会发现不会有输出。这是因为虽然使用go关键字进行了协程的创建,但是还没有等到执行的时候,main函数已经退出来了,进程已经关闭,所以起来的协程也不会被执行。
如果你有C相关的多线程经验时,可已经将协程改为线程,之后调用线程的join方法,让主线程等待子线程执行完毕后再退出。而在Go语言中,我们可以利用管道的写入阻塞和读取阻塞来完成类似线程join的行为。代码如下所示:
package main import "fmt" func print(ch chan int) { fmt.Println("Hello world") ch<- 1 } func main() { chs := make([]chan int) for i := 0; i < 10; i++ { chs[i] = make(chan int) go print(chs[i]) } for _, ch := range(chs){ <-ch } }
通过以上代码,我们就可以完成了并行输出10此Hello world 的效果。