Η πιο στιβαρή γλώσσα προγραμματισμού όσον αφορά τη συγχρονικότητα

Σύμφωνα με την StackOverflow Developer Survey και τον δείκτη TIOBE, το Go (ή Golang) έχει κερδίσει περισσότερη έλξη, ειδικά μεταξύ των προγραμματιστών backend και των ομάδων DevOps που εργάζονται στον αυτοματισμό υποδομής. Αυτός είναι αρκετός λόγος για να μιλήσουμε για το Go και τον έξυπνο τρόπο αντιμετώπισης του συγχρονισμού.

Το Go είναι γνωστό για την πρώτης κλάσης υποστήριξή του για συγχρονισμό ή για την ικανότητα ενός προγράμματος να αντιμετωπίζει πολλά πράγματα ταυτόχρονα. Η ταυτόχρονη εκτέλεση κώδικα γίνεται πιο κρίσιμο μέρος του προγραμματισμού καθώς οι υπολογιστές μετακινούνται από την εκτέλεση μιας ενιαίας ροής κώδικα πιο γρήγορα στην εκτέλεση περισσότερων ροών ταυτόχρονα.

Ένας προγραμματιστής μπορεί να κάνει το πρόγραμμά του να τρέχει πιο γρήγορα σχεδιάζοντάς το να εκτελείται ταυτόχρονα έτσι ώστε κάθε μέρος του προγράμματος να μπορεί να εκτελείται ανεξάρτητα από τα άλλα. Τρεις λειτουργίες στο Go, γορουτίνες, κανάλια και επιλογές, διευκολύνουν τη συγχρονικότητα όταν συνδυάζονται μαζί.

Οι γορουτίνες λύνουν το πρόβλημα της ταυτόχρονης εκτέλεσης κώδικα σε ένα πρόγραμμα και τα κανάλια λύνουν το πρόβλημα της ασφαλούς επικοινωνίας μεταξύ κώδικα που εκτελείται ταυτόχρονα.

Οι γκορουτίνες είναι αναμφίβολα ένα από τα καλύτερα χαρακτηριστικά του Go! Είναι πολύ ελαφριές, όχι όπως τα νήματα του λειτουργικού συστήματος, αλλά μάλλον εκατοντάδες Γκρουτίνες μπορούν να πολλαπλασιαστούν σε ένα νήμα λειτουργικού συστήματος (το Go έχει τον προγραμματιστή χρόνου εκτέλεσης για αυτό) με ένα ελάχιστο κόστος αλλαγής περιβάλλοντος! Με απλά λόγια, οι γορουτίνες είναι μια ελαφριά και φθηνή αφαίρεση πάνω από τα νήματα.

Αλλά πώς λειτουργεί η προσέγγιση ταυτόχρονης χρήσης του Go κάτω από την κουκούλα; Σήμερα, θέλω να προσπαθήσω να σας το εξηγήσω αυτό. Αυτό το άρθρο εστιάζει περισσότερο στην ενορχήστρωση των οντοτήτων συγχρονισμού του Go παρά σε αυτές τις ίδιες τις οντότητες. Επομένως, δεν θα βασιστούμε σε πάρα πολλά αποσπάσματα κώδικα σήμερα.

Go Runtime Scheduler

Για να πούμε, η δουλειά του είναι να διανέμει εκτελούμενες γορουτίνες (G) σε πολλαπλά νήματα λειτουργικού λειτουργικού συστήματος (M) που τρέχουν σε έναν ή περισσότερους επεξεργαστές (P). Οι επεξεργαστές χειρίζονται πολλαπλά νήματα. Τα νήματα χειρίζονται πολλαπλές γορουτίνες. Οι επεξεργαστές εξαρτώνται από το υλικό. ο αριθμός των επεξεργαστών ορίζεται στον αριθμό των πυρήνων της CPU σας.

  • G=Gorutine
  • M = Νήμα λειτουργικού συστήματος
  • P = Επεξεργαστής

Όταν δημιουργείται μια νέα γορουτίνα ή όταν μια υπάρχουσα γορουτίνα γίνεται εκτελούμενη, προωθείται σε μια λίστα με τρεχούμενες γορουτίνες του τρέχοντος επεξεργαστή. Όταν ο επεξεργαστής ολοκληρώσει την εκτέλεση μιας γορουτίνας, προσπαθεί πρώτα να εμφανίσει μια γορουτίνα από τη λίστα με τις εκτελούμενες γορουτίνες. Εάν η λίστα είναι κενή, ο επεξεργαστής επιλέγει έναν τυχαίο επεξεργαστή και προσπαθεί να κλέψει τις μισές από τις γορουτίνες με δυνατότητα εκτέλεσης.

Τι είναι η Γκορουτίνα;

Οι γορουτίνες είναι συναρτήσεις που εκτελούνται ταυτόχρονα με άλλες συναρτήσεις. Οι γορουτίνες μπορούν να θεωρηθούν ως ελαφριές κλωστές πάνω από ένα νήμα του λειτουργικού συστήματος. Το κόστος δημιουργίας ενός Goroutine είναι μικρό σε σύγκριση με ένα νήμα. Ως εκ τούτου, είναι σύνηθες για τις εφαρμογές Go να εκτελούνται χιλιάδες Goroutine ταυτόχρονα.

Οι γορουτίνες πολυπλέκονται σε μικρότερο αριθμό νημάτων του λειτουργικού συστήματος. Μπορεί να υπάρχει μόνο ένα νήμα σε ένα πρόγραμμα με χιλιάδες γορουτίνες. Εάν οποιαδήποτε Goroutine σε αυτό το νήμα μπλοκ λέει αναμονή για είσοδο χρήστη, τότε δημιουργείται ένα άλλο νήμα του λειτουργικού συστήματος ή τραβιέται ένα νήμα σταθμευμένο (αδρανές) και οι υπόλοιπες Goroutine μετακινούνται στο νήμα του λειτουργικού συστήματος που δημιουργήθηκε ή δεν έχει παρκάρει. Όλα αυτά φροντίζει ο προγραμματιστής χρόνου εκτέλεσης της Go. Μια γορουτίνα έχει τρεις καταστάσεις: τρέξιμο, τρέξιμο και μη τρέξιμο.

Goroutines vs. Threads

Γιατί να μην χρησιμοποιήσετε απλά νήματα λειτουργικού συστήματος όπως κάνει ήδη η Go; Αυτή είναι μια δίκαιη ερώτηση. Όπως αναφέρθηκε παραπάνω, οι Goroutines εκτελούνται ήδη πάνω από τα νήματα του λειτουργικού συστήματος. Αλλά η διαφορά είναι ότι πολλαπλές Goroutine τρέχουν σε μεμονωμένα νήματα λειτουργικού συστήματος.

Η δημιουργία μιας γορουτίνας δεν απαιτεί πολλή μνήμη, μόνο 2 kB χώρου στοίβας. Αναπτύσσονται κατανέμοντας και ελευθερώνοντας αποθήκευση σωρού όπως απαιτείται. Συγκριτικά, τα νήματα ξεκινούν από έναν πολύ μεγαλύτερο χώρο, μαζί με μια περιοχή μνήμης που ονομάζεται σελίδα φύλαξης που λειτουργεί ως φύλακας μεταξύ της μνήμης του ενός νήματος και του άλλου.

Οι γορουτίνες δημιουργούνται εύκολα και καταστρέφονται κατά το χρόνο εκτέλεσης, αλλά τα νήματα έχουν μεγάλο κόστος εγκατάστασης και αποκοπής. πρέπει να ζητήσει πόρους από το λειτουργικό σύστημα και να τους επιστρέψει μόλις ολοκληρωθεί.

Ο χρόνος εκτέλεσης κατανέμεται σε μερικά νήματα στα οποία πολυπλέκονται όλες οι γορουτίνες. Σε οποιαδήποτε χρονική στιγμή, κάθε νήμα θα εκτελεί μια γορουτίνα. Εάν αυτή η γορουτίνα είναι αποκλεισμένη (κλήση συνάρτησης, syscall, κλήση δικτύου κ.λπ.), θα αντικατασταθεί με μια άλλη γορουτίνα που θα εκτελεστεί σε αυτό το νήμα.

Συνοπτικά, το Go χρησιμοποιεί Goroutines και Threads, και και τα δύο είναι απαραίτητα στον συνδυασμό της ταυτόχρονης εκτέλεσης συναρτήσεων. Αλλά το Go χρησιμοποιώντας Goroutines κάνει το Go μια πολύ καλύτερη γλώσσα προγραμματισμού από ό,τι φαίνεται στην αρχή.

Γορουτίνες Ουρές

Το Go διαχειρίζεται γορουτίνες σε δύο επίπεδα, τοπικές ουρές και καθολικές ουρές. Σε κάθε επεξεργαστή συνδέονται τοπικές ουρές, ενώ η καθολική ουρά είναι κοινή.

Οι γορουτίνες δεν μπαίνουν στην καθολική ουρά μόνο όταν η τοπική ουρά είναι γεμάτη και επίσης προωθούνται σε αυτήν όταν η Go εισάγει μια λίστα με γορουτίνες στον προγραμματιστή, π.χ. από τον δημοσκόπηση δικτύου ή τις γορουτίνες που κοιμούνται κατά τη συλλογή σκουπιδιών.

Κλοπή Εργασίας

Όταν ένας επεξεργαστής δεν διαθέτει Goroutines, εφαρμόζει τους ακόλουθους κανόνες με αυτήν τη σειρά:

  • τραβήξτε την εργασία από την τοπική ουρά
  • τραβήξτε την εργασία από το poler δικτύου
  • κλέψουν εργασία από την τοπική ουρά του άλλου επεξεργαστή
  • τραβήξτε την εργασία από την παγκόσμια ουρά

Δεδομένου ότι ένας επεξεργαστής μπορεί να τραβήξει την εργασία από την καθολική ουρά όταν εξαντληθούν οι εργασίες, το πρώτο διαθέσιμο P θα εκτελέσει το goroutine. Αυτή η συμπεριφορά εξηγεί γιατί μια γορουτίνα εκτελείται σε διαφορετικό P και δείχνει πώς το Go βελτιστοποιεί το σύστημα αφήνοντας άλλες γορουτίνες να εκτελούνται όταν ένας πόρος είναι ελεύθερος.

Σε αυτό το διάγραμμα, μπορείτε να δείτε ότι το P1 τελείωσε τις γορουτίνες. Έτσι, ο προγραμματιστής χρόνου εκτέλεσης του Go θα παίρνει γορουτίνες από άλλους επεξεργαστές. Εάν κάθε άλλη ουρά εκτέλεσης του επεξεργαστή είναι κενή, ελέγχει για ολοκληρωμένα αιτήματα IO (syscalls, αιτήματα δικτύου) από το netpoller. Εάν αυτό το netpoller είναι άδειο, ο επεξεργαστής θα προσπαθήσει να πάρει γορουτίνες από την παγκόσμια ουρά εκτέλεσης.

Εκτέλεση και εντοπισμός σφαλμάτων

Σε αυτό το απόσπασμα κώδικα, δημιουργούμε 20 συναρτήσεις goroutine. Ο καθένας θα κοιμηθεί για ένα δευτερόλεπτο και μετά θα μετρήσει μέχρι το 1e10 (10.000.000.000). Ας διορθώσουμε το πρόγραμμα Go Scheduler ορίζοντας το env σε GODEBUG=schedtrace=1000.

Κώδικας

Αποτελέσματα

Τα αποτελέσματα δείχνουν τον αριθμό των γορουτινών στην καθολική ουρά με runqueue και τις τοπικές ουρές (αντίστοιχα P0 και P1) στην αγκύλη [5 8 3 0]. Όπως μπορούμε να δούμε με το χαρακτηριστικό grow, όταν η τοπική ουρά φτάσει τα 256 σε αναμονή γορουτίνες, οι επόμενες θα στοιβάζονται στην καθολική ουρά.

  • gomaxprocs: Οι επεξεργαστές διαμορφώθηκαν
  • idleprocs: Οι επεξεργαστές δεν χρησιμοποιούνται. Γκορουτίνα τρέξιμο.
  • threads: Νήματα σε χρήση.
  • idlethreads: Τα νήματα δεν χρησιμοποιούνται.
  • runqueue: Γορουτίνες στην παγκόσμια ουρά.
  • [1 0 0 0]: Γορουτίνες στην ουρά τοπικής εκτέλεσης κάθε επεξεργαστή.
idleprocs=1 threads=6 idlethreads=0 runqueue=0 [1 0 0 0]
idleprocs=2 threads=3 idlethreads=0 runqueue=0 [0 0 0 0]
idleprocs=4 threads=9 idlethreads=2 runqueue=0 [0 0 0 0]
idleprocs=0 threads=5 idlethreads=0 runqueue=0 [5 8 3 0]
idleprocs=4 threads=9 idlethreads=2 runqueue=0 [0 0 0 0]
idleprocs=0 threads=5 idlethreads=0 runqueue=8 [2 2 1 3]
idleprocs=4 threads=9 idlethreads=2 runqueue=0 [0 0 0 0]
idleprocs=0 threads=5 idlethreads=0 runqueue=10 [3 1 0 2]
idleprocs=4 threads=9 idlethreads=2 runqueue=0 [0 0 0 0]
idleprocs=0 threads=5 idlethreads=0 runqueue=9 [4 0 3 0]
idleprocs=4 threads=9 idlethreads=2 runqueue=0 [0 0 0 0]
idleprocs=0 threads=5 idlethreads=0 runqueue=10 [2 1 1 2]
idleprocs=4 threads=9 idlethreads=2 runqueue=0 [0 0 0 0]
idleprocs=0 threads=5 idlethreads=0 runqueue=6 [2 1 0 0]

Ευχαριστώ που διαβάσατε το άρθρο μου σχετικά με τη συγχρονία του Go. Ελπίζω να μπορούσατε να μάθετε κάτι νέο.

Στην υγειά σας!