点击上方“黑光技术”关注之
完美之道,不在无可增加,而在无可删减!
介绍
golang语言中最有特色之一的东东就是这个goroutine了,很多时候问起别人为什么golang的好用,golang的网络性能可以那么好,一般都会多多少少想到goroutine,提起goroutine。在linux中内核的调度最小单位是就是thread,同一个进程中的多个thread线程就对应内核中的多个thread实体。所以thread是内核级的,而gorountine是一个不同于thread的概念,gorountine是一个用户态,另外一种说法也就携程,是用户态的一种调度粒度,每个gorountine也有自己的栈空间,而且是在用户内存中的。golang中实现了对用户态的一种代码片段的高效调度执行,就目前来看是非常有效的,而且给用户编程带来了极大的方便。
channel是golang中另外一个最有特色的东东,在c中我们都是用管道,文件,信号等作为线程间通信的手段,但是在golang中几乎都是清一色的使用channel这个东东。channel,即“管道”,是用来传递数据的一个类型,即可以向channel里放入数据,也可以从中获取数据。在golang也有其它的方式作为线程间或者说gorountine之间进行通信,但是golang的编程指导中强烈建议任何地方需要通信都要使用channel,所以channel加上goroutine,就可以组合成一种简单而又强大的处理模型,即N个工作goroutine将处理的中间结果或者最终结果放入一个channel,另外有M个工作goroutine从这个channel拿数据,再进行进一步加工,通过组合这种过程,从而胜任各种复杂的业务。
go提供了sync包和channel来解决协程同步和通讯。sync.WaitGroup是等待一组协程结束。它实现了一个类似任务队列的结构,你可以向队列中加入任务,任务完成后就把任务从队列中移除,如果队列中的任务没有全部完成,队列就会触发阻塞以阻止程序继续运行。当需要阻塞当前执行线程,等待一组goroutine执行完毕之后再继续执行当前线程时,就需要用到WaitGroup。
sync.WaitGroup只有3个方法,Add(),Done(),Wait()。 其中Done()是Add(-1)的别名。简单的来说,Add就是添加或者减少等待goroutine的数量;Done:相当于Add(-1)使用Add()添加计数,Done()减掉一个计数; Wait:执行阻塞,直到所有的WaitGroup数量变成0。
下面的示例中,在子goroutine中进行一个需要一段时间的操作,主goroutine可以做一些别的事情,然后等待子goroutine完成。接收方会一直阻塞直到有数据到来。如果channel是无缓冲的,发送方会一直阻塞直到接收方将数据取出。如果channel带有缓冲区,发送方会一直阻塞直到数据被拷贝到缓冲区;如果缓冲区已满,则发送方只能在接收方取走数据后才能从阻塞状态恢复。
// Allocate a channel.
c := make(chan int)
// 启动一个goroutine,来做一些事情,事情做完后发送消息到channel
go func() {
doSomethingForAWhile1()
c <- 1 // 事情做完了,发送一个消息,这里说是消息其实就是一个值,这个值不重要,下面也不会使用,关键就是有这样一个操作
}()
doSomethingForAWhile2()
<-c // 这里会一直阻塞,直到上面有消息发送进去,这里就是等待排序完成,多goroutine协作的时候可以用
这是一种经典用法,一个或者多个生产者,消费者也有可能是一个或者多个,在c++中的经典解法就是使用阻塞队列,生产者直接往队列中发送数据,消费者从队列中消费数据,在生产者遇到队列满就阻塞,消费者在遇到队列空的时候也阻塞。在golang中的解决方案就是使用channel:将请求都转发给一个channel,然后初始化多个goroutine读取这个channel中的内容,并进行处理。简单的代码如下
// 建立一个全局的channel
var task_channel = make(chan net.Conn)
然后,启动多个goroutine进行消费
[code lang="c"]
for i := 0; i < 5; i ++ { go func() { for { select { case task := <- task_channel: process(task) } } } () } 服务端接收到请求之后,将任务传入channel中即可:
for i := 0; i < 5; i++ {
go func() {
for {
select {
case conn := <- task_channel:
handleConn(conn)
}
}
}
}
}
for {
conn, err := listener.Accept()
if err != nil {
continue
}
count += 1
task_chanel <- conn
}
}
不过,上面方案也有一个问题:就是channel初始化时是没有设置长度,而channel的默认长度是0,即只能写入一个元素,再写入就会被阻塞,因此当所有处理请求的goroutine都正在处理请求时,再有请求过来的话,就会被block。因此,需要在channel初始化时增加一个长度:
var task_channel = make(chan net.Conn,task_channel_len)
这样一来,我们将task_channel_len设置得足够大,请求就可以同时接收task_channel_len个请求而不用担心被block。不过,这其实还是有问题的:那如果真的同时有大于task_channel_len个请求过来呢?一方面,这就应该算是架构方面的问题了,可以通过对模块进行扩容等操作进行解决。另一方面,模块本身也要考虑如何进行“优雅降级了”。遇到这种情况,我们应该希望模块能够及时告知调用方,“我已经达到处理极限了,无法给你处理请求了”。其实,这种问题,可以很简单的在Golang中实现:如果channel发送以及接收操作在select语句中执行并且发生阻塞,default语句就会立即执行。
select {
case task_channel <- task:
//do something
default:
//warnning!
return fmt.Errorf("task_channel is full!")
}
即使是复杂、耗时的任务,也必须设置超时时间。一方面可能是业务对此有时限要求(用户必须在XX分钟内看到结果),另一方面模块本身也不能都消耗在一直无法结束的任务上,使得其他请求无法得到正常处理。因此,也需要对处理流程增加超时机制。
我一般设置超时的方案是:和之前提到的“接收发送给channel之后返回的结果”结合起来,在等待返回channel的外层添加select,并在其中通过time.After()来判断超时。
select {
case conn := <- task_channel:
handleConn(conn)
case <- time.After(time.Second * 3):
//处理超时
}
channel作为go语言的一种原生类型,自然可以通过channel进行传递。通过channel传递channel,可以非常简单优美的解决一些实际中的问题。我们可以在主gorountine中通过channel将请求传递给工作goroutine。同样,我们也可以通过channel将处理结果返回给主goroutine。
主goroutine:
type Request struct {
args []int
resultChan chan int
}
request := &Request{[]int{3, 4, 5}, make(chan int)}
// Send request
clientRequests <- request
// Wait for response.
fmt.Printf("answer: %d\n", <-request.resultChan)
主goroutine将请求发给request channel,然后等待result channel。子goroutine完成处理后,将结果写到result channel。
func handle(queue chan *Request) {
for req := range queue {
result := do_something()
req.resultChan <- result
}
}
下面是一个最简单的用法,也是一般常用的方式。
package main
import (
"fmt"
"sync"
)
func main() {
var wg sync.WaitGroup
i := 0
for ; i < 10; i++ {
wg.Add(1)
go func(i int){
fmt.Println("Hello world test",i)
wg.Done()
}(i)
}
wg.Wait()
fmt.Println("run done: ",i)
}
参考:https://golang.org/doc/effective_go.html#channels
黑光技术文章推荐
Envoy源码分析之Dispatcher
看完本文有收获?请分享给更多人
关注「黑光技术」加星标,关注大数据+微服务
看完,赶紧点个“好看”呀
点这里
↓↓↓↓