Introduction aux files d'attente de tâches en Go
\nLes files d'attente de tâches (task queues) sont l'un des modèles clés dans le développement d'applications à forte charge. Elles permettent d'organiser le traitement asynchrone des données, de répartir la charge et d'améliorer la tolérance aux pannes du système. En langage Go, grâce aux mécanismes de concurrence intégrés — goroutines et canaux — l'implémentation des files d'attente devient incroyablement élégante et performante.
\nDans cet article, nous allons voir comment créer des files d'attente de tâches en Go à partir de zéro. Vous apprendrez ce que sont les goroutines et les canaux, comment organiser un pool de workers (worker pool) et traiter les tâches en parallèle. Le contenu est destiné aux développeurs débutants familiers avec les bases de la syntaxe Go.
\n\nLes bases : goroutines et canaux
\nAvant de passer aux files d'attente, il est important de comprendre deux concepts fondamentaux de Go :
\n- \n
- Goroutine — un fil d'exécution léger. Il est lancé avec le mot-clé
goavant l'appel d'une fonction. \n - Canal (channel) — un « canal » typé pour transférer des données entre goroutines. Il permet de synchroniser leur travail. \n
Exemple d'une goroutine et d'un canal simples :
\npackage main\n\nimport "fmt"\n\nfunc main() {\n ch := make(chan string)\n\n go func() {\n ch <- "Bonjour depuis la goroutine !"\n }()\n\n msg := <-ch\n fmt.Println(msg)\n}\n\nDans cet exemple, nous avons créé un canal ch, lancé une goroutine anonyme qui envoie un message dans ce canal, et la goroutine principale a reçu ce message. C'est sur ce mécanisme que reposent les files d'attente de tâches.
Implémentation d'une file d'attente de tâches simple
\nLa file d'attente de tâches la plus simple est un canal dans lequel nous plaçons des tâches, et une ou plusieurs goroutines qui les traitent. Regardons un exemple de base :
\npackage main\n\nimport (\n "fmt"\n "time"\n)\n\nfunc worker(id int, tasks <-chan int) {\n for task := range tasks {\n fmt.Printf("Worker %d a commencé la tâche %d\\n", id, task)\n time.Sleep(time.Second) // simulation de travail\n fmt.Printf("Worker %d a terminé la tâche %d\\n", id, task)\n }\n}\n\nfunc main() {\n tasks := make(chan int, 10) // canal tamponné\n\n // lancement d'un worker\n go worker(1, tasks)\n\n // envoi de 5 tâches\n for i := 1; i <= 5; i++ {\n tasks <- i\n }\n\n close(tasks) // fermeture du canal\n time.Sleep(2 * time.Second) // laisser le temps de traiter\n}\n\nIci, worker est une fonction qui lit depuis le canal tasks. Le canal est tamponné (capacité 10), donc nous pouvons envoyer plusieurs tâches sans blocage. Après la fermeture du canal, la boucle for range se termine automatiquement.
Notez : nous avons utilisé <-chan int — un canal en lecture seule. C'est une bonne pratique qui indique clairement que le worker ne doit pas envoyer de données dans ce canal.
Canaux tamponnés et non tamponnés
\nDans l'exemple ci-dessus, nous avons utilisé un canal tamponné (make(chan int, 10)). La différence entre les types de canaux :
- \n
- Canal non tamponné — l'envoi est bloqué jusqu'à ce que le récepteur lise les données. Idéal pour la synchronisation. \n
- Canal tamponné — l'envoi est bloqué uniquement lorsque le tampon est plein. Permet d'accumuler les tâches et de réduire les latences. \n
Pour les files d'attente de tâches, on utilise généralement des canaux tamponnés afin que le producteur puisse envoyer rapidement une tâche et continuer son travail sans attendre son exécution.
\n\nPool de workers (Worker Pool) en Go
\nUn worker, c'est bien, mais plusieurs, c'est mieux. Un pool de workers permet de traiter les tâches en parallèle, en utilisant tous les cœurs du processeur. Voici comment l'implémenter :
\npackage main\n\nimport (\n "fmt"\n "sync"\n "time"\n)\n\nfunc worker(id int, tasks <-chan int, wg *sync.WaitGroup) {\n defer wg.Done()\n fo