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