《Go语言学习笔记》 8 并发

76 阅读3分钟

8.1 并发的含义

  • 并发:逻辑上具备同时处理多个任务的能力
  • 并行:物理在同一时刻执行多个并发任务

我们通常会说程序是并发设计的,也就是说,它允许多个任务同时执行,但实际上并不一定真的在同一时刻发生,在单核处理器上,它们能以间隔方式切换执行,而并行则依赖多核处理器等物理设备,让多个任务真正在同一时刻执行,它代表了当前程序运行状态。简单点说,并行是并发设计的理想模式。

多线程或多进程是并行的基本条件,但单线程也可用协程做到并发。尽管协程在单个线程上通过主动切换来实现多任务并发,但它也有自己的优势。除了将因阻塞而浪费的时间找回以外,还免去了线程切换的开销,有着不错的执行效率。协程上运行的多个任务本质上依旧是串行的,加上可自主调度,所以并不需要做同步处理。

很难说哪种方式更好一些,它们有各自适用的场景。通常情况下,用多进程来实现分布式和负载平衡,减轻单进程垃圾回收压力;用多线程抢夺更多的处理器资源;用协程来提高处理器时间片利用率。
简单讲goroutine归纳为协程并不合适。运行时会创建多个线程来执行并发任务,且任务单元可被调度到其他线程并行执行。这更像是多线程和协程的综合体,能最大限度提升执行效率,发挥多核处理能力。
只需在函数调用前添加gi关键字即可创建并发任务。

	go println("hello")
	go func(s string) {
		println(s)
	}("teest")

关键字go并非执行并发操作,而是创建一个并发单元。新建任务被放置在系统队列中,等待调度器安排合适系统线程去获取执行权。当前流程不会阻塞,不会等待该任务启动,且运行时也不保证并发任务的执行次序。
每个任务单元除保存函数指针、调用参数外,还会分配执行所需的栈内存空间。相比系统默认MB级别的线程栈,gotoutine自定义栈初始仅须2KB,,所以才能创建成千上万的并发任务。自定义栈采取按需分配策略,在需要时进行扩容,最大能到GB规模
与defer一样,goroutinue也会因“延迟执行”而立即计算并复制执行参数。

var c int

func counter() int {
	c++
	return c
}

func main() {
	a := 100
	go func(x, y int) {
		time.Sleep(time.Second)
		println("go:", x, y)
	}(a, counter())			//立即计算并复制参数
	a += 100
	println("main:,", a, counter())
	time.Sleep(time.Second * 3)
}
结果
main:, 200 2
go: 100 1

Wait

进程退出时不会等待并发任务结束,可用通道(channel)阻塞,然后发出退出信号


func main() {
	exit := make(chan struct{})

	go func() {
		time.Sleep(time.Second)
		println("goroutinue done..")
		close(exit)
	}()
	println("main..")
	<-exit
	println("main exit..")
}

除关闭通道外,写入数据也可解除阻塞。channel的更多信息。
如果要等到多个任务结束,推荐使用sync.WaitGroup.通过设定计数器,让每个gorounitune在退出前递减,直至归零时解除阻塞。

func main() {
	var wg sync.WaitGroup
	for i := 0; i < 10; i++ {
		wg.Add(1)
		go func(id int) {
			defer wg.Done()
			time.Sleep(time.Second)
			println("goroutine", id, "done..")
		}(i)
	}
	println("main..")
	wg.Wait()
	println("main exit..")
}
结果
main..
goroutine 9 done..
goroutine 7 done..
goroutine 1 done..
goroutine 8 done..
goroutine 5 done..
goroutine 3 done..
goroutine 6 done..
goroutine 0 done..
goroutine 2 done..
goroutine 4 done..
main exit..

尽管WaitGroup.Add 实现了原子操作,但建议在goroutine外累加计数器,以免Add尚未执行,Wait已经退出。

func main() {
	var wg sync.WaitGroup
	go func() {
		wg.Add(1)
		println("hi")
	}()
	wg.Wait()
	println("exit..")
}


可在多处使用Wait阻塞,它们都能接收到通知。

func main() {
	var wg sync.WaitGroup
	wg.Add(1)
	go func() {
		wg.Wait()
		println("wait exit")
	}()
	go func() {
		time.Sleep(time.Second)
		println("done.")
		wg.Done()
	}()
	wg.Wait()
	println("main exit..")
}
结果
done.
wait exit
main exit..

GOMAXPROCS

运行时可能会创建很多线程,但任何时候仅有限的几个线程参与并发任务执行。该数量默认与处理器核数相等,可用runtime.GOMAXPROCS(或环境变量修改)。
如果参数小于1,GOMAXPROCS仅返回当前设置值,不做任何修改

import (
	"math"
	"runtime"
	"sync"
)

func count() {
	x := 0
	for i := 0; i < math.MaxUint32; i++ {
		x += 1
	}
	println(x)
}
func test(n int) {
	for i := 0; i < n; i++ {
		count()
	}
}
func test2(n int) {
	var wg sync.WaitGroup
	wg.Add(n)
	for i := 0; i < n; i++ {
		go func() {
			count()
			wg.Done()
		}()
	}
	wg.Wait()
}
func main() {
	n := runtime.GOMAXPROCS(0)
	//test(n)
	test2(n)
}

Local Storage

与线程不同,goroutine任务无法设置优先级,无法获取编号,没有局部存储(TLS),甚至连返回值都会被抛弃。但除优先级外,其他都很容易实现。

func main() {
	var wg sync.WaitGroup
	var gs [5]struct {
		id     int
		result int
	}
	for i := 0; i < len(gs); i++ {
		wg.Add(1)
		go func(id int) {
			defer wg.Done()
			gs[id].id = id
			gs[id].result = (id + 1) * 100

		}(i)
	}
	wg.Wait()
	fmt.Printf("%#v\n", gs)
}
结果
[{id:0 result:100} {id:1 result:200} {id:2 result:300} {id:3 result:400} {id:4 result:500}]

Gosched

暂停,释放掉线程去执行其他任务。当前任务被放回队列,等待下次调度时回复执行。

func main() {
	runtime.GOMAXPROCS(1)
	exit := make(chan struct{})
	go func() {
		defer close(exit)
		go func() {
			println("b")
		}()
		for i := 0; i < 4; i++ {
			println("a", i)
			if i == 1 {
				runtime.Gosched()
			}
		}
	}()
	<-exit
}
结果
a 0
a 1
a 2
a 3a 0
a 1
b
a 2
a 3
结果不稳定

该函数很少被使用,因为运行时会主动向长时间运行的任务发出抢调度。只是当前版本实现的算法稍显粗糙,不能保证调度总能成功,所以主动切换还有适用场合。

Goexit

Goexit 立即终止当前任务,运行时确保所有已注册延迟调用被执行。该函数不会影响其他并发任务,不会引发panic,自然也就无法捕获

func main() {
	exit := make(chan struct{})
	go func() {
		defer close(exit)
		defer println("a")
		func() {
			defer func() {
				println("b", recover() == nil)
			}()
			func() {
				println("c")
				runtime.Goexit()
				println("c done")
			}()
			println("b done")

		}()
		println("a done")
	}()
	<-exit
	println("main exit")
}

结果
c
b true
a
main exit

如果在main.main里调用Goexit,它会等待其他任务结束,然后让进程直接崩溃

func main() {
	for i := 0; i < 2; i++ {
		go func(x int) {
			for n := 0; n < 2; n++ {
				fmt.Printf("%c:%d\n", 'a'+x, n)
				time.Sleep(time.Millisecond)
			}
		}(i)
	}
	runtime.Goexit()
	println("main exit")
}
结果
b:0
a:0
b:1
a:1
fatal error: no goroutines (main called runtime.Goexit) - deadlock!

无论身处哪一层,Goexit都能立即终止整个调用堆栈,这与return仅退出当前函数不同。标准函数库os.Exit 可终止进程,但不会执行延迟调用。

8.2 通道

相比Erlang,Go并未严格的并发安全。允许全局变量、指针、引用类型这些非安全内存共享操作,就需要开发人员自行维护数据一致性和完整性。Go鼓励使用CSP通道,以通信代替内存共享,实现并发安全。
通过消息来避免竞态的模型除了CSP,还有Actor。但两者有较大区别。
作为CSP核心,通道(channel)是显示的,要求操作双方必须知道数据类型和具体通道,并不关心另一端操作者身份和数量。可如果另一端未准备妥当,或消息未能及时处理,会阻塞当前端。
相比起来,Actor是透明额。它不在乎数据类型及通道,只要知道接收者信箱即可。默认就是异步方式,发送方对消息是否被接收和处理并不关心。
从底层实现上来说,通道只是一个队列。同步模式下,发送和接收双方配对,然后直接复制数据给对方。如果配对失败,则置入等待队列,直到另一方出现才被唤醒。异步模式抢夺的则是数据缓冲槽。发送方要求有空槽可写入,而接收方则要求有缓冲数据可读。需求不符时,同样加入等待队列,直到有另一方写入数据或腾出空槽后被唤醒。
除传递消息外,通道还被用作事件通知。

func main() {
	done := make(chan struct{})
	c := make(chan string)
	go func() {
		s := <-c
		println(s)
		close(done)
	}()
	c <- "hi"
	<-done
}

同步模式必须有配对操作的goroutine出现,否则会一直堵塞。而异步模式才缓冲区未满或数据未读完前,不会阻塞。

func main() {
	c := make(chan int,3)	// 创建3个缓冲槽的异步通道
	c <- 1
	c <- 2
	println(<-c)
	println(<-c)
}


多数时候,异步通道有助于提升性能,减少排队阻塞。
缓冲区大小仅是内部属性,不属于类型的组成部分。另外通道本身就是指针,可用相等操作符判断是否为同一对象或nil

func main() {
	var a, b chan int = make(chan int, 3), make(chan int, 2)
	var c chan bool
	println(a == b)
	println(c == nil)
	fmt.Printf("%p,%d\n", a, unsafe.Sizeof(a))
}
result:
false
true
0xc00007e080,8

虽然可传递指针来避免数据复制,但须额外注意数据并发安全。
内置函数cap和len返回缓冲区大小和当前已缓冲数量;而对于同步通道则都返回0,据此可判断通道是异步还是同步。

func main() {
	a, b := make(chan int), make(chan int, 3)
	b <- 1
	b <- 2
	println("a:", len(a), cap(a))	//a: 0 0
	println("b:", len(b), cap(b))	//b: 2 3
}


收发

除使用简单的发送和接收操作符外,还可使用ok-idom或range模式处理数据

func main() {
	done := make(chan struct{})
	c := make(chan int)

	go func() {
		defer close(done)

		for {
			x, ok := <-c
			if !ok {
				return
			}
			println(x)
		}
	}()
	for i := 0; i < 100; i++ {
		c <- i
	}
	close(c)
	<-done
}


对于循环接收数据,range模式更简洁一些;

func main() {
	done := make(chan struct{})
	c := make(chan int)

	go func() {
		defer close(done)

		for x := range c {
			println(x)
		}
	}()
	for i := 0; i < 100; i++ {
		c <- i
	}
	close(c)
	<-done
}

及时用close函数关闭通道引发结束通知,否则可能会导致死锁。
通知可以是群体性的。也未必就是通知结束,可以是任何需要表达的事件。

func main() {
	var wg sync.WaitGroup
	ready := make(chan struct{})
	wg.Add(3)
	for i := 0; i < 3; i++ {
		go func(id int) {
			defer wg.Done()
			println(id, ":ready.")
			<-ready
			println(id, "running...")
		}(i)
	}
	time.Sleep(time.Second)
	println("Ready?Go!")
	close(ready)
	wg.Wait()
}
result:
0 :ready.
2 :ready.
1 :ready.
Ready?Go!
1 running...
0 running...
2 running...

一次性事件用close效率更好,可传递不同数据标志实现。还可使用sync.Cond实现单播或广播事件。
对于closed或nil通道,发送和接收操作都有相应规则:

  • 向已关闭通道发送数据,引发panic。
  • 从已关闭接收数据,返回已缓冲数据或零值。
  • 无论收发,nil通道都会阻塞。
func main() {
	c := make(chan int, 3)
	c <- 10
	c <- 20
	close(c)
	// for i := 0; i < cap(c)+1; i++ {
	// 	x, ok := <-c
	// 	println(i, ":", ok, x)
	// }
	for x := range c {
		println(x)
	}
}

操作得对异步通道进行,同步通道关闭后就不能读取数据了

单向

通道默认是单向的,并不区分发送和接收端。但某些时候,我们可可以限制收发的方向来获得更严谨的逻辑操作。
尽管可使用make创建单向通道,但那没有任何意义。通常使用类型转换来获取单向通道,并分别赋值给操作双方。

func main() {
	var wg sync.WaitGroup
	wg.Add(2)

	c := make(chan int)
	var send chan<- int = c
	var recv <-chan int = c
	go func() {
		defer wg.Done()
		for x := range recv {
			println(x)
		}
	}()
	go func() {
		defer wg.Done()
		defer close(c)
		for i := 0; i < 100; i++ {
			send <- i
		}
		time.Sleep(time.Second)
	}()
	wg.Wait()
}


不能在单向通道上做逆向操作

func main() {

	c := make(chan int)
	var send chan<- int = c
	var recv <-chan int = c

	<-send	//<-send (receive from send-only type chan<- int)
	recv <- 1	//recv <- 1 (send to receive-only type <-chan int)
}


同样,close不能用于接收端

func main() {

	c := make(chan int)
	var send chan<- int = c
	var recv <-chan int = c

	close(recv)	// close(recv) (cannot close receive-only channel)
}



无法将单向通道重新转换回去

func main() {
	var a, b chan int
	a = make(chan int, 2)
	var recv <-chan int = a
	var send chan<- int = a

	b = (chan int)(recv) //cannot convert recv (type <-chan int) to type chan int
	b = (chan int)(send) // cannot convert send (type chan<- int) to type chan int
}


选择

如果要同时处理多个通道,可选用select语。它会随机选择一个可用通道做收发操作。

func main() {
	var wg sync.WaitGroup
	wg.Add(2)
	a, b := make(chan int), make(chan int)
	go func() {
		defer wg.Done()
		time.Sleep(time.Second)
		for {
			var (
				name string
				x    int
				ok   bool
			)
			select {		// 随机接收
			case x, ok = <-a:
				name = "a"
			case x, ok = <-b:
				name = "b"
			}
			if !ok {	// 没有数据时关闭
				return
			}
			println(name, x)
		}
	}()
	go func() {
		defer wg.Done()
		defer close(a)
		defer close(b)
		for i := 0; i < 10; i++ {
			select {		// 随机发送
			case a <- i:
			case b <- i * 10:
			}
		}
	}()
	wg.Wait()
}
结果:
a 0
b 10
a 2
b 30
b 40
a 5
b 60
a 7
b 80
b 90

如要等到全部通道消息处理结束(closed),可将已完成通道设置为nil。这样它就会被阻塞,不再被select选中。

func main() {
	var wg sync.WaitGroup
	wg.Add(3)
	a, b := make(chan int), make(chan int)
	go func() {
		defer wg.Done()
		time.Sleep(time.Second)
		for {
			select {
			case x, ok := <-a:
				if !ok {
					a = nil
					break
				}
				println("a",x)
			case x, ok := <-b:
				if !ok {
					b = nil
					break
				}
				println("b",x)
			}
			if a==nil && b == nil{
				return
			}
			
		}
	}()
	go func() {
		defer wg.Done()
		defer close(a)
		for i := 0; i < 10; i++ {
			a <- i
		}
	}()
		go func() {
		defer wg.Done()
		defer close(b)
		for i := 0; i < 3; i++ {
			b <- i * 10
		}
	}()
	wg.Wait()
结果
b 0
b 10
a 0
b 20
a 1
a 2
a 3
a 4
a 5
a 6
a 7
a 8
a 9


即便是同一通道,也会随机选择case执行。

func main() {
	var wg sync.WaitGroup
	wg.Add(2)
	a := make(chan int)
	go func() {
		defer wg.Done()
		for {
			var x int
			var ok bool
			select {
			case x, ok = <-a:
				println("a1", x*10)

			case x, ok = <-a:
				println("a2", x)
			}
			if !ok {
				return
			}

		}
	}()
	go func() {
		defer wg.Done()
		defer close(a)
		for i := 0; i < 10; i++ {
			select {
			case a <- i:
			case a <- i * 10:
			}
		}
	}()

	wg.Wait()
}
结果
a2 0
a1 10
a2 20
a2 30
a1 400
a1 500
a2 60
a1 700
a2 8
a2 90
a1 0

当所有通道都不可用时,select会执行default语句。如此可避开select阻塞,但须注意处理外层循环,以免陷入空耗。

func main() {
	done := make(chan struct{})
	c := make(chan int)

	go func() {
		defer close(done)
		for {
			select {
			case x, ok := <-c:
				if !ok {	// 当通道可选前不会执行
					return
				}
				println("data:", x)
			default:
				println("default")
			}
			fmt.Println(time.Now())
			time.Sleep(time.Second)
		}
	}()
	time.Sleep(time.Second * 3)
	c <- 100
	close(c)
	<-done
}
result
default
2019-03-08 11:08:11.6008349 +0800 CST m=+0.002444801
default
2019-03-08 11:08:12.6178948 +0800 CST m=+1.019504701
default
2019-03-08 11:08:13.6185616 +0800 CST m=+2.020171501
data: 100
2019-03-08 11:08:14.6190543 +0800 CST m=+3.020664201

也可用default处理一些默认逻辑

func main() {
	done := make(chan struct{})

	data := []chan int{
		make(chan int, 3),
	}
	go func() {
		defer close(done)
		for i := 0; i < 10; i++ {
			select {
			case data[len(data)-1] <- i:
			default:
				data = append(data, make(chan int, 3))	// 通道满时新增通道
			}
		}
	}()
	<-done	
	for i := 0; i < len(data); i++ {	// 显示数据
		c := data[i]
		close(c)
		for x := range c {	
			println(x)
		}
	}

}

模式

通常使用工厂方法将goroutine通道绑定。

type receiver struct {
	sync.WaitGroup
	data chan int
}

func newReceiver() *receiver {
	r := &receiver{
		data: make(chan int),
	}
	r.Add(1)
	go func() {
		defer r.Done()
		for x := range r.data {
			println("recv:", x)
		}
	}()
	return r
}
func main() {
	r := newReceiver()
	r.data <- 1
	r.data <- 2
	close(r.data)
	r.Wait()
}
结果
recv: 1
recv: 2

鉴于通道本身就是一个并发安全的队列,可用作 ID generator 、Pool等用途。

type pool chan []byte

func newPool(cap int) pool {
	return make(chan []byte, cap)
}
func (p pool) get() []byte {
	var v []byte
	select {
	case v = <-p:
	default:
		v = make([]byte, 10)
	}
	return v
}

func (p pool) put(b []byte) error {
	select {
	case p <- b:
		return nil
	default:
		return errors.New("faild to putback")
	}
}


用通道实现信号量

func main() {
	runtime.GOMAXPROCS(4)
	var wg sync.WaitGroup
	sem := make(chan struct{}, 2)
	for i := 0; i < 5; i++ {
		wg.Add(1)
		go func(id int) {
			defer wg.Done()
			sem <- struct{}{}	// 满了之后阻塞,等到前面处理完后后面才能接着处理
			defer func() {<-sem}()
			time.Sleep(time.Second * 2)
			fmt.Println(id, time.Now())
		}(i)
	}
	wg.Wait()
}

标准库time提供了timeout和tick channel 实现

func main() {
	go func() {
		for {
			select {
			case <-time.After(time.Second * 5):	// 5秒后发送消息
				fmt.Println("time out...")
				os.Exit(0)
			}
		}
	}()
	go func(){
		tick := time.Tick(time.Second)	// 每秒发送一个消息
		for {
			select{
			case <- tick:
				fmt.Println(time.Now())
			}
		}
	}()
	<-(chan struct{})(nil) //阻塞
}
结果
2019-03-08 14:39:54.4357387 +0800 CST m=+1.017798501
2019-03-08 14:39:55.4265249 +0800 CST m=+2.008584701
2019-03-08 14:39:56.4337129 +0800 CST m=+3.015772701
2019-03-08 14:39:57.4317151 +0800 CST m=+4.013775001
time out...

捕获INT、TERM信号,顺便实现一个简易的atexit函数

var exits = &struct {
	sync.RWMutex
	funcs   []func()
	signals chan os.Signal
}{}

func atexit(f func()) {
	exits.Lock()
	defer exits.Unlock()
	exits.funcs = append(exits.funcs, f)
}
func waitExit() {
	if exits.signals == nil {
		exits.signals = make(chan os.Signal)
		signal.Notify(exits.signals, syscall.SIGINT, syscall.SIGTERM)
	}
	exits.RLock()
	for _,f := range exits.funcs{
		defer f()
	}
	exits.RUnlock()
	<-exits.signals
}
func main() {
	atexit(func(){println("exit1...")})
	atexit(func(){println("exit2...")})
	waitExit()
}

性能

将发往通道的数据打包,减少传输次数,可有效提升性能。从实现上来说,通道队列依旧使用锁同步机制,单次获得更多数据,可改善因频繁加锁造成的性能问题。

const (
	max     = 50000000
	block   = 500
	bufsize = 100
)

func test() {
	done := make(chan struct{})
	c := make(chan int, bufsize)
	go func() {
		count := 0
		for x := range c {
			count += x
		}
		close(done)
	}()
	for i := 0; i < max; i++ {
		c <- i
	}
	close(c)
	<-done
}
func testBlock() {
	done := make(chan struct{})
	c := make(chan [block]int, bufsize)
	go func() {
		count := 0
		for a := range c {
			for _, x := range a {
				count += x
			}
		}
		close(done)
	}()
	for i := 0; i < max; i++ {
		var b [block]int
		for n := 0; n < block; n++ {
			b[n] = i + n
			if i+n == max-1 {
				break
			}
				
		}
		c <- b
	}
	close(c)
	<-done
}

虽然单次消耗更多内存,但性能提升非常明显,如将数组改成切片会造成更多内存分配次数。

资源泄露

通道可能会引发goroutine leak,确切的说,是指goroutine处于 发送或接收阻塞状态,但一直未被唤醒。垃圾回收器并不收集此类资源,导致他们会在等待队列里长久休眠,形成资源泄露。


func test() {
	c := make(chan int)
	for i := 0; i < 10; i++ {
		go func() {
			<-c
		}()
	}
}
func main() {
	test()
	for {
		time.Sleep(time.Second)
		runtime.GC()
	}
}


8.3 同步

通道并非用来取代锁的,它们有各自不同的使用场景。通道倾向解决逻辑层次的并发处理架构,而锁则用来保护局部范围内的数据安全。
标准库sync提供了互斥和读写锁,另外有原子操作等,可基本满足日常开发需求。Mutex,RWMutex的使用并不复杂,只有几个地方要注意。
将Mutex作为匿名字段,相关方法必须实现为pointer-receiver,否则会因复制导致锁机制失效。

type data struct {
	sync.Mutex
}

func (d *data) test(s string) {	// 如果这里是(d data)会因为d不同而失效,因为两次都是复制的d
	d.Lock()
	defer d.Unlock()
	for i := 0; i < 5; i++ {
		println(s, i)
		time.Sleep(time.Second)
	}
}
func main() {
	var wg sync.WaitGroup
	wg.Add(2)
	var d data
	go func() {
		defer wg.Done()
		d.test("read")
	}()
	go func() {
		defer wg.Done()
		d.test("write")
	}()
	wg.Wait()
}

应将Mutex锁粒度控制在最小范围内,及早释放。
Mutex不支持递归锁,即便在同一goroutinue下也会导致死锁。

var m sync.Mutex

func main() {
	m.Lock()
	{
		m.Lock()
		m.Unlock()
	}
	m.Unlock()
}
fatal error: all goroutines are asleep - deadlock!

在涉及并发安全类型时,千万注意此类问题。

type cache struct {
	sync.Mutex
	data []int
}

func (c *cache) count() int {
	c.Lock()
	n := len(c.data)
	c.Unlock()
	return n
}
func (c *cache) get() int {
	c.Lock()
	defer c.Unlock()
	var d int
	if n := c.count(); n > 0 {		//count 重复锁定,导致死锁
		d = c.data[0]
		c.data = c.data[1:]
	}
	return d
}
func main() {
	c := cache{
		data: []int{1, 2, 3, 4},
	}
	println(c.get())
}

相关建议:

  • 对性能要求较高时,应避免使用defer Unlock
  • 读写并发时,用RWMutex性能会更好一些。
  • 对单个数据进行读写保护时,可尝试用原子操作
  • 执行严格测试,尽可能打开数据竞争检查