首页 > 其他分享 >【Golang】go语言中如何控制goroutine的数量

【Golang】go语言中如何控制goroutine的数量

时间:2022-10-04 00:23:15浏览次数:69  
标签:fmt goroutine Golang func time go channel

Go语言中,goroutine的创建成本很低,调度效率高,Go语言在设计时就是按以数万个goroutine为规范进行设计的,数十万个并不意外,但是goroutine在内存占用方面确实具有有限的成本,你不能创造无限数量的它们,比如这个例子:

ch := generate() 
go func() { 
        for range ch { } 
}()

这段代码通过generate()方法获得一个channel,然后启动一个goroutine一直去处理这个channel的数据,这个goroutine什么时候会退出?答案是不确定,ch是由函数generate()来决定的,所以有可能这个goroutine永远都不会退出,这就有可能会引发内存泄漏。

goroutine就是G-P-M调度模型中的G,我们可以把goroutine看成是一种协程,创建goroutine也是有开销的,但是开销很小,初始只需要2-4k的栈空间,当goroutine数量越来越大时,同时存在的goroutine也越来越多时,程序就隐藏内存泄漏的问题。看一个例子:

func main()  {
	for i := 0; i < math.MaxInt64; i++ {
		go func(i int) {
			time.Sleep(5 * time.Second)
		}(i)
	}
}

1、Context

Go 语言中的每一个请求的都是通过一个单独的 goroutine 进行处理的,HTTP/RPC 请求的处理器往往都会启动新的Goroutine 访问数据库和 RPC 服务,我们可能会创建多个goroutine 来处理一次请求,而 Context 的主要作用就是在不同的 goroutine 之间同步请求特定的数据、取消信号以及处理请求的截止日期。

Context包主要衍生了四个函数:

func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc)
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
func WithValue(parent Context, key, val interface{}) Context

使用这四个函数我们对goroutine进行控制,具体展开就不再本文说了,我们以WithCancel方法写一个例子:

package main

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

func main() {
	ctx, cancel := context.WithCancel(context.Background())
	go Speak(ctx)
	time.Sleep(10 * time.Second)
	cancel()
	time.Sleep(2 * time.Second)
	fmt.Println("拜拜!")
}

func Speak(ctx context.Context) {
	for range time.Tick(time.Second) {
		select {
		case <-ctx.Done():
			fmt.Println("拜拜!")
			return
		default:
			fmt.Println("开始执行")
		}
	}
}

运行结果:

PS D:\cpz\go-demo\test13> go run test4.go
开始执行
开始执行
开始执行
开始执行
开始执行
开始执行
开始执行
开始执行
开始执行
拜拜!
拜拜!

这里我们使用withCancel创建了一个基于Backgroundctx,然后启动了一个goroutine每隔1s执行一次,10s后在主goroutine中发送取消新信号,那么启动的goroutine在检测到信号后就会取消退出。

2、channel

我们知道channel是用于goroutine的数据通信,在Go中通过goroutine+channel的方式,可以简单、高效地解决并发问题。上面我们介绍了使用context来达到对goroutine的控制,实际上context的内部实现也是使用的channel,所以有时候为了实现方便,我们可以直接通过channel+select或者channel+close的方式来控制goroutine的退出,我们分别来一写一个例子:

方式1:channel+select

package main

import "fmt"

func fibonacci(ch chan int, done chan struct{}) {
	x, y := 0, 1
	for {
		select {
		case ch <- x:
			x, y = y, x+y
		case <-done:
			fmt.Println("over")
			return
		}
	}
}
func main() {
	ch := make(chan int)
	done := make(chan struct{})
	go func() {
		for i := 0; i < 10; i++ {
			fmt.Println(<-ch)
		}
		done <- struct{}{}
	}()
	fibonacci(ch, done)
}

上面的例子是计算斐波那契数列的结果,我们使用两个channel,一个channel用来传输数据,另外一个channel用来做结束信号,这里我们使用的是select的阻塞式的收发操作,直到有一个channel发生状态改变,我们也可以在select中使用default语句,那么select语句在执行时会遇到这两种情况:

  • 当存在可以收发的Channel时,直接处理该Channel 对应的 case
  • 当不存在可以收发的Channel 时,执行 default 中的语句;

建议大家使用带default的方式,因为在一个nil channel上的操作会一直被阻塞,如果没有default case,只有nil channelselect会一直被阻塞。

方式2:channel+close

package main

import (
	"fmt"
	"time"
)

func main() {
	ch := make(chan int, 10)
	go func() {
		for i := 0; i < 10; i++ {
			ch <- i
		}
		close(ch)
	}()
	go func() {
		for val := range ch {
			fmt.Println(val)
		}
		fmt.Println("receive data over")
	}()
	time.Sleep(5 * time.Second)
	fmt.Println("program over")
}

channel可以单个出队,也可以循环出队,因为我们可以使用for-range循环处理channelrange ch会一直迭代到channel被关闭,根据这个特性,我们也可做到对goroutine的控制:

我们可以通过以下方式达到控制goroutine数量的目的,不过本身Gogoroutine就已经很轻量了,所以控制goroutine的数量还是要根据具体场景分析,并不是所有场景都需要控制goroutine的数量的,一般在并发场景我们会考虑控制goroutine的数量,接下来我们来看一看如下几种方式达到控制goroutine数量的目的。

0、常用的三方库

写 go 并发程序的时候如果程序会启动大量的goroutine ,势必会消耗大量的系统资源(内存,CPU),所以可以考虑使用goroutine池达到复用goroutine,节省资源,提升性能。也有一些开源的协程池库,例如:antsgo-playground/pooljeffail/tunny等,这里我们看ants的一个官方例子:

package main

import (
	"fmt"
	ants "github.com/panjf2000/ants/v2"
	"sync"
	"sync/atomic"
	"time"
)

var sum int32

func myFunc(i interface{}) {
	n := i.(int32)
	atomic.AddInt32(&sum, n)
	fmt.Printf("run with %d\n", n)
}

func demoFunc() {
	time.Sleep(10 * time.Millisecond)
	fmt.Println("Hello World!")
}

func main() {
	defer ants.Release()

	runTimes := 1000

	// Use the common pool.
	var wg sync.WaitGroup
	syncCalculateSum := func() {
		demoFunc()
		wg.Done()
	}
	for i := 0; i < runTimes; i++ {
		wg.Add(1)
		_ = ants.Submit(syncCalculateSum)
	}
	wg.Wait()
	fmt.Printf("running goroutines: %d\n", ants.Running())
	fmt.Printf("finish all tasks.\n")

	// Use the pool with a function,
	// set 10 to the capacity of goroutine pool and 1 second for expired duration.
	p, _ := ants.NewPoolWithFunc(10, func(i interface{}) {
		myFunc(i)
		wg.Done()
	})
	defer p.Release()
	// Submit tasks one by one.
	for i := 0; i < runTimes; i++ {
		wg.Add(1)
		_ = p.Invoke(int32(i))
	}
	wg.Wait()
	fmt.Printf("running goroutines: %d\n", p.Running())
	fmt.Printf("finish all tasks, result is %d\n", sum)
}

这个例子其实就是计算大量整数和的程序,这里通过ants.NewPoolWithFunc()创建了一个 goroutine 池。第一个参数是池容量,即池中最多有 10 个goroutine。第二个参数为每次执行任务的函数。当我们调用p.Invoke(data)的时候,ants池会在其管理的 goroutine 中找出一个空闲的,让它执行函数taskFunc,并将data作为参数。

Go语言的官方扩展包为我们提供了一个基于权重的信号量Semaphore,我可以根据信号量来控制一定数量的 goroutine 并发工作,官方也给提供了一个例子:workerPool,代码有点长就不在这里贴了,我们来自己写一个稍微简单点的例子:

package main

import (
	"context"
	"fmt"
	"golang.org/x/sync/semaphore"
	"runtime"
	"sync"
	"time"
)

const (
	// 同时运行的goroutine上限
	Limit = 5
	// 信号量的权重
	Weight = 1
)

func main() {
	start := time.Now()
	names := []int{
		1, 2, 3, 4, 5,
	}

	for i := 1; i <= 100000; i++ {
		names = append(names, i)
	}

	sem := semaphore.NewWeighted(Limit)
	var w sync.WaitGroup
	for _, name := range names {
		w.Add(1)
		go func(name int) {
			sem.Acquire(context.Background(), Weight)
			// ... 具体的业务逻辑

			fmt.Printf("Items is %v, This NumGoroutine is %v\n", name, runtime.NumGoroutine())
			sem.Release(Weight)
			w.Done()
		}(name)
	}
	w.Wait()
	times := time.Since(start)
	fmt.Println(times)
}

上面的例子我们使用 NewWeighted() 函数创建一个并发访问的最大资源数,也就是同时运行的goroutine上限为3,使用Acquire函数来获取指定个数的资源,如果当前没有空闲资源可用,则当前goroutine将陷入休眠状态,最后使用release函数释放已使用资源数量(计数器)进行更新减少,并通知其它 waiters

主要实现原理是利用waitGroup做并发控制,利用channel可以在goroutine之间进行数据通信,通过限制channel的队列长度来控制同时运行的goroutine数量,例子如下:

package main

import (
	"fmt"
	"runtime"
	"sync"
	"time"
)

var (
	// channel长度
	poolCount = 5
	// 复用的goroutine数量
	goroutineCount = 10
)

func main() {
	start := time.Now()
	jobsChan := make(chan int, poolCount)

	// workers
	var wg sync.WaitGroup
	for i := 0; i < goroutineCount; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			for item := range jobsChan {
				fmt.Printf("Items is %v, This NumGoroutine is %v\n", item, runtime.NumGoroutine())
			}
		}()
	}

	// senders
	for i := 0; i < 100000; i++ {
		jobsChan <- i
	}

	// 关闭channel,上游的goroutine在读完channel的内容,就会通过wg的done退出
	close(jobsChan)
	wg.Wait()

	times := time.Since(start)
	fmt.Println(times)
}

这种实现方式真的妙,与信号量的实现方式基本相似,某些场景大家也可以考虑使用这种方式来达到控制goroutine的目的,不过最好封装一下,要不有点丑陋,感兴趣的可以参考:https://github.com/eddycjy/gsema/blob/master/sema.go

本文主要目的是介绍控制goroutine的几种方式、控制goroutine数量的几种方式,goroutine的创建成本低、效率高带来了很大优势,同时也会有一些弊端,这就需要我们在实际开发中根据具体场景选择正确的方式使用goroutine

 

 

标签:fmt,goroutine,Golang,func,time,go,channel
From: https://www.cnblogs.com/chenpingzhao/p/16751559.html

相关文章

  • Django3.2前后端分离(配置)
    创建工程项目1.初始化项目步骤一:安装Django框架,安装前应确保Django的版本为LTS版,关于版本的描述见Django版本文档pipinstalldjango==3.2步骤二:创建django项目,通过d......
  • GOPATH的配置(WIN)
    配置GOPATH环境变量project文件夹在project文件夹新建3个文件夹src存放go语言源码pkg存放编译好的包对象文件bin存放链接好的可执行文件新建项目在src文件夹......
  • Django 框架
    目录​​Django框架​​​​django-restframework框架​​ Django框架 ​​Django框架简介​​​​Django简介以及安装​​​​Django如何自定义漂亮的404页面​​​​Dja......
  • Golang:输出Emoji表情符号
    Emojiisasimplegolangpackage.译文:Emoji是一个简单的golang包。文档​​https://github.com/kyokomi/emoji​​​​https://pkg.go.dev/github.com/kyokomi/emoji/v2​......
  • Golang:第七章 Golang 常量
    常量:程序编译阶段就确定下来的值,程序运行时无法改变该值定义常量//定义的时候就要初始化constconstantName[type]=valueeg:constPIfloat32=3.14//简写......
  • 分享Go书籍-《Go Web编程》
    大家好,我是沙漠尽头的狼。最近几天在看一本Go的书籍,看了100来页,感觉不错,分享给大家​。​书籍基本信息书籍信息:书名:GoWeb编程作者:(新加坡)郑兆雄(SauSheongChang......
  • 分享Go书籍-《Go Web编程》
    大家好,我是沙漠尽头的狼。最近几天在看一本Go的书籍,看了100来页,感觉不错,分享给大家​。​书籍基本信息书籍信息:书名:GoWeb编程作者:(新加坡)郑兆雄(SauSheongChan......
  • Python+Django+Nginx的从0到1的个人网站搭建(二)
    本文介绍阿里云ESC服务器的购买以及项目部署一、服务器购买点此购买再依次点击这里我选择新用户免费试用一个月,系统选择了Ubuntu,其余根据需求自选:记住你的公......
  • nonebot2 使用 nonebot_plugin_gocqhttp 插件如何升级gocq
    在使用nonebot_plugin_gocqhttp这个插件一段时候后发现需要升级gocq这里给出了升级方法的说明:0.需要下载最新gocqhttp版本1.在nonebot2的accounts文件夹中替换旧的.exe......
  • #yyds干货盘点#【愚公系列】2022年10月 Go教学课程 038-异常处理
    一、异常处理1.什么是异常处理异常处理,是编程语言或计算机硬件里的一种机制,用于处理软件或信息系统中出现的异常状况,异常处理的好处是你不用再绞尽脑汁去考虑各种错误,这为......