Code is cheap, talk is expensive
学习Go语言也有一段时间了,Go带给我的最直观的感受就是强大,简洁的并发能力。
于是乎,Go在Web等需要并发的地方大方光彩。我还记得第一次用Go写出一个httpServer是,
简直不敢相信如此简单明了。这可能是我接触语言较少有关系吧,反正如果是用C语言来写的花,
我估计还在为Socket监听,和多线程困恼。反正Go带给我的就是怎么酸爽!
Go语言中,每一个并发的活动成为goroutine
,创建一个goroutine
只需要在调用的函数前面加上关键字go
,即可创建一个协程。
goroutine
类型线程,但是比线程过于轻量,称为协程
。
这里给出书本上面的示例,实现一个并发的时钟服务器。
func main(){ lis,err := net.Listen("tcp","localhost:8080") if err != nil { log.Fatal(err) } for { conn,err := lis.Accept() if err != nil { log.Println(err) } go func(){ defer conn.Close() for { _,err := io.WriteString(conn, time.Now().Format("15:04:05\n")) if err != nil { return } time.Sleep(1*time.Second) } }(conn net.Conn) /* 这里不要使用内部函数来捕获变量,申明一个参数,将需要的变量传递进来。 */ } }
这里唯一需要关注的就是go
启动的匿名函数,这个函数处理每一个连接请求,然后每秒钟发送一次格式化的时间。
仅仅加上了go
,就让简易的Server有了并发能力。
对于多个协程,它们可能做着互相独立不干扰的工作,但也可能协程之间需要密切配置,来共同完成同样的任务,比如说支持并发的WebSpider
,可能有许多的goroutine
不辞劳苦地爬爬爬,
但是改如何控制它们的整体行为,比如说某个goroutine
爬过的url
,其它的就不用再爬了。又或者说,用户想要停止,如何通知每个goroutine
,让它们停下手中的活,乖乖的退出呢?
这个时候或许通道就派上用场了,这也是Go语言推荐的方案。
通道
是可以让一个goroutine
发送特定值到另一个goroutine
的通信机制。每一个通道
是一个具体类型
的导管,叫做通道的元素类型
。
通道
使用内置函数make
来创建,通道
类似map
传递的是引用,零值是nil
。
ch := make(chan int) //创建一个通道
通道
支持发送
接送
和 关闭
操作,前两个是通信,有点类似socket
,在发送和接受的时候,若缓冲区满/空,则会进入阻塞状态,关闭后的通道,只允许读
,写
会宕机。
ch <- x //发送
x = <-ch //接送&赋值
<- ch //仅接送
close(ch) //关闭一个通道
通道
在创建的时候可以指定缓存的大小,默认的没有缓存。此时对其的读写操作都会造成阻塞,因此需要至少两个goroutine
,一个负责读,另一个负责写,完成goroutine
之间的通信。
没有缓冲的通道的特性实现两个goroutine
同步化,因此又称为同步通道
。这个时候若仅仅作为同步使用,一般会创建make(chan struct{})
这个的通道以此强调。
听名字是不是很熟悉,类似shell
里面的管道概念,例如ps aux|grep XX
我们就使用到管道,一个命令的输出作为另一个命令的输入。
Go的通道很容易实现管道的功能
,下面是一个管道的示例
func main(){ naturals := make(chan int) squares := make(chan int) //计数器 go func(){ for x:=0 ; x<100 ; x++ { naturals <- x } close(naturals) //通知接收方停止 }() //平方产生器 go func(){ for x := range naturals { squares <- x*x } close(squares) //同上 }() //打印器 for x := range squares { fmt.println(x) } }
上面的例程有点类似流水生产线,上游->中游->下游,上游只负责写,中游负责读写,下游负责读。
对于明确知道只使用管道的读写之一的,我们可以写成函数,写成单通道的参数。
func counter(out chan<- int) //只写
func squarer(out chan<- int, in <-chan int)
func printer(in <-chan int)
这样若在函数内部错误的读一个只写的通道,编译器就会报错,可以帮助我们及早发现问题。
在创建的通道的时候指定大于零的容量,通道就变成了缓存通道
,缓存通道
有队列
的特性。
缓存通道
的大小
,容量
可以使用len
,cap
内置函数来获取。
当一个goroutine
需要处理多个通道
的时候,我们不能让一个某个通道阻塞我们的goroutine
从而忽略了其它通道的处理,这个时候select
就排上用场了(有点类似socket
的轮询操作)。
select { case <-ch1: case x := <-ch2: case ch3 <- y: //可选 default: //当default出现时,select永不阻塞,相当于轮询 //没有default时,若所有case都是阻塞状态,这select会阻塞, //有任一case得到响应,select则进入相应的case执行代码 //若同时有多个case满足条件,select随机选择一个进入 }
由于通道的可以是nil
,零值,对零值的通道进行读写将会永远阻塞,
因此select
中的零值case
将永不会选中,这个特性可以用来开启/关闭一些功能。
来个示例,加深一下理解
var verbose = flag.Bool("v", false, "Show verbose progress message") var done = make(chan struct{}) //标识是否停止 func main(){ flag.Parse() roots := flag.Args() if len(roots) == 0 { roots = []string{"."} } go func(){ os.Stdin.Read(make([]byte,1)) //任意按键停止程序 close(done) }() fileSize := make(chan int64) //接受文件大小 var n sync.WaitGroup for _, root := range roots { n.Add(1) go walkDir(root, &n, fileSize) } go func(){ n.Wait() close(fileSize) }() var tick <-chan time.Time if *verbose { tick = time.Tick(500*time.Millisecond) //500毫秒显示一次 } var nfiles, nbytes int64 loop: for { select{ case <-done: for range fileSize{} //吃空所有goroutine return case size,ok := <-fileSize: if !ok { break loop //跳出for循环 } nfiles ++ nbytes += size case <-tick.C: //定时任务 printDiskUsage(nfiles,nbytes) } } printDiskUsage(nfiles, nbytes) fmt.Println(nbytes) } func printDiskUsage(nfiles,nbytes int64){ fmt.Printf("du: %d files, %s\n",nfiles,formatUnit(nbytes)) } func formatUnit(nbytes int64) string { units := []string{"bytes","Kb","Mb","Gb","Pb"} index := 0 var unit = units[index] var b = float64(nbytes) for b>1e3 && index<len(units) { index ++ unit = units[index] b /= 1e3 } return fmt.Sprintf(".2f %s",b,unit) } func isCanceled() bool { select{ case <-done: return true default: return false } } func walkDir(dir string, n *sync.WaitGroup, fileSize <-chan int64){ defer n.Done() if isCanceled() { return } for _,entry := range dirents(dir) { if entry.IsDir() { n.Add(1) subDir := filePath.Join(dir,entry.Name()) go walkDir(subDir,n,fileSize) } else { fileSize <- entry.Size() } } } var sema = make(chan struct{}, 20) //最多20个goroutine func dirents(dir string) []os.FileInfo { select { case sema <- struct{}{}: //获取令牌 case <-done: return nil } defer func(){ <-sema }() // 释放令牌 entries,err := ioutil.ReadDir(dir) if err != nil { fmt.Fprintf(os.Stderr,"du: %v\n", err) return nil } return entries }
这个示例解决的问题
goroutine
可谓是Go的一大法宝,配合通道
可谓锦上添花。
以后可能会有许多的坑,而且这些东西我的理解还不够。
需要学习的东西还好多,比如说并发安全
,变量共享
等等。