go并发
Goroutines are functions or methods that run concurrently with other functions or methods. Goroutines can be thought of as light weight threads.
Goroutines/
Channel
使用go关键字启动一个协程(goroutine)
func main () {
go func () {
fmt.Println("world")
}()
fmt.Println("hello")
start := time.Now()
for {
if time.Now().Sub(start) > time.Second * 1 {
break
}
}
}
Concurrency is not parallelism
介于js和java之间,golang在用户空间实现轻量级的调度,也就是协程,协程在进程和线程的基础上做更高层次的抽象 。并发意味着在单核情况下两个协程依然像“并行”一样
package main
import (
"fmt"
"runtime"
"time"
)
func main() {
runtime.GOMAXPROCS(1)
go func() {
for i := 0; i < 10; i++ {
fmt.Println("goruntine1", i)
time.Sleep(time.Second)
}
}()
go func() {
for i := 0; i < 10; i++ {
fmt.Println("goruntine2", i)
time.Sleep(time.Second)
}
}()
go func() {
fmt.Println("dead")
for {
}
}()
time.Sleep(time.Second * 15)
}
/*
go的协程是并发的,由golang在用户空间内调度。
当设置程序的核数为1时,遇到死循环时,程序会卡死,其他协程等不到调度。
设置runtime.GOMAXPROCS(2),则不会卡死,因为只卡了一个核。
当然通常不会设置runtime.GOMAXPROCS(1),除非模拟单核环境。
*/
channel帮助协程间安全通信,围绕channel有很多延伸。这里可以先了解基本用法。
Synchronized Goroutines
同步外层goroutine与子goroutines
var ch chan int = make(chan int)
func main() {
go func() {
time.Sleep(time.Second)
fmt.Println("task 1 done")
ch <- 0 // 同步消息,主协程收到消息,阻塞取消。
}()
go func() {
time.Sleep(time.Second)
fmt.Println("task 2 done")
ch <- 0
}()
<-ch // 等待其他协程同步消息,阻塞中。
<-ch
fmt.Println("all done")
}
// 可以使用管道来做,有没有更好的方法呢?func main() {
wg := sync.WaitGroup{}
wg.Add(2)
go func() {
time.Sleep(time.Second)
fmt.Println("task 1 done")
wg.Done()
}()
go func() {
time.Sleep(time.Second)
fmt.Println("task 2 done")
wg.Done()
}()
wg.Wait() // 等待其他协程同步消息,阻塞中。
fmt.Println("all done")
}
// 使用sync.WaitGroup,可以更优雅的达到这个目的子goroutines之间
package main
import (
"fmt"
)
func main() {
ch := make(chan int)
ch2 := make(chan int)
go func() {
fmt.Println("first")
ch <- 0
}()
go func() {
<-ch
fmt.Println("second")
ch2 <- 0
}()
<-ch2
fmt.Println("all done")
}
// 无法再使用sync.WaitGroup,需要chan来实现
// 但是这种场景在实际编码中很少使用 :?
package main
import (
"fmt"
"sync"
)
func main() {
l1 := sync.Mutex{}
l2 := sync.Mutex{}
l1.Lock()
l2.Lock()
go func() {
fmt.Println("first")
l1.Unlock()
}()
go func() {
l1.Lock()
fmt.Println("second")
l2.Unlock()
}()
l2.Lock()
fmt.Println("all done")
}
// 初始化即取锁,再次取锁必然在换锁之后执行。
// 上面代码利用这一点做同步。
Do not communicate by sharing memory; instead, share memory by communicating
package main
var a string
var done bool
func setup() {
a = "hello, world"
done = true
}
func main() {
counter := 0
go setup()
for !done {
}
if a == "" {
println("empty string")
}
// 这段代码试图用共享内存的方式同步协程。
// go 不保证代码运行符合预期,也就是说有可能打印“empty string”
// see https://golang.org/ref/mem。
}Buffered Chan
make在生成chan的时候,可以将第二个参数设置为一个非0整型,此时返回buffered chan,参数为buffer大小,无第二个参数则为unbuffered chan(也叫synchronous chan)。buffered chan是一个线程安全的队列。unbuffered chan更像是传送门,两端是同步的,两端总是在互相等待。
var ch chan int = make(chan int)
go func () {
for i := 0 ; i < 10 ; i ++ {
go func (i int) {
ch <- i
print(i)
} (i)
}
} ()
go func () {
for i := 0 ; i < 10 ; i ++ {
<- ch
time.Sleep(time.Second)
}
} ()
time.Sleep(time.Second * 11)
/*
unbuffered chan,每1s打印一个。
在这种情况下,如果消费者比较慢,会使生成端的协程越积越多。
*/var ch chan int = make(chan int, 10) // buffered chan
go func () {
for i := 0 ; i < 10 ; i ++ {
go func (i int) {
ch <- i
print(i)
} (i)
}
} ()
go func () {
for i := 0 ; i < 10 ; i ++ {
<- ch
time.Sleep(time.Second)
}
} ()
time.Sleep(time.Second * 11)
/*
对比bufferd chan,生产者和消费者全异步,一下打印十个数字。
此场景用bufferd chan会提前释放生产者协程
*/经典代码段分析⬇️
var ch chan int = make(chan int, 4)
func concurrence(){
ch <- 1
defer func(){<- ch}()
// ...
}
/*
上面代码尝试使用buffered chan做并发控制。
当buffer长度为1的时候,效果跟加锁一样。
为4意味着并发度为4。
这样做有什么问题?
*/当然,其实这并不合适。如果使用这种方式,会无故产生大量阻塞的协程,从内存角度,每个调用栈至少4k,从调度角度,协程过多降低调度效率。如何实现一个并发数控制的模型?这篇文章讲得非常好。
Range Over/
Close
range over a chan,很多时候接收方不知道会接收多少条消息,此时需要用range处理chan,接收不定条目的消息。
package main
import "fmt"
func main() {
queue := make(chan string, 2)
queue <- "one"
queue <- "two"
close(queue)
for elem := range queue {
fmt.Println(elem)
}
}range over a chan 更像是一个语法糖,通过一个for循环,加上chan的接收,也可以模拟一个range over。我认为range over强调了chan与队列的相似性,chan实际上可以粗略的被认为是“协程安全的队列”
package main
import (
"fmt"
)
func main() {
queue := make(chan string, 2)
queue <- "one"
queue <- "two"
close(queue)
for {
elem, ok := <-queue
// 与写入不同,接收一个关闭的chan不会报错,会陷入死循环
// range over会自动判断已关闭chan的接收
// 用for循环当然不会,这是重点区别
if !ok {
break
}
fmt.Println(elem)
}
}如何停止数据传输?使用close。通常由发送端判断是否应该停止数据传输,接收端用“comma ok”判断是否结束。
import "time"
func main() {
var ch chan int = make(chan int)
go func() {
for i := 0; i < 10; i++ {
ch <- i
}
close(ch) // 没有数据要发送了
}()
for value := range ch { // 如果没有数据要发送了,循环终止
println(value)
}
println("done")
time.Sleep(time.Second * 11)
}Select
经常会有Promise.race这样的需求,最典型的就是超时,此时直接写是不可能满足的。
package main
import (
"fmt"
"time"
)
func main() {
c1 := make(chan string)
c2 := make(chan string)
go func() {
time.Sleep(time.Second * 1)
c1 <- "one"
}()
go func() {
time.Sleep(time.Second * 2)
c2 <- "two"
}()
start := time.Now().Unix()
msg2 := <-c2
fmt.Println(msg2, time.Now().Unix()-start) // 2
msg1 := <-c1
fmt.Println(msg1, time.Now().Unix()-start) // 2
}
/*
c2在c1前面,阻塞了c1,这时候需要多路复用了,看下一张slide
*/
引出select,select使同一个协程中的管道接收“平行化”,相比上一张slide中的chan互相阻塞,select会先阻塞,然后选择处理返回更快的一个管道,丢弃其他的
package main
import (
"fmt"
"time"
)
func main() {
c1 := make(chan string)
c2 := make(chan string)
go func() {
time.Sleep(time.Second * 1)
c1 <- "one"
}()
go func() {
time.Sleep(time.Second * 2)
c2 <- "two"
}()
for i := 0; i < 2; i++ {
select {
case msg2 := <-c2:
fmt.Println("received", msg2)
case msg1 := <-c1:
fmt.Println("received", msg1)
}
}
}
/*
实际场景中无法估计管道写入顺序。
select让两个chan无序化,先到先处理。
*/
再在两个并发场景中体会一下select的作用
func fetchConcurrent() {
aChan := make(chan fetchResult, 0)
go func(c chan fetchResult) {
c <- fetchA()
}(aChan)
bChan := make(chan fetchResult, 0)
go func(c chan fetchResult) {
c <- fetchB()
}(bChan)
cChan := make(chan fetchResult, 0)
go func(c chan fetchResult) {
c <- fetchC()
}(cChan)
a := <-aChan
b := <-bChan
c := <-cChan
fmt.Println(a)
fmt.Println(b)
fmt.Println(c)
}
/*都返回才返回,有依赖关系的情景使用*/func fetchConcurrent() {
aChan := make(chan fetchResult, 0)
bChan := make(chan fetchResult, 0)
cChan := make(chan fetchResult, 0)
go func(c chan fetchResult) {
c <- fetchA()
}(aChan)
go func(c chan fetchResult) {
c <- fetchB()
}(bChan)
go func(c chan fetchResult) {
c <- fetchC()
}(cChan)
for i := 0; i < 3; i++ {
select {
case a := <-aChan:
fmt.Println(a)
case b := <-bChan:
fmt.Println(b)
case c := <-cChan:
fmt.Println(c)
}
}
}
/*实时返回,在请求之前无依赖关系的情景使用*/select的随机性
package main
import "fmt"
func main() {
ch1 := make(chan int, 1)
ch2 := make(chan int, 1)
ch1 <- 1
ch2 <- 2
select {
case v := <-ch1:
fmt.Println(v)
case v := <-ch2:
fmt.Println(v)
}
}
/*
对于两个同时可以接收数据的chan来说,select会随机选择一个chan来读数据。
多次运行,打印1或者2
*/Goroutine leak/
BusyLoop
协程泄漏是使用协程最常见的问题。协程由于各种原因返回不及时,长此以往,导致内存泄漏。
协程泄漏的原因可能有:a)sender速度持续大于receiver。b)sender速度持续小于receiver。c)存在死循环。d)nil channels。
func goroutineLeak () {
ch := make(chan int)
go func () {
ch <- 0
ch <- 1
} ()
go func () {
<- ch
<- ch
<- ch
// 一个最简单的协程泄露。
// receiver速度大于sender速度。
// 这个协程没用了并得不到释放,就是协程泄露。
} ()
}
控制协程泄漏有两种思路,控制协程数量,确保协程可以及时销毁。手段大概有:a)使用buffered chan。b)控制worker数量。c)拒绝服务。d)使用context控制一组协程。
使用worker模式,而不是来一个请求启动一个协程。这里用Effective Go里面的例子举例。
package main
import (
"fmt"
"time"
)
type Request struct {
args []int // 请求实体的参数
f func([]int) int // 处理函数
resultChan chan int // 传输管道
}
func sum(a []int) (s int) {
for _, v := range a {
s += v
}
return
}
func handle(ch <-chan *Request) {
workers := 4 // worker 数量
for i := 0; i < workers; i++ {
// 启动workers
go func(wid int) {
for req := range ch {
fmt.Printf("worker %d working...\n", wid)
req.resultChan <- req.f(req.args)
}
}(i)
}
}
func main() {
// 使用4个worker并发处理100个请求
clientRequest := make(chan *Request, 10)
go handle(clientRequest)
for i := 0; i < 100; i++ {
// 模拟100个IncomingMessage
// 在go的http架构中,通常会为每个请求开一个协程
go func(i int) {
req := &Request{[]int{1, 2, i}, sum, make(chan int)}
clientRequest <- req
val := <-req.resultChan
fmt.Printf("result is %d\n", val)
}(i)
}
time.Sleep(1 * time.Second)
}
context可以对一组协程做统一控制。通过context包可以更好的控制协程的退出,减少协程泄漏的发生概率。除了提供基本的cancel模式,还提供timeout模式
gen2 := func(ctx context.Context) {
go func() {
for {
select {
case <-ctx.Done():
println(ctx.Value("k").(string))
println("2 done")
return // free goroutine
}
}
}()
}
gen1 := func(ctx context.Context) {
gen2(ctx)
go func() {
for {
select {
case <-ctx.Done():
println(ctx.Value("k").(string))
println("1 done")
return
}
}
}()
}
root := context.Background()
ctxWithCancel, cancel := context.WithCancel(root)
ctxCancel := context.WithValue(ctxWithCancel, "k", "v")
gen1(ctxCancel)
cancel() // 取消context,子协程会释放context.Done(),并且结束协程。
// cancel 的本质是把管道close掉,所有因该chan产生的阻塞会被放开。
time.Sleep(time.Second * 1)context不是语言级别的特性,它利用了close chan会释放接收方的阻塞这个特性。
package main
import "time"
func main() {
ch := make(chan int)
go func () {
<- ch
println("dead")
} ()
go func () {
<- ch
println("dead")
} ()
close(ch)
time.Sleep(3 * time.Second)
}
在使用closed chan释放的时候,还需要注意这篇文章中提到的busyLoop。这篇文章也强调了nil chan和closed chan的区别,以及nil chan的意义。下面是busyLoop的演示,读者可自行运行体会。
func busyLoop(ch1 <-chan int, ch2 <-chan int) <-chan int {
ch3 := make(chan int)
go func() {
defer close(ch3)
droped1, droped2 := false, false
for !droped1 || !droped2 {
select {
case val, ok := <-ch1:
if !ok {
fmt.Println("drop 1")
droped1 = true
continue
}
ch3 <- val
case val, ok := <-ch2:
if !ok {
fmt.Println("drop 2")
droped2 = true
continue
}
ch3 <- val
}
}
}()
return ch3
}
// ch1 是一个快队列(先close),ch2 是一个慢队列(后close)
// drop1会反复被打印,select的随机性不会判断ch1已经先close了,只会觉得ch1可读。
// 所以在慢队列ch2不可读的情况下频繁读ch1,那怎么样让它不可读呢?func busyLoop(ch1 <-chan int, ch2 <-chan int) <-chan int {
ch3 := make(chan int)
go func() {
defer close(ch3)
for !(ch1 == nil) || !(ch2 == nil) {
select {
case val, ok := <-ch1:
if !ok {
fmt.Println("drop 1")
ch1 = nil
continue
}
ch3 <- val
case val, ok := <-ch2:
if !ok {
fmt.Println("drop 2")
ch2 = nil
continue
}
ch3 <- val
}
}
}()
return ch3
}
// 此时用到nil chan,nil chan是真的读不出来东西了。
// busyLoop问题解决 :)Data Race
go中存在真正的并行,编码中时刻注意处理data race问题。拿一个测试用例举例
func TestSafty(t *testing.T) {
s1 := NewStack(0)
s2 := NewSafeStack(0)
w := sync.WaitGroup{}
loop := 2
itemCount := 200
for index := 0; index < 2; index++ {
w.Add(1)
go func() {
i := 0
for {
if i == itemCount {
break
}
s1.Push(1)
s2.Push(1)
i++
}
w.Done()
}()
}
w.Wait()
assert := tools.GenerateAssertor(t)
fmt.Printf("s1 size: %d", s1.Size())
fmt.Printf("s2 size: %d", s2.Size())
assert(s1.Size() < loop*itemCount, "should not be safe")
assert(s2.Size() == loop*itemCount, "should be safe")
}
/*
unsafe的stack在并发写入的时候,会有写入冲突的情况,所以最终写入成功数是不符合预期
*/safeStack怎么写?继承然后加个锁就行啦
package stack
import "sync"
type SafeStack struct {
Stack
lock sync.RWMutex
}
func NewSafeStack(size int) *SafeStack {
ss := SafeStack{}
s := NewStack(size)
ss.Stack = *s
ss.lock = sync.RWMutex{}
return &ss
}
func (s *SafeStack) Push(item interface{}) error {
s.lock.Lock()
err := s.Stack.Push(item)
s.lock.Unlock()
return err
}
func (s *SafeStack) Pop() (interface{}, error) {
s.lock.Lock()
val, err := s.Stack.Pop()
s.lock.Unlock()
return val, err
}go run -race main.go
并发程度
并发数越大越好吗?
func fib (n int, codeep int) int {
codeep --
if(n == 1) {
return 1
} else if(n == 2) {
return 2
} else {
if(codeep <= 0) {
return fib(n - 1, codeep) + fib(n - 2, codeep)
} else {
chh := make(chan int, 2)
go func() {
chh <- fib(n - 1, codeep)
} ()
go func () {
chh <- fib(n - 2, codeep)
} ()
i := <- chh
j := <- chh
return i + j
}
}
}func main () {
t1 := time.Now()
r := fib(30, 11)
println(r)
print(time.Since(t1).Nanoseconds())
//codeep=1 耗时=4423918 不并发
//codeep=2 耗时=2915501 2
//codeep=3 耗时=2235940 4
//codeep=4 耗时=1638563 8 // 我猜测因为并发数等于核心数所以这个最快?
//codeep=5 耗时=1827314
// .....
//codeep=10 耗时=2992178 // 因为调度更多反而更耗时,并发得不偿失
//codeep=11 耗时=3293409
}又做了一些实验,并不能支持我上面关于核心数的猜测...还需更深入的了解go的机制才能得出结论。但是有一件事是肯可以确定,如果某包中存在锁,协程之间竞争锁,程序会变慢
func do(factor int) int64 {
tStart := time.Now()
wg := sync.WaitGroup{}
opCount := int64(math.Pow(2, 22))
goruntineCount := int64(math.Pow(2, float64(factor)))
opPerGoruntine := opCount / goruntineCount
verbose := 0
lock := sync.Mutex{}
for i := int64(0); i < goruntineCount; i++ {
wg.Add(1)
go func() {
for j := int64(0); j < opPerGoruntine; j++ {
lock.Lock()
verbose++
lock.Unlock()
}
wg.Done()
}()
}
wg.Wait()
return time.Since(tStart).Nanoseconds()
}func testGroup(times int) func(int) {
return func(factor int) {
sum := int64(0)
for i := 0; i < times; i++ {
sum += do(factor)
}
println(sum / int64(times))
}
}
func main() {
test := testGroup(10)
test(0) // 59561397
test(1) // 70452791
test(2) // 118204986
test(3) // 140871114
test(4) // 172652510
test(5) // 224705678
}理性并发
Q&A
goroutine
By shaomingquan
goroutine
- 582