Golang在语言级别支持了协程,由runtime进行管理。
在Golang中并发执行某个函数非常简单:
func Add(x, y int) { fmt.Println(x + y) } func RunRoutine() { for i := 0; i < 10; i++ { go Add(i, i) } }
但是输出为空。
因为虽然新建了协程调用Add函数,但是该协程还没有来得及执行,程序就结束了。所以输出为空。
如果想让代码按预想的方式运行,就需要让主函数等待所有goroutine退出后再结束。这就引出了goroutine间通信的问题。
首先,我们先用最简单粗暴,也是传统的Lock来解决。
这时的思路是:我们加一个全局的变量count。每次调用了Add,我们就count++。这样,当count=10的时候,就说明10个goroutine都执行完成了。
但是,全局变量的访问需要加锁,这样才能保证count的访问是安全的。
代码如下:
var ( lock *sync.Mutex count int ) func Add(x, y int) { lock.Lock() defer lock.Unlock() fmt.Println(x + y) count++ } func RunRoutine() { lock = &sync.Mutex{} count = 0 for i := 0; i < 10; i++ { go Add(i, i) } for { lock.Lock() temp := count if temp >= 10 { break } lock.Unlock() } }
这样,执行结果如下:
2 4 18 10 12 14 6 16 0 8
根据结果来看,10个协程都执行结束了,并且10个协程的执行顺序也是随机的。
但是,事情貌似变得糟糕了。我们为了实现一个简单的功能,却写出了非常复杂的代码。
如果用Golang来解决呢,这时我们考虑用channel来解决问题。
channel是Golang提供的goroutine间的通信方式。我们可以使用channel在多个goroutine间传递消息。当然,channel是进程内的通信方式,如果需要进程间通信,可能Socket或者HTTP通信协议更合适。
先看看,如果用channel,如果解决上面的问题。
var ( chs []chan int ) func Add(x, y int) { fmt.Println(x + y) chs[x] <- 1 } func RunRoutine() { chs = make([]chan int, 10) for i := 0; i < 10; i++ { chs[i] = make(chan int) go Add(i, i) } for _, ch := range chs { <-ch } }
这里,我们定义了一个10个元素的channel数组。每次调用Add时,我们在对应的channel中写入一个数据。最后,我们在RunRoutine中遍历了整个数组,当所有的channel都读取完数据,说明10个goroutine都运行结束了。
现在,我们看看channel的语法:
channel的声明:
var chanName chan ElementType
例如: var ch chan int
channel的初始化:
可以利用make对channel进行初始化:
ch = make(chan int)
ch = make(chan int, 1)
前者初始化了一个无缓冲的channel。无缓冲即当某个协程在channel中写入了数据,就马上被阻塞,要等到其他协程消费了该数据,协程才会继续执行。
后者初始化了一个有缓冲的channel,缓冲长度为1。即ch为空时,当某个协程往ch中写入数据,并不会马上阻塞。当ch内有1个数据时,再往ch中写入数据,即会马上阻塞,知道协程消费了数据,使ch内的数据小于等于其缓冲量。
有缓冲的channel可以用range进行读取。
channel的读写:
ch <- 1
i := <- ch
总之就是用箭头来进行读写操作,很直观。
需要注意的就是读写操作带来的阻塞。
select:
select是类似switch的,用来处理channel异步IO的问题。
select { case <- ch: case ch <- 1: default: }
需要注意的是,多个case同时符合的时候,switch是按顺序执行的,select是随机选择一个分支执行的。
channel的超时机制:
Golang的channel并没有自带的超时机制,但是可以用select来实现。
考虑以下的几种方法:
timeout := make(chan bool, 1) go func() { time.Sleep(time.Second) timeout <- true }() select { case <- ch: case <- timeout: }
该方法提供了一个timeout,利用协程sleep 1s之后向timeout中写入一个true。当select中的ch在1秒内没有读出数据时,timeout将读出数据。
select { case <- ch: case <-time.After(time.Second): }
该方法利用了time.After。该方法的问题在于,这个计时器在select执行之后仍在在runtime中存在。这在高并发的应用场景下会产生性能问题。
to := time.NewTimer(time.Second) for { to.Reset(time.Second) select { case <-c: case <-to.C: } }
该方法算是上一种方法的改进。该方法利用了一个全局的timer,这样可以避免高并发下的计时器的滥用。
单向channel:
var ( ch1 chan int ch2 chan<- int ch3 <-chan int )
ch1是普通channel,ch2是只写channel,ch3是只读channel。
ch4 := make(chan int) ch5 := <-chan int(ch4) ch6 := chan<- int(ch4)
上面是各种channel之间的类型转换。
关闭channel:
close(ch)
好像并没有什么其他可说的。唯一一点是,我们可以利用多返回值,在读取channel的时候检查channel是否被关闭。
val, ok := <-ch
多核并行:
可以利用如下命令进行设置。但是Golang是否真的可以利用多个核心,还需要实际验证。
runtime.GOMAXPROCS(16)
同步锁:
最开始的时候已经利用锁实现了功能。需要注意的是Lock和Unlock的对应。
唯一性操作:
sync.Once(funcName)
思考了一下,大概可以用来实现单例模式。