GoLang 协程池

news2024/11/15 16:22:45

Goroutine

1.Goroutine 是 Golang 提供的一种轻量级线程,我们通常称之为「协程」,相比较线程,创建一个协程的成本是很低的。所以你会经常看到 Golang 开发的应用出现上千个协程并发的场景。

Goroutine 的优势:

   与线程相比,Goroutines 成本很低。 它们的堆栈大小只有几 kb,堆栈可以根据应用程序的需要增长和缩小,context switch 也很快,而在线程的情况下,堆栈大小必须指定并固定。

   Goroutine 被多路复用到更少数量的 OS 线程。 一个包含数千个 Goroutine 的程序中可能只有一个线程。如果该线程中的任何 Goroutine 阻塞等待用户输入,则创建另一个 OS 线程并将剩余的 Goroutine 移动到新的 OS 线程。所有这些都由运行时处理,作为开发者无需耗费心力关心,这也使得我们有很干净的 API 来支持并发。

Goroutines 使用 channel 进行通信。 channel 的设计有效防止了在使用 Goroutine 访问共享内存时发生竞争条件(race conditions) 。channel 可以被认为是 Goroutine 进行通信的管道。

何为并发?

     并发指在一段时间内有多个任务(程序,线程,协程等)被同时执行。注意,不是同一时刻。在单处理机系统中,每一时刻仅能有一个任务被执行,故微观上这些任务只能是分时地交替执行。倘若在计算机系统中有多个处理机,则这些可以并发执行的任务被分配到多个处理机上同时执行,这就实现了并行。并发 (Concurrency) 和 并行 ( Parallelism) 是不同的。这里引用 Erlang 之父 Joe Armstrong 对并发与并行区别的形象描述:

2.并发的好处

并发最直接的好处就是高效。

假如我们要做三件事情,一是吃饭,二是洗脚,三是看电视。如果串行执行,那么非常浪费时间。

如果改成并发执行,吃饭,洗脚,看电视同时进行,则非常节省时间。

3.go 如何实现并发

Go 程(goroutine)是由 Go 运行时管理的轻量级线程。通过它我们可以轻松实现并发编程。

示例:

package main

import (

        "fmt"

        "sync"

        "time"

)

func eatFood() {

        fmt.Println("eat cost 3 seconds")

        time.Sleep(3 * time.Second)

}

func washFeet() {

        fmt.Println("wash feet cost 3 seconds")

        time.Sleep(3 * time.Second)

}

func watchTV() {

        fmt.Println("watch tv cost 3 seconds")

        time.Sleep(3 * time.Second)

}

func worker(task func(), wg *sync.WaitGroup) {

        defer wg.Done()

        task()

}

func main() {

        var wg sync.WaitGroup

        wg.Add(3)

        // 并发执行

        start := time.Now().Unix()

        go worker(eatFood, &wg)  // 创建一个 goroutine 执行吃饭任务

        go worker(washFeet, &wg) // 创建一个 goroutine 执行洗脚任务

        go worker(watchTV, &wg)  // 创建一个 goroutine 执行看电视任务

        wg.Wait()                // 等待三个 goroutine 全部执行完成

        fmt.Printf("total only cost %v seconds\n", time.Now().Unix()-start)

}

结果输出:

watch tv cost 3 seconds

wash feet cost 3 seconds

eat cost 3 seconds

total only cost 3 seconds

 

4.GPM调度模型

    说到 Go 程,不得不说 Go 程的调度。Go 优秀的并发性能得益于出色的基于 G-M-P 模型的 Go 程调度器,官方宣称用 Golang 写并发程序的时候随便起个成千上万的 goroutine 毫无压力。

Go 程的调度模型是 G-P-M,通过 P(Processor,逻辑处理器)将 G(Goroutine,用户线程)与 M(Machine,系统线程)解耦,我们可以随意开启多个 G 交由调度器分配到 P,再通过 P 将 G 交由 M 来完成执行。

 

Go 调度器的基本模型:

每个 P 维护一个 G 的本地队列;

当一个 G 被创建出来,或者变为可执行状态时,优先把它放到 P 的本地队列中,否则放到全局队列;

当一个 G 在 M 里执行结束后,P 会从队列中把该 G 取出;如果此时 P 的队列为空,即没有其他 G 可以执行, M 会先尝试从全局队列寻找 G 来执行,如果全局队列为空,它会随机挑选另外一个 P,从它的队列里拿走一半 G 到自己的队列中执行。

注意: P 的数量在默认情况下,会被设定为 CPU 的核数。而 M 虽然需要跟 P 绑定执行,但数量上并不与 P 相等。这是因为 M 会因为系统调用或者其他事情被阻塞,因此随着程序的执行,M 的数量可能增长,而 P 在没有用户干预的情况下,则会保持不变。

Go 协程代价

Go 程虽然轻量,但仍有开销。

Go 的开销主要是三个方面:创建(占用内存)、调度(增加调度器负担)和删除(增加 GC 压力)。
调度开销

时间上,协程调度也会有 CPU 开销。我们可以利用runntime.Gosched()让当前协程主动让出 CPU 去执行另外一个协程,下面看一下协程之间切换的耗时。

package main

import (

        "fmt"

        "runtime"

        "time"

)

const Num = 10000

func cal() {

        for i := 0; i < Num; i++ {

                runtime.Gosched()

        }

}

func main() {

        //设置一个processor

        runtime.GOMAXPROCS(1)

        start := time.Now().UnixNano()

        go cal()

        for i := 0; i < Num; i++ {

                runtime.Gosched()

        }

        end := time.Now().UnixNano()

        fmt.Printf("total %vns per %vns\n", end-start, (end-start)/Num)

}

输出结果值:

total 515100ns per 51ns

可见一次协程的切换,耗时55ns,相对于线程的微秒级耗时切换,性能表现非常优秀,但仍有开销;

 

GC开销

创建Go协程到运行结束,占用的内存资源需要由GC来回收,如果无休止的创建大量Go协程到运行结束,占用内存资源需要GC来回收,如果无休止的创建大量Go协程后,势必会造成Gcyali

 

package main

import (

        "fmt"

        "runtime"

        "runtime/debug"

        "sync"

        "time"

)

func createLargeNumGoroutine(num int, wg *sync.WaitGroup) {

        wg.Add(num)

        for i := 0; i < num; i++ {

                go func() {

                        defer wg.Done()

                }()

        }

}

func main() {

        //设置一个Processor 保证go 协程串行

        runtime.GOMAXPROCS(1)

        //关闭gc改为手动执行

        debug.SetGCPercent(1)

        var wg sync.WaitGroup

        createLargeNumGoroutine(1000, &wg)

        wg.Wait()

        t := time.Now()

        runtime.GC() //手动GC

        cost := time.Since(t)

        fmt.Printf("GC cost %v when goroutine num is %v\n", cost, 1000)

        createLargeNumGoroutine(10000, &wg)

        wg.Wait()

        t = time.Now()

        runtime.GC() // 手动 GC。

        cost = time.Since(t)

        fmt.Printf("GC cost %v when goroutine num is %v\n", cost, 10000)

        createLargeNumGoroutine(100000, &wg)

        wg.Wait()

        t = time.Now()

        runtime.GC() // 手动 GC。

        cost = time.Since(t)

        fmt.Printf("GC cost %v when goroutine num is %v\n", cost, 100000)

}

输出:

GC cost 539.8µs when goroutine num is 1000

GC cost 0s when goroutine num is 10000

GC cost 11.2763ms when goroutine num is 100000

当创建的 Go 程数量越多,GC 耗时越大。上面的分析目的是为了尽可能地量化 goroutine 的开销。虽然官方宣称用 Golang 写并发程序的时候随便起个成千上万的 goroutine 毫无压力,但当我们起十万、百万甚至千万个 goroutine 呢?goroutine 轻量的开销将被放大。

 

6.协程池的作用

   无休止地创建大量 goroutine,势必会因为对大量 go 程的创建、调度和销毁带来性能损耗。为了解决这个问题,可以引入协程池。使用协程池限制 Go 程的开辟个数在大型并发场景是有必要的,这也是性能优化方法中对象复用思想的一个具体应用。

7.简易协程池的设计&实现

一个简单的协程池可以这么设计。

(1)定义一个接口表示任务,每一个具体的任务实现这个接口。

(2)使用 channel 作为任务队列,当有任务需要执行时,将这个任务插入到队列中。

(3)开启固定的协程(worker)从任务队列中获取任务来执行。

上面这个协程池的特点

(1)Go 程数量固定。可以将 worker 的数量设置为最大同时并发数 runtime.NumCPU()。

(2)Task 泛化。提供任务接口,支持多类型任务,不同业务场景下只要实现任务接口便可以提交到任务队列供 worker 调用。

(3)简单易用。设计简约,实现简单,使用方便。

一个示例:

package main

import (

        "fmt"

        "runtime"

        "sync"

        "time"

)

// Task 任务接口

type Task interface {

        Execute()

}

// pool协程池

type Pool struct {

        TaskChannel chan Task //任务队列

}

// NewPool 创建一个协程池。

func NewPool(cap ...int) *Pool {

        // 获取 worker 数量

        var n int

        if len(cap) > 0 {

                n = cap[0]

        }

        if n == 0 {

                n = runtime.NumCPU()

        }

        p := &Pool{

                TaskChannel: make(chan Task),

        }

        // 创建指定数量 worker 从任务队列取出任务执行。

        for i := 0; i < n; i++ {

                go func() {

                        for task := range p.TaskChannel {

                                task.Execute()

                        }

                }()

        }

        return p

}

// submit 提交任务

func (p *Pool) Submit(t Task) {

        p.TaskChannel <- t

}

// EatFood 吃饭任务

type EatFood struct {

        wg *sync.WaitGroup

}

func (e *EatFood) Execute() {

        defer e.wg.Done()

        fmt.Println("eat cost 3 seconds")

        time.Sleep(3 * time.Second)

}

// washFeet 洗脚任务

type WashFeet struct {

        wg *sync.WaitGroup

}

func (w *WashFeet) Execute() {

        defer w.wg.Done()

        fmt.Println("wash feet cost 3 seconds")

        time.Sleep(3 * time.Second)

}

// watch TV看电视任务

type WatchTV struct {

        wg *sync.WaitGroup

}

func (w *WatchTV) Execute() {

        defer w.wg.Done()

        fmt.Println("Watch Tv cost 3 seconds")

        time.Sleep(3 * time.Second)

}

func main() {

        p := NewPool()

        var wg sync.WaitGroup

        wg.Add(3)

        task1 := &EatFood{

                wg: &wg,

        }

        task2 := &WashFeet{

                wg: &wg,

        }

        task3 := &WatchTV{

                wg: &wg,

        }

        p.Submit(task1)

        p.Submit(task2)

        p.Submit(task3)

        //等待所有任务执行完成

        wg.Wait()

}

输出:

eat cost 3 seconds

Watch Tv cost 3 seconds

wash feet cost 3 seconds

设计时,我们也可以将任务队列中的任务设计为无参匿名函数,这样子使用起来可能会更简单。

// pool协程池

type Pool struct {

        TaskChannel chan Task //任务队列

}

总体来说上面简易的协程池的不足:

(1)无法知道worker 与pool的状态

(2)woker数量不足无法动态扩增

(3)worker数量过多无法自动缩减

8.开源协程池的使用

一个成熟的协程池应该具备如下能力:

(1)worker & pool 状态控制;

性能测试、任务超时等都需要知道和控制任务与 Go 程池的状态。

(2)无锁化操作;

在 worker 和 pool 的状态读写时使用 atomic 原子操作,避免多次上锁带来性能损耗。

(3)动态可伸缩;

根据实际请求量的大小,动态扩缩容以避免 Go 程数量出现过少或过多的情况。

(4)资源复用。

对回收的 worker 放到 sync.Pool 进行复用,而不是直接销毁,降低 GC 压力,提高性能。

目前有很多第三方库实现了协程池,可以很方便地用来控制协程的并发数量,比较受欢迎的有:

    ants 是一个高性能的 goroutine 池,实现了对大规模 goroutine 的调度管理、goroutine 复用,允许使用者在开发并发程序的时候限制 goroutine 数量,复用资源,达到更高效执行任务的效果。继续用吃饭、洗脚、看电视为例,演示ants的使用:

package main

import (

        "fmt"

        "sync"

        "time"

        "github.com/panjf2000/ants/v2"

)

func eatFood() {

        fmt.Println("eat cost 3 seconds")

        time.Sleep(3 * time.Second)

}

func washFeet() {

        fmt.Println("wash feet cost 3 seconds")

        time.Sleep(3 * time.Second)

}

func watchTV() {

        fmt.Println("watch tv cost 3 seconds")

        time.Sleep(3 * time.Second)

}

func main() {

        var wg sync.WaitGroup

        taskEatFood := func() {

                defer wg.Done()

                eatFood()

        }

        taskWashFeet := func() {

                defer wg.Done()

                watchTV()

        }

        taskWatchTV := func() {

                defer wg.Done()

                watchTV()

        }

        t := time.Now()

        wg.Add(3)

        //use the common pool

        ants.Submit(taskEatFood)

        ants.Submit(taskWashFeet)

        ants.Submit(taskWatchTV)

        wg.Wait()

        fmt.Printf("total only cost %v\n", time.Since(t))

}

输出结果:

watch tv cost 3 seconds

watch tv cost 3 seconds

eat cost 3 seconds

total only cost 3.0051813s

小结:

资源复用是高性能编程的基本方法之一,在高并发场景,我们可以使用协程池来复用协程提高程序性能。

其他诸如:

无锁: 尽量不要加锁对某个变量读写,而应该分多个变量单独读写;

缓存: 网络IO是接口耗时的主要部分,对实时性要求不高的业务场景,可以增加本地缓存降低接口耗时;

减包: 限制请求时分页大小,减小回包大小,降低打解包对 CPU 的消耗。

  • Jeffail/tunny
  • panjf2000/ants

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/366920.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Spring Security+jwt+redis+自定义认证逻辑 权限控制

Spring Securityjwtredis自定义认证逻辑 权限控制 1.拦截访问基本思路 2.创建数据库表&#xff1a;角色表&#xff08;应该6个表&#xff0c;这里只用用户表代替角色表&#xff09;、权限表、路径表、角色-权限表、权限-路径表 /* SQLyog Professional v12.14 (64 bit) MySQL…

Leetcode第450题删除二叉搜索树中的结点|C语言

题目&#xff1a; 给定一个二叉搜索树的根节点 root 和一个值 key&#xff0c;删除二叉搜索树中的 key 对应的节点&#xff0c;并保证二叉搜索树的性质不变。返回二叉搜索树&#xff08;有可能被更新&#xff09;的根节点的引用。 一般来说&#xff0c;删除节点可分为两个步骤…

一个跟蘑菇结缘的企业老板

记得那是一个很久以前的一家公司了董事长办公室里中的大型盆栽里面长了一个蘑菇董事长认为是祥瑞每天都会浇水后来一个新来的保洁阿姨以为杂草啥的给他掰掉扔垃圾桶了董事长第二天来浇水的时候发现没了就问谁动了他的蘑菇问道之后就跑到楼道大垃圾桶那里把蘑菇找回来种在花盆里…

“点工”的觉悟,5年时间从7K到24K的转变,我的测试道路历程~

2015年7月我从一个90%以上的人都不知道的二本院校毕业&#xff08;新媒体专业&#xff09;&#xff0c;凭借自学的软件测试&#xff08;点点点&#xff09;在北京找到了一份月薪7000的工作&#xff0c;在当时其实还算不错&#xff0c;毕竟我的学校起点比较差&#xff0c;跟大部…

python学习笔记——csv文件

目录 一、csv文件和Excel文件区别 二、手动转换&#xff08;文本与列表&#xff09; ①普通的写(列表嵌套转成文本的表格形式) ②普通的读&#xff08;文本的表格形式转成列表嵌套&#xff09; 二、csv库-读 1、CSV库-读-reader() 2、CSV库-读-DictReader() 三、csv库-写 …

基于YOLO的酸枣病虫害检测识别实践

在我前面的博文中对于农作物病虫害的检测识别已经做过了&#xff0c;不过那个主要是针对水稻的&#xff0c;文章如下&#xff1a;《基于yolov5的轻量级水稻虫害目标检测项目实践》感兴趣的话可以自行移步阅读。这里主要是针对酸枣常见的几种病虫害检测检测识别&#xff0c;首先…

苹果电脑如何录屏?3个方法,轻松学会

苹果电脑是很多创作者、视频制作人和经常工作用户的选择&#xff0c;但是如何在苹果电脑上录制高质量的屏幕视频呢&#xff1f;苹果电脑如何录屏&#xff1f;本文将介绍3种不同的方法&#xff0c;帮助小伙伴轻松学会如何在苹果电脑上录制屏幕视频。 方法一&#xff1a;使用Mac自…

假设检验的基本思想

假设检验 首先了解参数估计&#xff0c;比如有服从正态分布的数据集X∼N(μ,σ2)X\sim N(\mu,\sigma^{2})X∼N(μ,σ2)&#xff0c;我们希望根据样本x1,...xnx_{1},...x_{n}x1​,...xn​估计出参数μ,σ\mu,\sigmaμ,σ&#xff0c;这些参数可以是一个具体值&#xff0c;也可以…

【C++】Windows动态库【.DLL文件】制作方法总结

如题&#xff0c;我们本篇介绍如何制作DLL&#xff0c;将代码类中的方法以接口的形式暴露出来给exe程序使用。会涉及类厂创建方法实例、声明DLL接口、.def文件的使用等。 目录 一、DLL介绍 二、C制作DLL文件 2.1 DLL端 2.2 调用端 三、DLL导出类方法 四、COM技术制作DLL…

扎心话题 | 设计院背后的潜规则你知道吗?

大家好&#xff0c;我是建模助手。 大家都知道&#xff0c;在过去的2022年经济是真难&#xff01;以小编所在的广东为例&#xff0c;全年GDP增长仅1.9%。 这个数据足以呈现一个社会现象——不仅消费力咔咔下降&#xff0c;各行各业更有不同程度地嗝屁。这其中也包括一些设计院…

只要一直向前定能到达远方,社科院与杜兰大学金融管理硕士项目为你注入动力

在人生这条道路上&#xff0c;我们很远的路要走&#xff0c;不管前方是否平坦&#xff0c;我们只要坚持前向&#xff0c;终将抵达远方。一路上我们付出很多&#xff0c;也收获很多。想要变得更强大&#xff0c;就要不断优化自身&#xff0c;积攒更多的能量&#xff0c;社科院与…

Flask入门(10):Flask使用SQLAlchemy

目录11.SQLAlchemy11.1 简介11.2 安装11.3 基本使用11.4 连接11.5 数据类型11.6 执行原生sql11.7 插入数据11. 8 删改操作11.9 查询11.SQLAlchemy 11.1 简介 SQLAlchemy的是Python的SQL工具包和对象关系映射&#xff0c;给应用程序开发者提供SQL的强大功能和灵活性。它提供了…

浅析无人值守+智慧巡检变电站安全管控系统设计方案

一、项目背景 安全是电力生产的基石&#xff0c;确保电网安全和人身安全&#xff0c;是电网企业安全工作的出发点和落脚点。 随着智能信息化技术应用越来越广泛&#xff0c;智能信息化现场安全管理是近年来基于智能安全巡检技术下发展起来的现场作业安全管理新技术。 变电站运…

【机器学习】朴素贝叶斯算法

朴素贝叶斯&#xff08;Naive Bayes&#xff09;是经典的机器学习算法之一&#xff0c;也是为数不多的基于概率论的分类算法。由于朴素贝叶斯计算联合概率&#xff0c;所以朴素贝叶斯模型属于生成式模型。经典应用案例包括&#xff1a;文本分类、垃圾邮件过滤等。 1.贝叶斯公式…

rust 安装

rust 安装一、需要一个c的环境二、配置环境变量三、开始安装一、需要一个c的环境 安装Visual Studio 二、配置环境变量 Rust需要安装两个东西&#xff0c;一个是rustup&#xff0c;一个是cargo。所以你需要设置两个环境变量来分别指定他们的安装目录。 通过RUSTUP_HOME指定…

滤波算法:经典卡尔曼滤波

卡尔曼滤波实质上就是基于观测值以及估计值二者的数据对真实值进行估计的过程。预测步骤如图1所示&#xff1a; ​图1 卡尔曼滤波原理流程图 假设我们能够得到被测物体的位置和速度的测量值 ​&#xff0c;在已知上一时刻的最优估计值 ​以及它的协方差矩阵 的条件下&#xff…

ChatGPT热潮背后,金融行业大模型应用路在何方?——金融行业大模型应用探索

ChatGPT近两个月以来不断引爆热点&#xff0c;对人工智能应用发展的热潮前所未有地高涨&#xff0c;ChatGPT所代表的大模型在语义理解、多轮交互、内容生成中所展现的突出能力令人惊喜。而人工智能技术在金融行业的落地应用仍然面临挑战&#xff0c;虽然已经让大量宝贵的人力从…

易基因|ChIP-seq等组学研究鉴定出结直肠癌的致癌超级增强子:Nature子刊

大家好&#xff0c;这里是专注表观组学十余年&#xff0c;领跑多组学科研服务的易基因。超级增强子&#xff08;Super enhancer&#xff09;是一类包含多个普通增强子的大簇&#xff0c;主要富集高密度的转录因子、辅助因子及增强子相关表观修饰位点。与普通增强子相比&#xf…

canal实时同步mysql数据到elasticsearch(部署,配置,测试)

这里写目录标题简介工作原理MySQL主备复制原理canal 工作原理canal 使用流程环境搭建环境使用版本mysql配置修改配置创建从库权限账号创建测试数据库创建测试数据表elasticsearch配置创建索引建立映射canal的下载部署下载canal配置服务端 canal-deployer配置客户端canal-adapte…

Keysight E5061B网络分析仪

Keysight E5061B&#xff08;安捷伦&#xff09;网络分析仪可在 5 Hz 至 3 GHz 的宽频率范围内提供多功能的高性能网络分析。E5061B 提供了 ENA 系列共有的出色射频性能&#xff0c;还提供了成熟的 LF&#xff08;低频&#xff09;网络测量功能&#xff1b;包括带有内置 1 Mohm…