入门21_阅读go并发编程

go语言并发编程:https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzUzNTY5MzU2MA==&action=getalbum&album_id=1325302744319737858

1. 学会使用context取消goroutine执行的方法

为什么需要取消功能
img1,img2,img3
上下文只能被取消一次。如果您想在同一操作中传播多个错误,那么使用上下文取消可能不是最佳选择。使用取消上下文的场景是你实际上确实要取消某项操作,而不仅仅是通知下游进程发生了错误。还需要记住的另一件事是,应该将相同的上下文实例传递给你可能要取消的所有函数和goroutine。
用WithTimeout或WithCancel包装一个已经支持取消功能的上下文将会造成多种可能会导致你的上下文被取消的情况,应该避免这种二次包装。
With 系列函数

1
2
3
4
5
6
7
8
9
10
11
12
13
// 传递一个父Context作为参数,返回子Context,以及一个取消函数用来取消Context。
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
// 和WithCancel差不多,它会多传递一个截止时间参数,意味着到了这个时间点,会自动取消Context,
// 当然我们也可以不等到这个时候,可以提前通过取消函数进行取消。
func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc)

// WithTimeout和WithDeadline基本上一样,这个表示是超时自动取消,是多少时间后自动取消Context的意思
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)

//WithValue函数和取消Context无关,它是为了生成一个绑定了一个键值对数据的Context,
// 绑定的数据可以通过Context.Value方法访问到,这是我们实际用经常要用到的技巧,一般我们想要通过上下文来传递数据时,可以通过这个方法,
// 如我们需要tarce追踪系统调用栈的时候。
func WithValue(parent Context, key, val interface{}) Context

golang-context详解:https://zhuanlan.zhihu.com/p/266318909

2. Context是怎么在Go语言中发挥关键作用的

3. Go语言sync包的应用详解

sync.RWMutex
sync.WaitGroup
sync.WaitGroup也是一个经常会用到的同步原语,它的使用场景是在一个goroutine等待一组goroutine执行完成。
sync.Map
sync.Map是一个并发版本的Go语言的map
sync.Pool
sync.Pool是一个并发池,负责安全地保存一组对象。它有两个导出方法:
Get() interface{} 用来从并发池中取出元素。
Put(interface{}) 将一个对象加入并发池。
sync.Once
sync.Once是一个简单而强大的原语,可确保一个函数仅执行一次。
sync.Cond(略)
扩展包中提供的三种原语,也就是 ErrGroup、Semaphore 和 SingleFlight。

ErrGroup

子仓库 x/sync 中的包 errgroup 其实就为我们在一组 Goroutine 中提供了同步、错误传播以及上下文取消的功能,我们可以使用如下所示的方式并行获取网页的数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var g errgroup.Group
var urls = []string{
"http://www.golang.org/",
"http://www.google.com/",
"http://www.somestupidname.com/",
}
for i := range urls {
url := urls[i]
g.Go(func() error {
resp, err := http.Get(url)
if err == nil {
resp.Body.Close()
}
return err
})
}
if err := g.Wait(); err == nil {
fmt.Println("Successfully fetched all URLs.")
}

Go 方法能够创建一个 Goroutine 并在其中执行传入的函数,而 Wait 方法会等待 Go 方法创建的 Goroutine 全部返回后返回第一个非空的错误,如果所有的 Goroutine 都没有返回错误,该函数就会返回 nil。

Semaphore信号量

SingleFlight

singleflight 是 Go 语言扩展包中提供了另一种同步原语,这其实也是作者最喜欢的一种同步扩展机制,它能够在一个服务中抑制对下游的多次重复请求,一个比较常见的使用场景是 — 我们在使用 Redis 对数据库中的一些热门数据进行了缓存并设置了超时时间,缓存超时的一瞬间可能有非常多的并行请求发现了 Redis 中已经不包含任何缓存所以大量的流量会打到数据库上影响服务的延时和质量。
图片?
但是 singleflight 就能有效地解决这个问题,它的主要作用就是对于同一个 Key 最终只会进行一次函数调用,在这个上下文中就是只会进行一次数据库查询,查询的结果会写回 Redis 并同步给所有请求对应 Key 的用户:

Mutex 互斥锁

如果互斥锁处于初始化状态,就会直接通过置位 mutexLocked 加锁;
如果互斥锁处于 mutexLocked 并且在普通模式下工作,就会进入自旋,执行 30 次 PAUSE 指令消耗 CPU 时间等待锁的释放;
如果当前 Goroutine 等待锁的时间超过了 1ms,互斥锁就会被切换到饥饿模式;
互斥锁在正常情况下会通过 runtime_SemacquireMutex 方法将调用 Lock 的 Goroutine 切换至休眠状态,等待持有信号量的 Goroutine 唤醒当前协程;
如果当前 Goroutine 是互斥锁上的最后一个等待的协程或者等待的时间小于 1ms,当前 Goroutine 会将互斥锁切换回正常模式;
如果互斥锁已经被解锁,那么调用 Unlock 会直接抛出异常;
如果互斥锁处于饥饿模式,会直接将锁的所有权交给队列中的下一个等待者,等待者会负责设置 mutexLocked 标志位;
如果互斥锁处于普通模式,并且没有 Goroutine 等待锁的释放或者已经有被唤醒的 Goroutine 获得了锁就会直接返回,在其他情况下回通过 runtime_Semrelease 唤醒对应的 Goroutine;

RWMutex 读写互斥锁

readerSem — 读写锁释放时通知由于获取读锁等待的 Goroutine;
writerSem — 读锁释放时通知由于获取读写锁等待的 Goroutine;
w 互斥锁 — 保证写操作之间的互斥;
readerCount — 统计当前进行读操作的协程数,触发写锁时会将其减少 rwmutexMaxReaders 阻塞后续的读操作;
readerWait — 当前读写锁等待的进行读操作的协程数,在触发 Lock 之后的每次 RUnlock 都会将其减一,当它归零时该 Goroutine 就会获得读写锁;
当读写锁被释放 Unlock 时首先会通知所有的读操作,然后才会释放持有的互斥锁,这样能够保证读操作不会被连续的写操作『饿死』;

WaitGroup 等待一组 Goroutine 结束

Add 不能在和 Wait 方法在 Goroutine 中并发调用,一旦出现就会造成程序崩溃;
WaitGroup 必须在 Wait 方法返回之后才能被重新使用;
Done 只是对 Add 方法的简单封装,我们可以向 Add 方法传入任意负数(需要保证计数器非负)快速将计数器归零以唤醒其他等待的 Goroutine;
可以同时有多个 Goroutine 等待当前 WaitGroup 计数器的归零,这些 Goroutine 也会被『同时』唤醒;

Once 程序运行期间仅执行一次

Do 方法中传入的函数只会被执行一次,哪怕函数中发生了 panic;
两次调用 Do 方法传入不同的函数时只会执行第一次调用的函数;

Cond 发生指定事件时唤醒

Wait 方法在调用之前一定要使用 L.Lock 持有该资源,否则会发生 panic 导致程序崩溃;
Signal 方法唤醒的 Goroutine 都是队列最前面、等待最久的 Goroutine;
Broadcast 虽然是广播通知全部等待的 Goroutine,但是真正被唤醒时也是按照一定顺序的;

ErrGroup 为一组 Goroutine 提供同步、错误传播以及上下文取消的功能

出现错误或者等待结束后都会调用 Context 的 cancel 方法取消上下文;
只有第一个出现的错误才会被返回,剩余的错误都会被直接抛弃;

Semaphore 带权重的信号量

Acquire 和 TryAcquire 方法都可以用于获取资源,前者用于同步获取会等待锁的释放,后者会在无法获取锁时直接返回;
Release 方法会按照 FIFO 的顺序唤醒可以被唤醒的 Goroutine;
如果一个 Goroutine 获取了较多地资源,由于 Release 的释放策略可能会等待比较长的时间;

SingleFlight 用于抑制对下游的重复请求

Do 和 DoChan 一个用于同步阻塞调用传入的函数,一个用于异步调用传入的参数并通过 Channel 接受函数的返回值;
Forget 方法可以通知 singleflight 在持有的映射表中删除某个键,接下来对该键的调用就会直接执行方法而不是等待前面的函数返回;
一旦调用的函数返回了错误,所有在等待的 Goroutine 也都会接收到同样的错误;

4. Go语言计时器的使用详解

5. 面试官让我用channel实现sync包里的同步锁,是不是故意为难我?

6. Golang连接池的几种实现案例

7. Go并发编程里的数据竞争以及解决之道

8. 上周并发题的解题思路以及介绍Go语言调度器

9. 看Kubernetes源码,学习怎么用Go实现调度队列

10. 并发编程-信号量的使用方法和其实现原理

11. 觉得WaitGroup不好用?试试ErrorGroup吧!

12. 并发编程–用SingleFlight合并重复请求

13. 我用休眠做并发控制,搞垮了下游服务

有人肯定要问了,诶~这么看每秒是不超过500个请求啊!
em ~ 从调用方的角度看确实超不过 500,但是却没有考虑下游服务也是分忙时和闲时的(因为是比较基础的服务,不光一个业务方调)。如果下游服务正好在忙时,在1s内没有处理完上一批发过来的 500 个请求,上游就又发过来 500 个请求,用不了多少时间系统就会达到过载状态。
那么有人会问,下游服务难道自己没有做限流或者队列暂存吗?当时老哥也是这么问的,理想情况确实应该有,但那是理想情况,现实情况就是这个内部服务自己没做限流,我相信这在大家的公司也是比较普遍的情况 。
针对这种情况,如果是你来写这个调用方的程序,你认为有哪些的方法既能使用并发增加调用方发起请求的能力又能照顾到下游服务的忙时负载呢
思路
01,资源预定,线程池思路底层服务限制在n(底层提供n取值,预留资源隔离)个线程。此服务只会占用底层n个并发量
02,资源反馈,如果底层不晓得n,或n本身就不稳定。可参考tcp拥塞机制,先启动1线程(协程),在2,4,8,直到时延超出自己阈值,比如200ms。就线性增大协程并发量

14. 几个预防并发搞垮下游服务的方法

使用限流器
限流器了之后,只是让我们的并发请求分布地更均匀了,最好我们能在受到下游反馈完成后再开始下次并发。
使用WaitGroup
这里调用程序会等待这一批任务都执行完后,再开始查下一批数据进行下一批请求,等待时间取决于这一批请求中最晚返回的那个响应用了多少时间。
使用Semaphore
如果你不想等一批全部完成后再开始下一批,也可以采用一个完成后下一个补上的策略,这种比使用WaitGroup做并发控制,如果下游资源够,整个任务的处理时间会更快一些。这种策略需要使用信号量(Semaphore)做并发控制
使用生产者消费者模式
也有不少读者回复说得加线程池才行,因为每个人公司里可能都有在用的线程池实现,直接用就行,我在这里就不再献丑给大家实现线程池了。在我看来我们其实是需要实现一个生产者和消费者模式,让线程池帮助我们限制只有固定数量的消费者线程去做下游服务的调用,而生产者则是将数据存储里取出来。

15. Golang 五种原子性操作的用法详解

16. Go的atomic.Value为什么不加锁也能保证数据线程安全?

【Go】原子操作atomic.Value的使用:https://blog.csdn.net/q895431756/article/details/111063656
1.atomic.Value可以实现对自定义类型的原子操作
2.不能存入nil
3.对于同一个atomic.Value不能存入类型不同的值
4.最好不要使用atomic.Value存储引用类型的值,可能导致数据不是并发安全的

Golang 五种原子性操作的用法详解:https://mp.weixin.qq.com/s?__biz=MzUzNTY5MzU2MA==&mid=2247489229&idx=1&sn=3674ab103ec4dd704f0e9d1a784eeed4

Go 语言提供了哪些原子操作
Go语言通过内置包sync/atomic提供了对原子操作的支持,其提供的原子操作有以下几大类:

增减,操作的方法名方式为AddXXXType,保证对操作数进行原子的增减,支持的类型为int32、int64、uint32、uint64、uintptr,使用时以实际类型替换前面我说的XXXType就是对应的操作方法。
载入,保证了读取到操作数前没有其他任务对它进行变更,操作方法的命名方式为LoadXXXType,支持的类型除了基础类型外还支持Pointer,也就是支持载入任何类型的指针。
存储,有载入了就必然有存储操作,这类操作的方法名以Store开头,支持的类型跟载入操作支持的那些一样。
比较并交换,也就是CAS (Compare And Swap),像Go的很多并发原语实现就是依赖的CAS操作,同样是支持上面列的那些类型。
交换,这个简单粗暴一些,不比较直接交换,这个操作很少会用。

使用目的:互斥锁是用来保护一段逻辑,原子操作用于对一个变量的更新保护。
底层实现:Mutex由操作系统的调度器实现,而atomic包中的原子操作则由底层硬件指令直接提供支持,这些指令在执行的过程中是不允许中断的,因此原子操作可以在lock-free的情况下保证并发安全,并且它的性能也能做到随CPU个数的增多而线性扩展。
对于一个变量更新的保护,原子操作通常会更有效率,并且更能利用计算机多核的优势。

Go 并发编程 — 结构体多字段的原子操作 atomic.Value:https://www.icode9.com/content-1-1362486.html

参考

Golang 并发编程之同步原语:https://mp.weixin.qq.com/s?__biz=MzUzNTY5MzU2MA==&mid=2247484379&idx=1&sn=1a2abc6f639a34e62f3a5a0fcd774a71

#
Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×