Go专家编程-并发控制 Channel/WaitGroup/Context +反射

本文为《Go专家编程》读书笔记~



并发控制

协程A执行过程中需要创建子协程A1、A2、A3…An,协程A创建完子协程后就等待子协程退 出。针对这种场景,GO提供了三种解决方案:

  1. Channel: 使用channel控制子协程 (实现简单,清晰易懂)
  2. WaitGroup : 使用信号量机制控制子协程 (子协程个数动态可调整)
  3. Context: 使用上下文控制子协程 (对子协程派生出来的孙子协程的控制)

Channel

程序通过创建N个channel来管理N个协程,每个协程都有一个channel用于跟父协程通信,父协程创建完所有协 程中等待所有协程结束。

channels[i] = make(chan int) //切片中放入一个channel

这个例子中,父协程仅仅是等待子协程结束,其实父协程也可以向管道中写入数据通知子协程结束,这时子协程需要定期的探测管道中是否有消息出现。

WaitGroup

WaitGroup,可理解为Wait-Goroutine-Group,即等待一组goroutine结束。比如某个goroutine需要等待其 他几个goroutine全部完成,那么使用WaitGroup可以轻松实现。

var wg sync.WaitGroup
wg.Add(2) //设置计数器,数值即为goroutine的个数
----
wg.Done() //goroutine执行结束后将计数器减1
----
wg.Wait() //主goroutine阻塞等待计数器变为0

信号量

信号量是Unix系统提供的一种保护共享资源的机制,用于防止多个线程同时访问某个资源。
可简单理解为信号量为一个数值:

  • 当信号量>0时,表示资源可用,获取信号量时系统自动将信号量减1;
  • 当信号量==0时,表示资源暂不可用,获取信号量时,当前线程会进入睡眠,当信号量为正时被唤醒;

数据结构

type WaitGroup struct { 
	state1 [3]uint32
}

state1是个长度为3的数组,其中包含了state和一个信号量,而state实际上是两个计数器:

  1. counter: 当前还未执行结束的goroutine计数器
  2. waiter count: 等待goroutine-group结束的goroutine数量,即有多少个等候者
  3. semaphore: 信号量
Add(delta int)

Add()做了两件事,一是把delta值累加到counter中,因为delta可以为负值,也就是说counter有可能变成0或 负值,所以第二件事就是当counter值变为0时,跟据waiter数值释放等量的信号量,把等待的goroutine全部唤 醒,如果counter变为负值,则panic.

Wait()

Wait()方法也做了两件事,一是累加waiter, 二是阻塞等待信号量

这里用到了CAS算法保证有多个goroutine同时执行Wait()时也能正确累加waiter。

Done()

Done()只做一件事,即把counter减1,我们知道Add()可以接受负值,所以Done实际上只是调用了Add(-1)

Context

它与WaitGroup最大的不同点是context对于派生 goroutine有更强的控制力,它可以控制多级的goroutine。

Context接口

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

该方法返回一个deadline和标识是否已设置deadline的bool值,如果没有设置deadline,则ok == false,此 时deadline为一个初始值的time.Time值

Done()

该方法返回一个channel,需要在select-case语句中使用,如”case <-context.Done():”。
当context关闭后,Done()返回一个被关闭的管道,关闭的管理仍然是可读的,据此goroutine可以收到关闭请求;当context还未关闭时,Done()返回nil

Err()

该方法描述context关闭的原因。
关闭原因由context实现控制,不需要用户设置。
比如Deadline context,关闭原因可能是因为deadline,也可能提前被主动关闭,那么关闭原因就会不同: 因deadline关闭:“context deadline exceeded”; 因主动关闭: “context canceled”。
当context关闭后,Err()返回context的关闭原因;当context还未关闭时,Err()返回nil;

Value()

有一种context,它不是用于控制呈树状分布的goroutine,而是用于在树状分布的goroutine间传递信息。 Value()方法就是用于此种类型的context,该方法根据key值查询map中的value。

cancelCtx

children中记录了由此context派生的所有child,此context被cancle时会把其中的所有child都cancle掉。
cancelCtx与deadline和value无关,所以只需要实现Done()和Err()接口外露接口即可。

Done()接口实现

按照Context定义,Done()接口只需要返回一个channel即可。
对于cancelCtx来说只需要返回成员变量done即可。
在这里插入图片描述

Err()接口实现

按照Context定义,Err()只需要返回一个error告知context被关闭的原因。对于cancelCtx来说只需要返回成员 变量err即可。

cancel()接口实现

cancel()内部方法是理解cancelCtx的最关键的方法,其作用是关闭自己和其后代,其后代存储在 cancelCtx.children的map中,其中key值即后代对象,value值并没有意义,这里使用map只是为了方便查询而 已。

WithCancel()方法实现

WithCancel()方法作了三件事:

  1. 初始化一个cancelCtx实例
  2. 将cancelCtx实例添加到其父节点的children中(如果父节点也可以被cancel的话)
  3. 返回cancelCtx实例和cancel()方法

timerCtx

timerCtx在cancelCtx基础上增加了deadline用于标示自动cancel的最终时间,而timer就是一个触发自动 cancel的定时器。
对于接口来说,timerCtx在cancelCtx基础上还需要实现Deadline()和cancel()方法,其中cancel()方法是重写的。

Deadline()接口实现

Deadline()方法仅仅是返回timerCtx.deadline而矣。而timerCtx.deadline是WithDeadline()或 WithTimeout()方法设置的

cancel()接口实现

cancel()方法基本继承cancelCtx,只需要额外把timer关闭。
timerCtx被关闭后,timerCtx.cancelCtx.err将会存储关闭原因

valueCtx

valueCtx只是在Context基础上增加了一个key-value对,用于在各级协程间传递一些数据。 由于valueCtx既不需要cancel,也不需要deadline,那么只需要实现Value()接口即可。

Value()接口实现

当前context查找不到key时,会向父节点查找(可以通过子context查询到父的value值。)

tips

  • Context仅仅是一个接口定义,跟据实现的不同,可以衍生出不同的context类型;
  • cancelCtx实现了Context接口,通过WithCancel()创建cancelCtx实例;
  • timerCtx实现了Context接口,通过WithDeadline()和WithTimeout()创建timerCtx实例;
  • valueCtx实现了Context接口,通过WithValue()创建valueCtx实例;
  • 三种context实例可互为父节点,从而可以组合成不同的应用形式;

反射

反射概念

  1. 反射提供一种让程序检查自身结构的能力 (检查interface变量的底层类型和值的机制)
  2. 反射是困惑的源泉

关于静态类型

Go是静态类型语言,比如”int”、”float32”、”[]byte”等等。每个变量都有一个静态类型,且在编译 时就确定了。

type Myint int 
var i int 
var j Myint

i 和j 类型相同吗?A:i 和j类型是不同的。 二者拥有不同的静态类型,没有类型转换的话是不可以互相赋值 的,尽管二者底层类型是一样的。

特殊的静态类型interface

interface类型是一种特殊的类型,它代表方法集合。 它可以存放任何实现了其方法的值。
接口类型的变量可以存储任何实现该接口的值

特殊的interface类型

最特殊的interface类型为空interface类型,即 interface {} ,interface用来表示一组方法集合,所有实现该方法集合的类型都被认为是实现了该接口。那么空interface类型的方法集合为空,也就是说所有类型都可以认为是实现了该接口。
一个类型实现空interface并不重要,重要的是一个空interface类型变量可以存放所有值,记住是所有值,这才是最最重要的。 这也是有些人认为Go是动态类型的原因,这是个错觉。

interface类型是如何表示的

interface类型的变量可以存放任何实现了该接口的值。还是以上面的 io.Reader 为例进行说 明, io.Reader 是一个接口类型, os.OpenFile() 方法返回一个 File 结构体类型变量,该结构体类型实现 了 io.Reader 的方法,那么 io.Reader 类型变量就可以用来接收该返回值。

var r io.Reader 
tty, err := os.OpenFile("/dev/tty", os.O_RDWR, 0) 
if err != nil { 
return nil, err 
} 
r = tty

Q: r的类型是什么?A: r的类型始终是 io.Reader interface类型,无论其存储什么值。
Q:那 File 类型体现在哪里?A:r保存了一个(value, type)对来表示其所存储值的信息。 value即为r所持有元素的值,type即为所持有元素的底层类型
Q:如何将r转换成另一个类型结构体变量?比如转换成 io.Writer A:使用类型断言,如 w = r.(io.Writer) . 意思 是如果r所持有的元素如果同样实现了io.Writer接口,那么就把值传递给w。

反射三定律

前面之所以讲类型,是为了引出interface,之所以讲interface是想说interface类型有个(value,type)对, 而反射就是检查interface的这个(value, type)对的。具体一点说就是Go提供一组方法提取interface的 value,提供另一组方法提取interface的type.
反射包里有两个接口类型

  1. reflect.Type 提供一组接口处理interface的类型,即(value, type)中的type
  2. reflect.Value 提供一组接口处理interface的值,即(value, type)中的value

反射第一定律:反射可以将interface类型变量转换成反射对象

var x float64 = 3.4 
t := reflect.TypeOf(x) //t is reflext.Type 
fmt.Println("type:", t)  
v := reflect.ValueOf(x) //v is reflext.Value 
fmt.Println("value:", v)

注意:反射是针对interface类型变量的,其中 TypeOf() 和 ValueOf() 接受的参数都是 interface{} 类型的,也即x值是被转成了interface传入的。

反射第二定律:反射可以将反射对象还原成interface对象

对象x转换成反射对象v,v又通过Interface()接口转换成interface对象,interface对象通过.(float64)类型断言获取float64类型的值。

var x float64 = 3.4 
v := reflect.ValueOf(x) //v is reflext.Value 
var y float64 = v.Interface().(float64) 
fmt.Println("value:", y)

反射第三定律:反射对象可修改,value值必须是可设置的

通过反射可以将interface类型变量转换成反射对象,可以使用该反射对象设置其持有的值。
reflect.Value 提供了 Elem() 方法,可以获得指针向指向的 value

var x float64 = 3.4  
v := reflect.ValueOf(&x) 
// 传入reflect.ValueOf()函数的其实是x的值,而非x本身。即通过v修改其值是无法影响x的,也即是无效 的修改,所以golang会报错。
v.Elem().SetFloat(7.1)
// 获得指针向指向的 value 
fmt.Println("x :", v.Elem().Interface())
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
goroutine 52 [select]: github.com/go-sql-driver/mysql.(*mysqlConn).startWatcher.func1() D:/Program Files (x86)/Go/bin/pkg/mod/github.com/go-sql-driver/mysql@v1.7.1/connection.go:614 +0xaf created by github.com/go-sql-driver/mysql.(*mysqlConn).startWatcher D:/Program Files (x86)/Go/bin/pkg/mod/github.com/go-sql-driver/mysql@v1.7.1/connection.go:611 +0x10a goroutine 83 [select]: github.com/go-redis/redis/v8/internal/pool.(*ConnPool).reaper(0x12f262a0, 0xdf8475800) D:/Program Files (x86)/Go/bin/pkg/mod/github.com/go-redis/redis/v8@v8.11.5/internal/pool/pool.go:485 +0xd6 created by github.com/go-redis/redis/v8/internal/pool.NewConnPool D:/Program Files (x86)/Go/bin/pkg/mod/github.com/go-redis/redis/v8@v8.11.5/internal/pool/pool.go:111 +0x242 goroutine 85 [chan receive]: go-study/models.sendProc(0x12e40cf0) D:/go/go-study/models/Message.go:88 +0x48 created by go-study/models.Chat D:/go/go-study/models/Message.go:79 +0x30d goroutine 86 [IO wait]: internal/poll.runtime_pollWait(0x33340b00, 0x72) D:/Program Files (x86)/Go/src/runtime/netpoll.go:305 +0x52 internal/poll.(*pollDesc).wait(0x138f60f4, 0x72, 0x0) D:/Program Files (x86)/Go/src/internal/poll/fd_poll_runtime.go:84 +0x37 internal/poll.execIO(0x138f6014, 0xa365e0) D:/Program Files (x86)/Go/src/internal/poll/fd_windows.go:175 +0xfc internal/poll.(*FD).Read(0x138f6000, {0x12f41000, 0x1000, 0x1000}) D:/Program Files (x86)/Go/src/internal/poll/fd_windows.go:441 +0x13b net.(*netFD).Read(0x138f6000, {0x12f41000, 0x1000, 0x1000}) D:/Program Files (x86)/Go/src/net/fd_posix.go:55 +0x3f net.(*conn).Read(0x12c0aa68, {0x12f41000, 0x1000, 0x1000}) D:/Program Files (x86)/Go/src/net/net.go:183 +0x4f bufio.(*Reader).fill(0x12d1eae0) D:/Program Files (x86)/Go/src/bufio/bufio.go:106 +0xe9 bufio.(*Reader).Peek(0x12d1eae0, 0x2) D:/Program Files (x86)/Go/src/bufio/bufio.go:144 +0x6d github.com/gorilla/websocket.(*Conn).read(0x12f68000, 0x2) D:/Program Files (x86)/Go/bin/pkg/mod/github.com/gorilla/websocket@v1.5.0/conn.go:371 +0x30 github.com/gorilla/websocket.(*Conn).advanceFrame(0x12f68000) D:/Program Files (x86)/Go/bin/pkg/mod/github.com/gorilla/websocket@v1.5.0/conn.go:809 +0xae github.com/gorilla/websocket.(*Conn).NextReader(0x12f68000) D:/Program Files (x86)/Go/bin/pkg/mod/github.com/gorilla/websocket@v1.5.0/conn.go:1009 +0xb5 github.com/gorilla/websocket.(*Conn).ReadMessage(0x12f68000) D:/Program Files (x86)/Go/bin/pkg/mod/github.com/gorilla/websocket@v1.5.0/conn.go:1093 +0x25 go-study/models.recvProc(0x12e40cf0) D:/go/go-study/models/Message.go:100 +0x105 created by go-study/models.Chat D:/go/go-study/models/Message.go:81 +0x352
06-02

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值