Θα συζητήσουμε πώς η χρήση του RWMutex έναντι του Mutex μπορεί να βελτιώσει σημαντικά την απόδοση.
Εισαγωγή
Δεν είναι επιθυμητή η πρόσβαση πολλών νημάτων στην ίδια μνήμη ταυτόχρονα. Στο Golang μπορούμε να έχουμε πολλές διαφορετικές γορουτίνες που όλες έχουν πιθανώς πρόσβαση στις ίδιες μεταβλητές μνήμης, κάτι που μπορεί να οδηγήσει σε κατάσταση αγώνα. Mutex, αμοιβαίος αποκλεισμός, μαζί με ομάδες αναμονής για αποφυγή συνθηκών αγώνα. Μπορούν να χρησιμοποιηθούν Mutex και RWmutex του golang.
Mutex v/s RWmutex
Το Mutex στο πρώτο είναι οκτώ byte. Το RWMutex είναι είκοσι τέσσερα byte. Από τα επίσημα έγγραφα: Το RWMutex είναι ένα κλείδωμα αμοιβαίου αποκλεισμού αναγνώστη/συγγραφέα. Η κλειδαριά μπορεί να κρατηθεί από έναν αυθαίρετο αριθμό αναγνωστών ή έναν μόνο συγγραφέα. Η μηδενική τιμή για ένα RWMutex είναι ένα ξεκλείδωτο mutex.
// Mutex v/s RWMutex //Mutextype Mutex struct { state int32 sema uint32 }
// RWMutextype RWMutex struct { w Mutex // held if there are pending writers writerSem uint32 // semaphore for writers to wait for completing readers readerSem uint32 // semaphore for readers to wait for completing writers readerCount int32 // number of pending readers readerWait int32 // number of departing readers }
Με λίγα λόγια, οι αναγνώστες δεν υποχρεούνται να περιμένουν ο ένας τον άλλον. Απλώς πρέπει να περιμένουν τους συγγραφείς που κρατούν την κλειδαριά. Δεδομένου ότι μια λειτουργία απλής ανάγνωσης δεν αλλάζει τα περιεχόμενα του αρχείου, είναι αποδεκτό να επιτρέπεται σε πολλούς αναγνώστες να διαβάζουν το ίδιο αρχείο ταυτόχρονα για να βελτιωθεί η απόδοση του προγράμματος. Αλλά η εγγραφή αλλάζει το περιεχόμενο του αρχείου, απαιτείται αμοιβαία αποκλειστική πρόσβαση. Διαφορετικά, θα συνέβαιναν υπερβολικά μεγάλα λάθη. Ένα sync.RWMutex είναι επομένως κατάλληλο για δεδομένα που διαβάζονται σε μεγάλο βαθμό και ο πόρος που αποθηκεύεται σε ένα sync.Mutex είναι χρόνος.
Κάθε λειτουργία εγγραφής που προστατεύεται από ένα RWMutex είναι O(readers).
Συγκριτική αξιολόγηση
Ο κώδικας εκτελέστηκε 10 φορές σε ένα σύστημα Windows με τσιπ intel i5 και η απόδοση ήταν ως η μέση τιμή για την ολοκλήρωση της ταυτόχρονης λειτουργίας ανάγνωσης-εγγραφής.
Κωδικός & Έξοδος:
package main import ( "fmt" "math/rand" "sync" "time" //"os" ) var a float64 = 0.0 var b float64 = 0.0 func done() { r := rand.New(rand.NewSource(99)) lst1 := []int{} lst2 := []int{} lock1 := sync.Mutex{} lock2 := sync.RWMutex{} wtgrp1 := sync.WaitGroup{} wtgrp2 := sync.WaitGroup{} for i := 0; i < 10; i++ { lst1 = append(lst1, i) lst2 = append(lst2, i) } // 1. spawn 1000 goroutines and synchronize through locks now1 := time.Now() for i := 0; i < 1000; i++ { wtgrp1.Add(1) pos := r.Intn(10) go func() { defer wtgrp1.Done() lock1.Lock() defer lock1.Unlock() goLst := []int{} // goLst, destroy automatically // Process each element of lst1 randomly for j := 0; j < 1000; j++ { goLst = append(goLst, lst1[pos]) } }() } wtgrp1.Wait() diff1 := time.Now().Sub(now1) // 2. spawn 1000 goroutines and synchronize through read-write locks now2 := time.Now() for i := 0; i < 1000; i++ { wtgrp2.Add(1) pos := r.Intn(10) go func() { defer wtgrp2.Done() lock2.RLock() defer lock2.RUnlock() goLst := []int{} // goLst, destroy automatically // Process each element of lst2 randomly for j := 0; j < 1000; j++ { goLst = append(goLst, lst1[pos]) } }() } wtgrp2.Wait() diff2 := time.Now().Sub(now2) fmt.Println(diff1, " ", diff2) a = a + float64(diff1) b = b + float64(diff2) } func main() { fmt.Println("Mutex RWMutex") for i := 0; i < 10; i++ { done() time.Sleep(2 * time.Second) } fmt.Println(a, " ", b) fmt.Println(b / a) fmt.Println("Faster by : ", ((a-b)*100)/a, " %") }
Με λίγα λόγια…
Το RWmutex θα πρέπει να χρησιμοποιείται με ομάδες αναμονής όταν έχουμε ένα μόνο πρόγραμμα εγγραφής, για να αλλάξουμε την κοινόχρηστη μνήμη και πολλαπλούς αναγνώστες.