Θα συζητήσουμε πώς η χρήση του RWMutex έναντι του Mutex μπορεί να βελτιώσει σημαντικά την απόδοση.

Εισαγωγή

Δεν είναι επιθυμητή η πρόσβαση πολλών νημάτων στην ίδια μνήμη ταυτόχρονα. Στο Golang μπορούμε να έχουμε πολλές διαφορετικές γορουτίνες που όλες έχουν πιθανώς πρόσβαση στις ίδιες μεταβλητές μνήμης, κάτι που μπορεί να οδηγήσει σε κατάσταση αγώνα. Mutex, αμοιβαίος αποκλεισμός, μαζί με ομάδες αναμονής για αποφυγή συνθηκών αγώνα. Μπορούν να χρησιμοποιηθούν Mutex και RWmutex του golang.

Mutex v/s RWmutex

Το Mutex στο πρώτο είναι οκτώ byte. Το RWMutex είναι είκοσι τέσσερα byte. Από τα επίσημα έγγραφα: Το RWMutex είναι ένα κλείδωμα αμοιβαίου αποκλεισμού αναγνώστη/συγγραφέα. Η κλειδαριά μπορεί να κρατηθεί από έναν αυθαίρετο αριθμό αναγνωστών ή έναν μόνο συγγραφέα. Η μηδενική τιμή για ένα RWMutex είναι ένα ξεκλείδωτο mutex.

// Mutex v/s RWMutex
//Mutex
type Mutex struct {
    state int32
    sema  uint32
}
// RWMutex 
type 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 θα πρέπει να χρησιμοποιείται με ομάδες αναμονής όταν έχουμε ένα μόνο πρόγραμμα εγγραφής, για να αλλάξουμε την κοινόχρηστη μνήμη και πολλαπλούς αναγνώστες.