chaoz的杂货铺

生命有息、学无止境、折腾不止

0%

2022-Golang-并发编程-协程

并发编程

协程(Goroutine)

与其他函数同时运行的函数或者方法,可以认为是轻量级线程。一个程序运行数千个是很常见的现象。

go中的协程与线程的区别是什么呢?

https://segmentfault.com/a/1190000040373756
https://www.ruanyifeng.com/blog/2013/04/processes_and_threads.html
协程和线程的整体对比

20220214163439
进程:
进程是操作系统对一个正在运行的程序的一种抽象,进程是资源分配的最小单位。
进程间的通信需要借助IPC,在进程间进行信息交换,性能开销很大。
创建进程,一般是调用fork方法,性能开销也很大。
单个 CPU 一次只能运行一个任务。

  • 什么是 fork 方法:
    系统中除了0号进程(系统创建的)之外,linux系统中都是由其他进程创建的。创建新进程的进程,即调用fork函数的进程为父进程,新建的进程为子进程。

调用一次fork()方法,该方法会返回两次。一次是在调用进程(也就是派生出的子进程的父进程)中返回一次,返回值是新派生的进程的进程ID。一次是在子进程中返回,返回值是0,代表当前进程为子进程。如果返回值为-1的话,则代表在派生新进程的过程中出错。

    • fork函数出错的情况  
      fork函数返回值为-1即创建失败,有两种情况可能会导致fork 函数出错;
      系统中已经有太多的进程存在;
      调用fork函数的用户的进程太多。
    • fork()的实质过程
      在fork()调用完之后,父、子进程是并发执行的,并没有先后顺序!!
      即在fork()之前的进程拥有的资源会被复制到新的进程中去,注意是复制,不是复用。
    • fork()的用法
      一个进程进行自身的复制,这样每个副本可以独立的完成具体的操作,在多核处理器中可以并行处理数据。这也是网络服务器的其中一个典型用途,多进程处理多连接请求。
      一个进程想执行另一个程序。比如一个软件包含了两个程序,主程序想调起另一个程序的话,它就可以先调用fork来创建一个自身的拷贝,然后通过exec函数来替换成将要运行的新程序。

线程:
多线程比多进程之间更容易共享数据,在上下文切换中线程一般比进程更高效。
线程之间能够非常方便、快速地共享数据。
只需将数据复制到进程中的共享区域就可以了,但需要注意避免多个线程修改同一份内存。
创建线程比创建进程要快 10 倍甚至更多。
线程都是同一个进程下自家的孩子,像是内存页、页表等就不需要了。

协程:
协程(Coroutine)是用户态的线程。通常创建协程时,会从进程的堆中分配一段内存作为协程的栈。
线程的栈有 8 MB,而协程栈的大小通常只有 KB,而 Go 语言的协程更夸张,只有 2-4KB,非常的轻巧。

协程的特点:
1.节省 CPU:避免系统内核级的线程频繁切换,造成的 CPU 资源浪费。好钢用在刀刃上。而协程是用户态的线程,用户可以自行控制协程的创建于销毁,极大程度避免了系统级线程上下文切换造成的资源浪费。
2.节约内存:在 64 位的Linux中,一个线程需要分配 8MB 栈内存和 64MB 堆内存,系统内存的制约导致我们无法开启更多线程实现高并发。而在协程编程模式下,可以轻松有十几万协程,这是线程无法比拟的。
3.稳定性:前面提到线程之间通过内存来共享数据,这也导致了一个问题,任何一个线程出错时,进程中的所有线程都会跟着一起崩溃。
4.开发效率:使用协程在开发程序之中,可以很方便的将一些耗时的IO操作异步化,例如写文件、耗时 IO 请求等。

  • 用户态和内核态的区别
    内核态(Kernel Mode):运行操作系统程序,操作硬件
    用户态(User Mode):运行用户程序
    intel x86 CPU有四种不同的执行级别0-3,linux只使用了其中的0级和3级分别来表示内核态和用户态。
    • 这两种状态的主要差别是
      处于用户态执行时,进程所能访问的内存空间和对象受到限制,其所处于占有的处理器是可被抢占的
      处于内核态执行时,则能访问所有的内存空间和对象,且所占有的处理器是不允许被抢占的。

怎么去等待所有协程都完成任务了的呢?

  • WaitGroup
    • Add:WaitGroup 类型有一个计数器,默认值是 0,通常通过个方法来标记需要等待的子协程数量
    • Done:当某个子协程执行完毕后,可以通过 Done 方法标记已完成,常用 defer 语句来调用
    • Wait 阻塞当前协程,直到对应 WaitGroup 类型实例的计数器值归零

父goroutine退出,如何使得子goroutine也退出?

一点基本概念
1.goroutine无法从外部被关闭,除了结束进程
2.父goroutine和它创建的goroutine是平行关系,不是父goroutine退出了,子goroutine就会退出。
3.select 中如果有default就会执行一遍就结束,如果没有就相当于一个for循环直到某个case为ture。
4.context其实也是通过channel来实现的. (详情可以看context标准库)

golang for循环中使用 goroutine 产生的问题

https://juejin.cn/post/6844904133208604679
原因:这2段代码实际上是遍历数组的所有变量。由于闭包只是绑定到这个value变量上,并没有被保存到goroutine栈中,所以以上代码极有可能运行的结构都输出为切片的最后一个元素。因为这样写会导致for循环结束后才执行goroutine多线程操作,这时候value值只指向了最后一个元素。这样的结果不是我们所希望的,而且还会产生并发的资源抢占冲突所以是非常不推荐这样写的。

两种解决方案:

  1. 在这里将 val 作为一个参数传入 goroutine 中
  2. 循环内定义新的变量

如何拿到goroutine的返回值

https://juejin.cn/post/6969183416969330725
go语言在执行goroutine的时候、是没有返回值的、这时候我们要用到go语言中特色的channel来获取返回值。
通过channel拿到返回值有两种处理方式,一种形式是具有go风格特色的,即发送给一个for channel 或 select channel 的独立goroutine中,由该独立的goroutine来处理函数的返回值。还有一种传统的做法,就是将所有goroutine的返回值都集中到当前函数,然后统一返回给调用函数。

goroutine创建数量有限制吗?Goroutine 数量控制在多少合适,会影响 GC 和调度?为什么不要大量使用goroutine?为什么不要频繁创建和停止goroutine?

https://geektutu.com/post/hpg-concurrency-control.html
https://www.helloworld.net/p/8643482901
https://juejin.cn/post/6999807716482875422

没有限制。
“合理” 这个词,是需要看具体场景来定义的,可结合上述对 GPM 的学习和了解。得出:
M:有限制,默认数量限制是 10000,可调整。
G:没限制,但受内存影响。
P:受本机的核数影响,可大可小,不影响 G 的数量创建。
Goroutine 数量在 MG 的可控限额以下,多个把个、几十个,少几个其实没有什么影响,就可以称其为 “合理”。

真实情况:
在真实的应用场景中,没法如此简单的定义。如果你 Goroutine:
在频繁请求 HTTP,MySQL,打开文件等,那假设短时间内有几十万个协程在跑,那肯定就不大合理了(可能会导致 too many files open)。
常见的 Goroutine 泄露所导致的 CPU、Memory 上涨等,还是得看你的 Goroutine 里具体在跑什么东西。
还是得看 Goroutine 里面跑的是什么东西。

Goroutine泄漏

只要产生了阻塞就会产生Goroutine溢出,好像是。
https://segmentfault.com/a/1190000040161853

channel 使用不当

有一个 channel 的读写操作出现了问题,自然就阻塞了。

发送不接收
发送不接收

nil channel

channel 如果忘记初始化,那么无论你是读,还是写操作,都会造成阻塞。

奇怪的慢等待

对 http.Client 设置超时时间,对 http.Client 设置超时时间

互斥锁忘记解锁

同步锁使用不当

排查方法

我们可以调用 runtime.NumGoroutine 方法来获取 Goroutine 的运行数量,进行前后一比较,就能知道有没有泄露了。

但在业务服务的运行场景中,Goroutine 内导致的泄露,大多数处于生产、测试环境,因此更多的是使用 PProf:

1
2
3
4
import (
"net/http"
_ "net/http/pprof"
)

http.ListenAndServe(“localhost:6060”, nil))
只要我们调用 http://localhost:6060/debug/pprof/goroutine?debug=1,PProf 会返回所有带有堆栈跟踪的 Goroutine 列表。

并行goroutine如何实现

好像就没有并行goroutine

并发与并行

https://cloud.tencent.com/developer/article/1424249
并发和并行的区别
并发,指的是多个事情,在同一时间段内同时发生了。
并行,指的是多个事情,在同一时间点上同时发生了。

并发的多个任务之间是互相抢占资源的。
并行的多个任务之间是不互相抢占资源的、

只有在多CPU的情况中,才会发生并行。否则,看似同时发生的事情,其实都是并发执行的。

用go实现一个协程池,大概用什么实现

https://www.jianshu.com/p/440f4c3f7c78
https://whlpsi.com/post/18.html
https://studygolang.com/articles/12478
https://geektutu.com/post/hpg-sync-pool.html

goroutine为什么比线程开销小,实现原理

https://huweicai.com/process-thread-goroutine/
1.Go协程默认的栈空间内存大小只有 2KB(上限1GB),而Linux线程栈大小默认是8MB,4096倍的差距
2.线程切换需要进行系统调用,开销比普通函数调用大很大,而协程则完全在用户态实现没有这个开销
3.单从数据结构上而言,协程结构体只有大概50个成员,而线程结构体拥有100个左右的成员,比协程多一倍
4.协程切换不需要保存寄存器信息,协程通过栈来传递变量
20220215013627

groutinue什么时候会被挂起

https://developer.51cto.com/article/681462.html
主要场景为:
通道(Channel)。
垃圾回收(GC)。
休眠(Sleep)。
锁等待(Lock)。
抢占(Preempted)。
IO 阻塞(IO Wait)
其他,例如:panic、finalizer、select 等。

僵尸进程和孤儿进程

https://blog.csdn.net/qq_27068845/article/details/78816995

孤儿进程

孤儿进程将被 init 进程(进程号为 1)所收养,并由 init 进程对它们完成状态收集工作。

由于孤儿进程会被 init 进程收养,所以孤儿进程不会对系统造成危害。

僵尸进程

僵尸进程通过 ps 命令显示出来的状态为 Z(zombie)。

系统所能使用的进程号是有限的,如果产生大量僵尸进程,将因为没有可用的进程号而导致系统不能产生新的进程。

要消灭系统中大量的僵尸进程,只需要将其父进程杀死,此时僵尸进程就会变成孤儿进程,从而被 init 进程所收养,这样 init 进程就会释放所有的僵尸进程所占有的资源,从而结束僵尸进程。

进程状态

状态 说明
R running or runnable (on run queue)
正在执行或者可执行,此时进程位于执行队列中。
D uninterruptible sleep (usually I/O)
不可中断阻塞,通常为 IO 阻塞。
S interruptible sleep (waiting for an event to complete)
可中断阻塞,此时进程正在等待某个事件完成。
Z zombie (terminated but not reaped by its parent)
僵死,进程已经终止但是尚未被其父进程获取信息。
T stopped (either by a job control signal or because it is being traced)
结束,进程既可以被作业控制信号结束,也可能是正在被追踪。

进程间通信有哪几种方式,进程间通信哪种最高?

https://juejin.cn/post/7021888536097849358
20220215024024

线程间通信,常用方式:

信号量
互斥锁
条件变量

对于Go语言来说,Go程序启动之后对外是一个进程,内部包含若干协程,协程相当于用户态轻量级线程,所以协程的通信方式大多可以使用线程间通信方式来完成。

协程间通信方式,官方推荐使用channel,channel在一对一的协程之间进行数据交换与通信十分便捷。但是,一对多的广播场景中,则显得有点无力,此时就需要sync.Cond来辅助。

为什么共享内存效率高?

golang不要通过共享内存来通信,而应该通过通信来共享内存

我们知道各进程之间是独立存在,互不影响的。有没有一种方式让这些进程之间产生联系呢?当然有!那就是共享内存。共享内存是进程间通信中最简单的方式之一。站在进程的角度来说,共享内存就是可以同时被多个进程访问的内存。由于所有进程共享同一块内存,因此这种通信方式效率非常高。

https://cloud.tencent.com/developer/article/1797464
https://www.51cto.com/article/693037.html

设计一个秒杀系统

https://github.com/Nobodiesljh/seckill-golang
慕课网资料已下载

思考题

go实现并发如何保证安全?

https://www.cnblogs.com/taotaozhuanyong/p/15048196.html
http://www.codebaoku.com/it-go/it-go-226692.html

Go是否可以无限go? 如何限定数量?

https://www.kancloud.cn/aceld/golang/1958323

缓存穿透如何解决

当然缓存系统是不可避免的,少量的缓存穿透对系统也没有损害,不可避免的原因有以下几点:

缓存系统的容量是有限的,不可能存储系统所有的数据,那么在查询未缓存数据的时候就会发生缓存穿透。
另一方面就是基于‘二八原则’,我们通常只会缓存常用的那 20% 的热点数据。
正常情况下的缓存穿透是没什么伤害的,但是如果你的系统遭遇攻击,存在大量的缓存穿透的话,那么可能就是一个麻烦了,如果大量的缓存穿透超过了后端服务器的承受能力,那么就有可能造成服务崩溃,这是不可接受的。

基于存在这种大量缓存穿透的可能性,所以我们就需要从根源上解决缓存穿透的问题,解决缓存穿透,目前一般有两种方案:缓存空值和使用布隆过滤器。

https://zhuanlan.zhihu.com/p/112108671

goalng 实现布隆过滤器?

https://blog.csdn.net/l2975/article/details/122204622
https://blog.csdn.net/weixin_42310154/article/details/119386707

golang 实现缓存空值

喜欢这篇文章?打赏一下作者吧!

欢迎关注我的其它发布渠道