Tous les langages de programmation performants possèdent des fonctionnalités qui ont fait leur succès. Le point fort de Go est la programmation concurrente. Il a été conçu autour d'un modèle théorique fort (CSP) et fournit une syntaxe de niveau de langage sous la forme du mot clé "go" qui démarre une tâche asynchrone (ouais, le langage porte le nom du mot clé), ainsi qu'une méthode intégrée. communiquer entre tâches concurrentes.
Dans cet article (première partie), je présenterai le modèle CSP que la simultanéité de Go implémente, les goroutines et comment synchroniser le fonctionnement de plusieurs goroutines coopérants. Dans un prochain article (deuxième partie), je parlerai des canaux de Go et de la coordination entre goroutines sans structure de données synchronisée..
CSP est synonyme de communication de processus séquentiels. Il a été introduit pour la première fois par Tony (C. A. R.) Hoare en 1978. CSP est un cadre de haut niveau permettant de décrire des systèmes concurrents. Il est beaucoup plus facile de programmer des programmes simultanés corrects au niveau d'abstraction CSP qu'aux niveaux typiques d'abstraction des threads et des verrous.
Les Goroutines sont un jeu de coroutines. Cependant, ils ne sont pas exactement les mêmes. Un goroutine est une fonction qui est exécutée sur un thread distinct du thread de lancement afin de ne pas le bloquer. Plusieurs goroutines peuvent partager le même thread de système d'exploitation. Contrairement aux coroutines, les goroutines ne peuvent pas explicitement céder le contrôle à un autre goroutine. L'exécution de Go prend en charge le transfert implicite du contrôle lorsqu'un goroutine particulier bloquerait l'accès I / O.
Voyons du code. Le programme Go ci-dessous définit une fonction, appelée de manière créative "f", qui dort de manière aléatoire jusqu’à une demi-seconde, puis affiche son argument. le principale()
la fonction appelle le F()
fonctionner dans une boucle de quatre itérations, où à chaque itération il appelle F()
trois fois avec "1", "2" et "3" dans une rangée. Comme vous vous en doutez, le résultat est le suivant:
--- Exécuter séquentiellement en tant que fonctions normales 1 2 3 1 2 3 1 2 3 1 2 3
Puis principal invoque F()
comme un goroutine dans une boucle similaire. Maintenant, les résultats sont différents car le runtime de Go exécute le F
goroutines simultanément, puis puisque le sommeil aléatoire est différent entre les goroutines, l’impression des valeurs n’a pas lieu dans l’ordre F()
a été invoqué. Voici la sortie:
--- Courir simultanément en tant que goroutines 2 2 3 1 3 2 1 3 1 1 3 2 2 1 3
Le programme lui-même utilise les packages de bibliothèque standard "time" et "math / rand" pour implémenter la veille aléatoire et attendre à la fin que toutes les goroutines soient terminées. Ceci est important car lorsque le thread principal se termine, le programme est terminé, même s'il reste des goroutines en attente.
package principal d'importation ("fmt" "time" "math / rand") var r = rand.New (rand.NewSource (time.Now (). UnixNano ())) func f (chaîne de caractères) // Veille jusqu'à une demi-seconde de retard: = heure.Durée (r.Int ()% 500) * heure.Millisecondes fois.Sommeil (retard) fmt.Println (s) func main () fmt.Println ("--- Exécuté séquentiellement comme fonctions normales ") pour i: = 0; je < 4; i++ f("1") f("2") f("3") fmt.Println("--- Run concurrently as goroutines") for i := 0; i < 5; i++ go f("1") go f("2") go f("3") // Wait for 6 more seconds to let all go routine finish time.Sleep(time.Duration(6) * time.Second) fmt.Println("--- Done.")
Lorsque vous avez un tas de goroutines sauvages partout, vous voulez souvent savoir quand ils ont fini.
Il existe différentes façons de le faire, mais l’une des meilleures approches consiste à utiliser un WaitGroup
. UNE WaitGroup
est un type défini dans le package "sync" qui fournit le Ajouter()
, Terminé()
et Attendre()
opérations. Cela fonctionne comme un compteur qui compte le nombre de routines actives toujours actives et attend qu'elles soient toutes terminées. Chaque fois que vous démarrez un nouveau goroutine, vous appelez Ajouter (1)
(vous pouvez en ajouter plusieurs si vous lancez plusieurs routines go). Quand un goroutine est fait, il appelle Terminé()
, ce qui réduit le nombre d'un, et Attendre()
bloque jusqu'à ce que le compte atteigne zéro.
Convertissons le programme précédent pour utiliser un WaitGroup
au lieu de dormir pendant six secondes au cas où à la fin. Notez que le F()
fonction utilise reporter wg.Done ()
au lieu d'appeler wg.Done ()
directement. Ceci est utile pour assurer wg.Done ()
est toujours appelé, même s’il ya un problème et que le goroutine se termine plus tôt. Sinon, le nombre n'atteindra jamais zéro et wg.Wait ()
peut bloquer pour toujours.
Un autre petit truc est que j'appelle wg.Add (3)
juste une fois avant d'invoquer F()
trois fois. Notez que j'appelle wg.Add ()
même en invoquant F()
comme une fonction régulière. Ceci est nécessaire car F()
appels wg.Done ()
indépendamment du fait qu'il fonctionne en tant que fonction ou goroutine.
package principal d'importation ("fmt" "time" "math / rand" "sync") var r = rand.New (rand.NewSource (time.Now (). UnixNano ())) var wg sync.WaitGroup func f (s chaîne) reporter wg.Done () // Attente d’une demi-seconde de retard: = heure.Durée (r.Int ()% 500) * heure.Millisecondes.Sommeil (retard) fmt.Println (s) func main () fmt.Println ("--- Exécuté séquentiellement comme des fonctions normales") pour i: = 0; je < 4; i++ wg.Add(3) f("1") f("2") f("3") fmt.Println("--- Run concurrently as goroutines") for i := 0; i < 5; i++ wg.Add(3) go f("1") go f("2") go f("3") wg.Wait()
Les goroutines du programme 1,2,3 ne communiquent pas entre elles ni ne fonctionnent sur des structures de données partagées. Dans le monde réel, cela est souvent nécessaire. Le paquet "sync" fournit le type Mutex avec Fermer à clé()
et Ouvrir()
méthodes d’exclusion mutuelle. Un bon exemple est la carte standard Go.
Ce n'est pas synchronisé par conception. Cela signifie que si plusieurs goroutines accèdent simultanément à la même carte sans synchronisation externe, les résultats seront imprévisibles. Mais si toutes les goroutines acceptent d’acquérir un mutex partagé avant chaque accès et le relâchent plus tard, alors l’accès sera sérialisé..
Mettons tout cela ensemble. Le célèbre Tour of Go propose un exercice pour construire un robot d'indexation. Ils fournissent un excellent cadre avec un simulateur de Fetcher et des résultats qui vous permettent de vous concentrer sur le problème à résoudre. Je vous recommande fortement d'essayer de le résoudre vous-même.
J'ai écrit une solution complète en utilisant deux approches: une carte synchronisée et des canaux. Le code source complet est disponible ici.
Voici les parties pertinentes de la solution "sync". Premièrement, définissons une carte avec une structure mutex pour contenir les URL extraites. Notez la syntaxe intéressante dans laquelle un type anonyme est créé, initialisé et affecté à une variable dans une instruction.
var fetchedUrls = struct urls map [chaîne] bool m sync.Mutex urls: make (map [chaîne] bool)
Maintenant, le code peut verrouiller le m
mutex avant d'accéder à la carte des URL et déverrouiller quand c'est fait.
// Vérifiez si cette URL a déjà été extraite (ou en cours d'extraction) fetchedUrls.m.Lock () si fetchedUrls.urls [url] fetchedUrls.m.Unlock () return // OK. Récupérons cette URL fetchedUrls.urls [url] = true fetchedUrls.m.Unlock ()
Ce n’est pas complètement sûr car tout le monde peut accéder au fetchedUrls
variable et oublier de verrouiller ou déverrouiller. Une conception plus robuste fournira une structure de données qui prend en charge des opérations sécurisées en effectuant le verrouillage / déverrouillage automatiquement.
Go supporte parfaitement la concurrence utilisant des goroutines légères. Il est beaucoup plus facile à utiliser que les threads traditionnels. Lorsque vous avez besoin de synchroniser l’accès aux structures de données partagées, Go vous ouvre la porte. sync.Mutex
.
Il y a encore beaucoup à dire sur la simultanéité de Go. Restez à l'écoute…