go的并发控制手段有
channel,waitgroup,context,
sync包中的rwlock,lock,pool,Once,cond,map等在另一篇文章介绍。

这篇文章将介绍这些并发控制技术的使用方式以及实现原理

目录

一:channel

二:waitgroup

底层结构

方法

方法的实现原理

waitgroup的使用

注意事项

三:context

Context 接口

Context 接口的实现结构

对外函数

使用方式

使用规范

使用示例

一:channel

对于channel的使用方式和实现原理,另一篇文章已有介绍,这里就不再多赘述。   

二:waitgroup

sync.WaitGroup用来解决携程间的同步阻塞等待的问题。
可以用于一个goroutine阻塞等待n个goroutine
也可以用于n个goroutine阻塞等待1个。
或者n个阻塞等待m个。

底层结构

底层结构存储有12个字节
4字节的计数器,4字节的等待者个数,4字节是信号量

方法

waitgroup结构有3个方法:Add,Wait,Done,其中Done调用的是Add(-1)

方法的实现原理

Add方法中,根据传入参数去计算计数器,如果计数器为0,则根据等待者个数假设为n,
则调用n次释放信号量,去唤醒等待的goroutine

Wait方法中,先判断计数器为0则不等待立即返回,否则累加等待者个数后使用信号量挂起当前的goroutine、
使用方法也比较简单。

waitgroup的使用

使用方法也比较简单。
工作的携程每次开启则Add(1),执行结束则Done
阻塞等待的携程只需要调用Wait()即可。

注意事项

1.waitgroup变量在传递的过程中因为是值类型的,所以传参过程需要传递指针,
否则传参过去是一份拷贝,工作携程调用Done不会唤醒等待的携程。造成永久阻塞。
2.add和wait一定要在同一个携程里面操作。
不然如果add再另一个携程里面,wait的携程可能还没等待另一个携程add就已经退出。  

三:context

Golang context是Golang的并发控制技术, 它与WaitGroup的不同点是context更容易控制派生的goroutine。
context 主要用来往子孙 goroutine 传递上下文信息,包括:取消信号、超时时间、截止时间、k-v 等。
通过context,我们可以方便地对同一个请求所产生地goroutine进行约束管理,可以设定超时、deadline,甚至是取消这个请求相关的所有goroutine。
上下文则几乎已经成为传递与请求同生存周期变量的标准方法。在网络编程下,当接收到一个网络请求Request,处理Request时,我们可能需要开启不同的Goroutine来获取数据与逻辑处理,即一个请求Request,会在多个Goroutine中处理。而这些Goroutine可能需要共享Request的一些信息;同时当Request被取消或者超时的时候,所有从这个Request创建的所有Goroutine也应该被结束。

Context 接口

type Context interface {
    Deadline() (deadline time.Time, ok bool)
    Done() <-chan struct{}
    Err() error
    Value(key interface{}) interface{}
}

Deadline使用场景,子goroutinue会返回一个超时时间,Goroutine获得了超时时间后,例如可以对某些io操作设定超时时间。
Done方法返回一个信道(channel),即它是一个表示Context是否已关闭的信号
当Done信道关闭后,Err方法表明Context被撤的原因。
Value可以让Goroutine共享一些数据,当然获得数据是协程安全的。但使用这些数据的时候要注意同步,比如返回了一个map,而这个map的读写则要加锁。

Context 接口的实现结构

emptycontext 实现,通常用作父的context
 cancelCtx 实现,父goroutine主动调用cancel函数取消子goroutinue
 timerCtx ,父goroutine主动取消,或者定时或指定时间调用cancel函数取消子goroutinue
 valueCtx ,父goroutinue将值传给子goroutinue

对外函数

//go 的context对外提供6个函数
func Background() Context
func TODO() Context
// 返回 emptyCtx

func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
// 返回cancelCtx

func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc)
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
WithTimeout函数内也是调用WithDeadline
// 返回timerCtx

func WithValue(parent Context, key, val interface{}) Context
// 返回valueCtx

使用方式

Goroutine,他们的创建和调用关系总是像层层调用进行的,就像人的辈分一样,而更靠顶部的Goroutine应有办法主动关闭其下属的Goroutine的执行(不然程序可能就失控了)。为了实现这种关系,Context结构也应该像一棵树,叶子节点须总是由根节点衍生出来的。

要创建Context树,第一步就是要得到根节点,context.Background函数的返回值就是根节点Background()它常常作为处理Request的顶层context存在。
有了根节点,又该怎么创建其它的子节点,孙节点呢?context包为我们提供了多个函数来创建他们:

使用规范

1.不要把context存储在结构体中,而是要显式地进行传递
2.把context作为第一个参数,并且一般都把变量命名为ctx
3.就算是程序允许,也不要传入一个nil的context,如果不知道是否要用context的话,用context.TODO()来替代
4.context.WithValue()只用来传递请求范围的值,不要用它来传递可选参数
5.就算是被多个不同的goroutine使用,context也是安全的

使用示例

valueCtx
这里子携程中一直使用ctx中的值处理逻辑
或者等待取消结束运行,这里不会结束,因为valueCtx没有取消函数,
一般都和别的组合使用。

package main

import (
	"fmt"
	"time"
	"context"
)

func HandelRequest(ctx context.Context) {
	for {
		select {	
			case <-ctx.Done():
			fmt.Println("HandelRequest Done.")
			return
		default:
			fmt.Println("HandelRequest running, parameter: ", ctx.Value("parameter"))
			time.Sleep(2 * time.Second)
		}
	}
}

func main() {
	ctx := context.WithValue(context.Background(), "parameter", "1")
	go HandelRequest(ctx)

	time.Sleep(10 * time.Second)
}