1.什么是go的高并发
在Go语言中提到的“高并发”通常指的是Go的能力,通过其轻量级线程机制——协程(goroutines)——以及其他并发原语,如通道(channels)和WaitGroups,来同时处理大量的并发任务或客户端请求。Go语言的这一特性使得它在构建网络服务和分布式系统方面尤其受欢迎。 以下是Go语言处理高并发的一些关键点与工具:
- 协程(Goroutines): Go的协程是轻量级的线程。创建一个新的协程非常廉价,只需消耗几kB的栈空间,并且栈的大小会按需增长和缩小。这意味着在同一个程序中可以轻松地启动数以万计的协程。
- 通道(Channels): 通道是Go语言中的一种内置类型,它提供了协程之间通信和同步的方式。可以通过通道在协程之间安全地传递消息。通道可以是缓冲的或无缓冲的,从而提供了不同的同步模型。
- 同步原语: Go的
sync
包提供了基本的同步原语,如互斥锁(Mutex)、WaitGroups等。WaitGroups用于等待一组协程完成,Mutex则用于保护共享资源以避免并发访问时的冲突。 - Select语句: Go的select语句让你可以等待多个通道操作,并在准备就绪时处理它们。这是处理多个并发通道时的强大工具。
- 运行时调度: Go的运行时包含了自己的内部调度器,它利用M:N调度模型(其中M是协程数,N是OS线程数)来实现高效的并发。调度器会自动在可用的系统线程上分配和管理协程。 正因为这些特性和工具,Go语言特别适合高并发的场景,例如Web服务器、微服务、实时通信系统、网络游戏服务器等,可以有效地利用现代多核CPU的硬件能力。在设计系统时,你只需考虑逻辑上的并发处理,而不必担心底层线程管理和复杂的同步机制。这使得开发高并发应用变得更简单、更清晰。
2.如何指定协程数量
package main
import (
"fmt"
"sync"
)
func main() {
const maxGoroutines = 5 // 想要同时运行的协程的最大数量
const numJobs = 20 // 任务总数
// 创建一个缓冲信道,容量为maxGoroutines
goroutinePool := make(chan struct{}, maxGoroutines)
var wg sync.WaitGroup // WaitGroup用于等待所有的任务完成
for i := 0; i < numJobs; i++ {
goroutinePool <- struct{}{} // 如果信道满了,会阻塞在这里
wg.Add(1)
// 启动一个协程
go func(jobId int) {
defer wg.Done() // 任务完成时调用Done
defer func() { <-goroutinePool }() // 释放信道的位置
// 这里可以执行实际的并发任务
fmt.Printf("Starting job %d", jobId)
// ... do some work ...
fmt.Printf("Finished job %d", jobId)
}(i)
}
wg.Wait() // 等待所有任务完成
fmt.Println("All jobs have been processed.")
}
在上面的代码中,我们创建了一个名为goroutinePool
的缓冲信道,这个缓冲信道的容量是我们想要的协程池的大小。通过控制向缓冲信道发送空结构体的数量,我们可以限制同时运行的协程数量。当一个协程启动时,它会尝试将一个空结构体发送到goroutinePool
信道。如果信道已满(即已达到我们设置的协程池容量),它会阻塞,直到信道有空位。当任务完成时,协程执行<-goroutinePool
操作来从信道中移除一个元素,为其他等待启动的协程腾出空间。
此外,我们使用了sync.WaitGroup
来等待所有的协程都完成它们的工作。这样做可确保程序在所有协程都完成它们的任务之前不会退出。
在上面这段代码中,const maxGoroutines = 5
定义了我们想要在任一时刻内同时运行的最大协程数量。而const numJobs = 20
指定了总共需要处理的任务数量。每个任务都是一个简单的打印任务,实际应用中你可以替换成任何需要并发执行的逻辑。
3.封装成公共方法
package concurrent
import (
"runtime/debug"
"support/logger"
"sync"
)
type syncErrList struct {
errs []error
mu sync.RWMutex
}
func (l *syncErrList) appendError(err error) {
.Lock()
l.errs = append(l.errs, err)
.Unlock()
}
func (l *syncErrList) listErrors() []error {
.RLock()
defer .RUnlock()
return l.errs
}
// GoLimit 限制一个同步任务中并发的协程数
type GoLimit struct {
c chan struct{}
wg *sync.WaitGroup
errs *syncErrList
}
func NewGoLimit(size int) *GoLimit {
return &GoLimit{
c: make(chan struct{}, size),
wg: &sync.WaitGroup{},
errs: &syncErrList{},
}
}
func (g *GoLimit) Run(f func()) *GoLimit {
g.wg.Add(1)
g.c <- struct{}{}
go func() {
defer func() {
if err := recover(); err != nil {
logger.Error("GoLimit Run panic as: %s\n%s", err, debug.Stack())
}
g.wg.Done()
<-g.c
}()
f()
}()
return g
}
func (g *GoLimit) RunError(f func() error) *GoLimit {
g.wg.Add(1)
g.c <- struct{}{}
go func() {
defer func() {
if err := recover(); err != nil {
logger.Error("GoLimit RunError panic as: %s\n%s", err, debug.Stack())
}
g.wg.Done()
<-g.c
}()
if err := f(); err != nil {
g.errs.appendError(err)
}
}()
return g
}
func (g *GoLimit) Wait() {
g.wg.Wait()
}
func (g *GoLimit) ListErrors() []error {
return g.errs.listErrors()
}
func (g *GoLimit) FirstError() error {
errs := g.errs.listErrors()
if len(errs) > 0 {
return errs[0]
} else {
return nil
}
}
4.运用场景
func (o *ossWrapper) CopyFolder(remoteDir string, remoteDistDir string) error {
contents, err := o.ListObjects(remoteDir)
if err != nil {
return err
}
goLimit := concurrent.NewGoLimit(goLimitCount)
for _, content := range contents {
contentTmp := content
if !strings.HasSuffix(contentTmp.Key, "/") {
var f = func() {
subfix := strings.Replace(contentTmp.Key, remoteDir, "", 1)
_ = o.CopyObject(contentTmp.Key, util.JoinPath(remoteDistDir, subfix))
}
goLimit.Run(f)
}
}
goLimit.Wait()
return nil
}
func (o *ossWrapper) MoveFolder(remoteDir string, remoteDistDir string) error {
contents, err := o.ListObjects(remoteDir)
if err != nil {
return err
}
goLimit := concurrent.NewGoLimit(goLimitCount)
for _, content := range contents {
if !strings.HasSuffix(content.Key, "/") {
contentTmp := content
goLimit.Run(func() {
subfix := strings.Replace(contentTmp.Key, remoteDir, "", 1)
_ = o.CopyObject(contentTmp.Key, util.JoinPath(remoteDistDir, subfix))
_ = o.DeleteObject(contentTmp.Key)
})
}
}
goLimit.Wait()
return nil
}
在上面的例子中可以清楚的看到在进行目录操作时使用多协程操作可以增加程序运行效率
5.代码分析
- c是进行go协程数量的控制,wg控制同步,errs是保存协程出错的error
1.启动协程运用的recover,如果程序panic会恢复, 没启动一个协程管道c写入一个数据,当这个有缓冲的管道满了后,就等待,因此可以控制最大协程数目,waitGroup是控制所有的协程结束,才会真正的结束。
1.通过waitGroup的add done wait方法实现同步。只有文件夹的所有文件都处理,该函数才算完成。
6.单元测试
package concurrent
import (
"fmt"
"/pkg/errors"
"strconv"
"testing"
)
func TestGoLimit_RunError(t *testing.T) {
limit := NewGoLimit(10)
for i := 0; i < 10; i++ {
ti := i
limit.RunError(func() error {
return errors.New(strconv.Itoa(ti))
})
}
limit.Wait()
errs := limit.ListErrors()
fmt.Println(errs)
firstErr := limit.FirstError()
fmt.Println(firstErr)
if len(errs) != 10 {
t.Error("length of errs should be 10\n")
}
if errs[0] != firstErr {
t.Errorf("errs[0] should equal firstErr errs[0]: %v, firsrErr: %v\n", errs[0], firstErr)
}
}