La concurrence est devenue une fonctionnalité essentielle dans les langages de programmation modernes. La plupart des langages de programmation disposent désormais d'une méthode pour obtenir la concurrence.
Certaines de ces implémentations sont très puissantes et peuvent déplacer la charge vers différents threads système, comme Java, etc. ; certaines simulent ce comportement sur le même thread, comme Ruby, etc.
Le modèle de concurrence de Golang est très puissant, appelé CSP (Communicating Sequential Process), qui divise un problème en processus séquentiels plus petits, puis planifie des instances de ces processus (appelées Goroutines). Ces processus communiquent en transmettant des informations via des canaux.
Dans cet article, nous explorerons comment tirer parti de la concurrence de Golang et comment l'utiliser dans WorkerPool. Dans le deuxième article de la série, nous explorerons comment créer une solution de concurrence puissante.
Supposons que nous devions appeler une interface API externe et que l'ensemble du processus prend 100 ms. Si nous devons appeler cette interface 1000 fois de manière synchrone, cela prendra 100 secondes.
//// model/data.go package model type SimpleData struct { ID int } //// basic/basic.go package basic import ( "fmt" "github.com/Joker666/goworkerpool/model" "time" ) func Work(allData []model.SimpleData) { start := time.Now() for i, _ := range allData { Process(allData[i]) } elapsed := time.Since(start) fmt.Printf("Took ===============> %s\n", elapsed) } func Process(data model.SimpleData) { fmt.Printf("Start processing %d\n", data.ID) time.Sleep(100 * time.Millisecond) fmt.Printf("Finish processing %d\n", data.ID) } //// main.go package main import ( "fmt" "github.com/Joker666/goworkerpool/basic" "github.com/Joker666/goworkerpool/model" "github.com/Joker666/goworkerpool/worker" ) func main() { // Prepare the data var allData []model.SimpleData for i := 0; i < 1000; i++ { data := model.SimpleData{ ID: i } allData = append(allData, data) } fmt.Printf("Start processing all work \n") // Process basic.Work(allData) }
Start processing all work Took ===============> 1m40.226679665s
Le code ci-dessus crée le package modèle, qui contient une structure qui n'a qu'un seul membre de type int. Nous traitons les données de manière synchrone, ce qui n'est évidemment pas optimal puisque ces tâches peuvent être traitées simultanément. Changeons la solution et utilisons goroutine et canal pour la gérer.
//// worker/notPooled.go func NotPooledWork(allData []model.SimpleData) { start := time.Now() var wg sync.WaitGroup dataCh := make(chan model.SimpleData, 100) wg.Add(1) go func() { defer wg.Done() for data := range dataCh { wg.Add(1) go func(data model.SimpleData) { defer wg.Done() basic.Process(data) }(data) } }() for i, _ := range allData { dataCh <- allData[i] } close(dataCh) wg.Wait() elapsed := time.Since(start) fmt.Printf("Took ===============> %s\n", elapsed) } //// main.go // Process worker.NotPooledWork(allData)
Start processing all work Took ===============> 101.191534ms
Dans le code ci-dessus, nous créons un canal de cache d'une capacité de 100 et poussons les données dans le canal via NoPooledWork(). Une fois que la longueur du canal atteint 100, nous ne pouvons pas y ajouter d'éléments tant qu'un élément n'est pas lu. Utilisez for range pour lire le canal et générer un traitement goroutine. Ici, nous n'avons aucune limite sur le nombre de goroutines générées, qui peuvent gérer autant de tâches que possible. En théorie, autant de données que possible peuvent être traitées compte tenu des ressources requises. En exécutant le code, il n’a fallu que 100 ms pour effectuer 1 000 tâches. C'est fou ! Pas entièrement, lisez la suite.
À moins que nous ayons toutes les ressources sur terre, il n'y a qu'un nombre limité de ressources qui peuvent être allouées à un moment donné. La mémoire minimale occupée par une goroutine est de 2k, mais elle peut aussi atteindre 1G. La solution ci-dessus consistant à exécuter toutes les tâches simultanément, en supposant un million de tâches, épuisera rapidement la mémoire et le processeur de la machine. Nous devons soit mettre à niveau la configuration de la machine, soit trouver d'autres meilleures solutions.
计算机科学家很久之前就考虑过这个问题,并提出了出色的解决方案 - 使用 Thread Pool 或者 Worker Pool。这个方案是使用 worker 数量受限的工作池来处理任务,workers 会按顺序一个接一个处理任务,这样就避免了 CPU 和内存使用急速增长。
我们通过实现 worker pool 来修复之前遇到的问题。
//// worker/pooled.go func PooledWork(allData []model.SimpleData) { start := time.Now() var wg sync.WaitGroup workerPoolSize := 100 dataCh := make(chan model.SimpleData, workerPoolSize) for i := 0; i < workerPoolSize; i++ { wg.Add(1) go func() { defer wg.Done() for data := range dataCh { basic.Process(data) } }() } for i, _ := range allData { dataCh <- allData[i] } close(dataCh) wg.Wait() elapsed := time.Since(start) fmt.Printf("Took ===============> %s\n", elapsed) } //// main.go // Process worker.PooledWork(allData)
Start processing all work Took ===============> 1.002972449s
上面的代码,worker 数量限制在 100,我们创建了相应数量的 goroutine 来处理任务。我们可以把 channel 看作是队列,worker goroutine 看作是消费者。多个 goroutine 可以监听同一个 channel,但是 channel 里的每一个元素只会被处理一次。
Go 语言的 channel 可以当作队列使用。
这是一个比较好的解决方案,执行代码,我们看到完成所有任务花费 1s。虽然没有 100ms 这么快,但已经能满足业务需要,而且我们得到了一个更好的解决方案,能将负载均摊在不同的时间片上。
我们能做的还没完。上面看起来是一个完整的解决方案,但却不是的,我们没有处理错误情况。所以需要模拟出错的情形,并且看下我们需要怎么处理。
//// worker/pooledError.go func PooledWorkError(allData []model.SimpleData) { start := time.Now() var wg sync.WaitGroup workerPoolSize := 100 dataCh := make(chan model.SimpleData, workerPoolSize) errors := make(chan error, 1000) for i := 0; i < workerPoolSize; i++ { wg.Add(1) go func() { defer wg.Done() for data := range dataCh { process(data, errors) } }() } for i, _ := range allData { dataCh <- allData[i] } close(dataCh) wg.Add(1) go func() { defer wg.Done() for { select { case err := <-errors: fmt.Println("finished with error:", err.Error()) case <-time.After(time.Second * 1): fmt.Println("Timeout: errors finished") return } } }() defer close(errors) wg.Wait() elapsed := time.Since(start) fmt.Printf("Took ===============> %s\n", elapsed) } func process(data model.SimpleData, errors chan<- error) { fmt.Printf("Start processing %d\n", data.ID) time.Sleep(100 * time.Millisecond) if data.ID % 29 == 0 { errors <- fmt.Errorf("error on job %v", data.ID) } else { fmt.Printf("Finish processing %d\n", data.ID) } } //// main.go // Process worker.PooledWorkError(allData)
我们修改了 process() 函数,处理一些随机的错误并将错误 push 到 errors chnanel 里。所以,为了处理并发出现的错误,我们可以使用 errors channel 保存错误数据。在所有任务处理完成之后,可以检查错误 channel 是否有数据。错误 channel 里的元素保存了任务 ID,方便需要的时候再处理这些任务。
比之前没处理错误,很明显这是一个更好的解决方案。但我们还可以做得更好,
我们将在下篇文章讨论如何编写一个强大的 worker pool 包,并且在 worker 数量受限的情况下处理并发任务。
Go 语言的并发模型足够强大给力,只需要构建一个 worker pool 就能很好地解决问题而无需做太多工作,这就是它没有包含在标准库中的原因。但是,我们自己可以构建一个满足自身需求的方案。很快,我会在下一篇文章中讲到,敬请期待!
Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!