Πριν από το .NET 4.0, εάν χρειαζόταν να χρησιμοποιήσουμε την κλάση Dictionary σε περιβάλλον πολλαπλών νημάτων, δεν είχαμε άλλη επιλογή από το να εφαρμόσουμε μόνοι μας τον συγχρονισμό νημάτων για να διατηρήσουμε τα νήματα ασφαλή.
Πολλοί προγραμματιστές έχουν σίγουρα εφαρμόσει μια παρόμοια λύση ασφαλή για νήματα, είτε δημιουργώντας έναν εντελώς νέο τύπο λεξικού ασφαλούς για νήματα, είτε απλώς ενσωματώνοντας ένα αντικείμενο Λεξικού σε μια κλάση και προσθέτοντας έναν μηχανισμό κλειδώματος σε όλες τις μεθόδους, τον οποίο ονομάζουμε "Λεξικό + Κλειδαριές".
Αλλά τώρα, έχουμε το ConcurrentDictionary. Η περιγραφή ασφαλούς νήματος της τεκμηρίωσης κλάσης Dictionary στο MSDN αναφέρει ότι εάν χρειάζεται να χρησιμοποιήσετε μια ασφαλή υλοποίηση νήματος, χρησιμοποιήστε το ConcurrentDictionary.
Έτσι, τώρα που έχουμε μια κλάση λεξικού ασφαλή για νήματα, δεν χρειάζεται πλέον να την εφαρμόζουμε μόνοι μας. Τέλεια, έτσι δεν είναι;
Προέλευση του προβλήματος
Στην πραγματικότητα, έχω χρησιμοποιήσει το CocurrentDictionary μόνο μία φορά στο παρελθόν, στη δοκιμή μου για να δοκιμάσω την ανταπόκρισή του. Επειδή είχε καλή απόδοση στα τεστ, το αντικατέστησα αμέσως με την τάξη μου, έκανα κάποιες δοκιμές και μετά κάτι πήγε στραβά.
Λοιπόν, τι πήγε στραβά; Δεν είπες ασφαλές νήμα;
Μετά από περισσότερες δοκιμές, βρήκα τη ρίζα του προβλήματος. Ωστόσο, για κάποιο λόγο, το MSDN έκδοση 4.0 δεν περιλαμβάνει περιγραφή της υπογραφής της μεθόδου GetOrAdd που απαιτεί τη διαβίβαση μιας παραμέτρου τύπου πληρεξουσίου. Αφού κοίταξα την έκδοση 4.5, βρήκα αυτή τη σημείωση:
Εάν καλέσετε το GetOrAdd ταυτόχρονα σε διαφορετικά νήματα, το addValueFactory μπορεί να κληθεί πολλές φορές, αλλά το ζεύγος κλειδιού/τιμής ενδέχεται να μην προστεθεί στο λεξικό για κάθε κλήση. Αυτό είναι το πρόβλημα που αντιμετώπισα. Επειδή δεν περιγράφηκε προηγουμένως στην τεκμηρίωση, έπρεπε να κάνω περισσότερες δοκιμές για να επιβεβαιώσω το πρόβλημα. Φυσικά, το πρόβλημα που αντιμετωπίζω σχετίζεται με τη χρήση μου, γενικά, χρησιμοποιώ τον τύπο λεξικού για την προσωρινή αποθήκευση ορισμένων δεδομένων:
Αυτά τα δεδομένα είναι πολύ αργά στη δημιουργία. Αυτά τα δεδομένα μπορούν να δημιουργηθούν μόνο μία φορά, επειδή η δεύτερη δημιουργία θα δημιουργήσει μια εξαίρεση ή πολλαπλές δημιουργίες μπορεί να οδηγήσουν σε διαρροή πόρων κ.λπ. Είχα πρόβλημα με τη δεύτερη πάθηση. Εάν και τα δύο νήματα διαπιστώσουν ότι ένα κομμάτι δεδομένων δεν υπάρχει, θα δημιουργηθεί μία φορά, αλλά μόνο ένα αποτέλεσμα θα αποθηκευτεί με επιτυχία. Τι γίνεται με το άλλο;
Εάν η διαδικασία που δημιουργείτε δημιουργεί μια εξαίρεση, μπορείτε να χρησιμοποιήσετε τη δοκιμή: πιάστε (όχι αρκετά κομψό, αλλά λύνει το πρόβλημα). Τι γίνεται όμως αν ένας πόρος δημιουργηθεί και δεν ανακυκλωθεί;
Θα μπορούσατε να πείτε ότι ένα αντικείμενο δημιουργείται και θα συλλεχθεί σκουπίδια εάν δεν αναφέρεται πλέον σε αυτό. Ωστόσο, σκεφτείτε τι θα συνέβαινε εάν συνέβαινε η κατάσταση που περιγράφεται παρακάτω:
Δημιουργήστε κώδικα δυναμικά με το Emit. Χρησιμοποίησα αυτήν την προσέγγιση σε ένα πλαίσιο Remoting και έβαλα όλες τις υλοποιήσεις σε ένα συγκρότημα που δεν μπορούσε να ανακυκλωθεί. Εάν ένας τύπος δημιουργηθεί δύο φορές, ο δεύτερος θα υπάρχει πάντα, ακόμα κι αν δεν έχει χρησιμοποιηθεί ποτέ. Δημιουργήστε ένα νήμα άμεσα ή έμμεσα. Για παράδειγμα, πρέπει να δημιουργήσουμε ένα στοιχείο που χρησιμοποιεί ένα ιδιόκτητο νήμα για την επεξεργασία ασύγχρονων μηνυμάτων και βασίζεται στη σειρά με την οποία λαμβάνονται. Όταν δημιουργηθεί το στοιχείο, δημιουργείται ένα νήμα. Όταν καταστραφεί αυτή η παρουσία στοιχείου, τερματίζεται και το νήμα. Αλλά αν διαγράψουμε την αναφορά στο αντικείμενο μετά την καταστροφή του στοιχείου, αλλά το νήμα δεν τελειώνει για κάποιο λόγο και κρατά την αναφορά στο αντικείμενο. Στη συνέχεια, εάν το νήμα δεν πεθάνει, ούτε το αντικείμενο θα ανακυκλωθεί. Εκτελέστε μια λειτουργία P/Invoke. Απαιτήστε ο αριθμός των χρόνων κλεισίματος για τη ληφθείσα λαβή να είναι ίδιος με τον αριθμό των ανοιγμάτων. Σίγουρα, υπάρχουν πολλές παρόμοιες καταστάσεις. Για παράδειγμα, ένα αντικείμενο λεξικού θα διατηρήσει μια σύνδεση με μια υπηρεσία σε έναν απομακρυσμένο διακομιστή, η οποία μπορεί να ζητηθεί μόνο μία φορά, και εάν ζητηθεί δεύτερη φορά, η άλλη υπηρεσία θα θεωρήσει ότι έχει προκύψει κάποιο είδος σφάλματος και θα το καταγράψει στο αρχείο καταγραφής. (Σε μια εταιρεία στην οποία εργαζόμουν, υπήρχαν ορισμένες νομικές κυρώσεις για αυτήν την κατάσταση.) ) Έτσι, είναι εύκολο να δούμε ότι το Dictionary + Locks δεν μπορεί να αντικατασταθεί βιαστικά με το ConcurrentDictionary, ακόμα κι αν η τεκμηρίωση λέει ότι είναι ασφαλές για νήματα.
Αναλύστε το πρόβλημα
Ακόμα δεν καταλαβαίνετε;
Είναι αλήθεια ότι αυτό το ζήτημα μπορεί να μην προκύψει στο πλαίσιο της προσέγγισης Dictionary + Locks. Δεδομένου ότι αυτό εξαρτάται από τη συγκεκριμένη υλοποίηση, ας δούμε αυτό το απλό παράδειγμα:
Στον παραπάνω κώδικα, κρατάμε το κλείδωμα στο λεξικό πριν αρχίσουμε να αναζητούμε την τιμή του κλειδιού. Εάν το καθορισμένο ζεύγος κλειδιού-τιμής δεν υπάρχει, θα δημιουργηθεί απευθείας. Ταυτόχρονα, επειδή έχουμε ήδη μια κλειδαριά σε αυτό το λεξικό, μπορούμε να προσθέσουμε ζεύγη κλειδιού-τιμής απευθείας στο λεξικό. Στη συνέχεια, αφήστε το κλείδωμα λεξικού και επιστρέψτε το αποτέλεσμα. Εάν δύο νήματα αναζητούν την ίδια τιμή κλειδιού ταυτόχρονα, το πρώτο νήμα που λαμβάνει το κλείδωμα του λεξικού θα ολοκληρώσει τη δημιουργία του αντικειμένου και το άλλο νήμα θα περιμένει την ολοκλήρωση αυτής της δημιουργίας και θα λάβει το αποτέλεσμα της τιμής κλειδιού που δημιουργήθηκε μετά τη λήψη του κλειδώματος του λεξικού.
Αυτό είναι καλό, έτσι δεν είναι;
Πραγματικά δεν είναι! Δεν νομίζω ότι η παράλληλη δημιουργία ενός αντικειμένου όπως αυτό, όπου χρησιμοποιείται μόνο ένα στο τέλος, δεν δημιουργεί το πρόβλημα που περιέγραψα.
Η κατάσταση και το πρόβλημα που προσπαθώ να επεξεργαστώ μπορεί να μην είναι πάντα αναπαραγώγιμα, σε ένα παράλληλο περιβάλλον μπορούμε απλά να δημιουργήσουμε δύο αντικείμενα και στη συνέχεια να απορρίψουμε ένα. Λοιπόν, πώς ακριβώς συγκρίνουμε το Dictionary + Locks και το ConcurrentDictionary;
Η απάντηση είναι: εξαρτάται από τη στρατηγική χρήσης της κλειδαριάς και τον τρόπο χρήσης του λεξικού.
Παιχνίδι 1: Δημιουργήστε το ίδιο αντικείμενο παράλληλα
Αρχικά, ας υποθέσουμε ότι ένα αντικείμενο μπορεί να δημιουργηθεί δύο φορές, οπότε τι θα συμβεί αν δύο νήματα δημιουργήσουν αυτό το αντικείμενο ταυτόχρονα;
Δεύτερον, πόσο χρόνο ξοδεύουμε σε παρόμοιες δημιουργίες;
Μπορούμε απλά να δημιουργήσουμε ένα παράδειγμα όπου η δημιουργία ενός αντικειμένου διαρκεί 10 δευτερόλεπτα. Όταν το πρώτο νήμα δημιουργεί το αντικείμενο 5 δευτερόλεπτα αργότερα, η δεύτερη υλοποίηση προσπαθεί να καλέσει τη μέθοδο GetOrAdd για να πάρει το αντικείμενο και επειδή το αντικείμενο εξακολουθεί να μην υπάρχει, αρχίζει επίσης να δημιουργεί το αντικείμενο.
Σε αυτήν την κατάσταση, έχουμε 2 CPU που λειτουργούν παράλληλα για 5 δευτερόλεπτα και όταν το πρώτο νήμα τελειώσει να λειτουργεί, το δεύτερο νήμα πρέπει να συνεχίσει να λειτουργεί για 5 δευτερόλεπτα για να ολοκληρωθεί η κατασκευή του αντικειμένου. Όταν το δεύτερο νήμα ολοκληρώσει την κατασκευή του αντικειμένου, διαπιστώνει ότι ένα αντικείμενο υπάρχει ήδη και επιλέγει να χρησιμοποιήσει το υπάρχον αντικείμενο και να απορρίψει απευθείας το αντικείμενο που δημιουργήθηκε πρόσφατα.
Εάν το δεύτερο νήμα απλώς περιμένει και η δεύτερη CPU κάνει κάποια άλλη δουλειά (εκτέλεση άλλων νημάτων ή εφαρμογών, εξοικονόμηση ενέργειας), θα πάρει το επιθυμητό αντικείμενο μετά από 5 δευτερόλεπτα αντί για 10 δευτερόλεπτα.
Έτσι, υπό αυτές τις συνθήκες, το Dictionary + Locks κερδίζει ένα μικρό παιχνίδι.
Παιχνίδι 2: Επισκεφθείτε διαφορετικά αντικείμενα παράλληλα
Όχι, η κατάσταση που είπατε δεν είναι καθόλου αληθινή!
Λοιπόν, το παραπάνω παράδειγμα είναι λίγο περίεργο, αλλά περιγράφει το πρόβλημα, απλώς αυτή η χρήση είναι πιο ακραία. Λοιπόν, σκεφτείτε τι συμβαίνει εάν το πρώτο νήμα δημιουργεί ένα αντικείμενο και το δεύτερο νήμα χρειάζεται πρόσβαση σε ένα άλλο αντικείμενο κλειδιού-τιμής και αυτό το αντικείμενο κλειδιού-τιμής υπάρχει ήδη;
Στο ConcurrentDictionary, ο σχεδιασμός χωρίς κλείδωμα κάνει τις αναγνώσεις πολύ γρήγορες επειδή δεν υπάρχει κλείδωμα στην ανάγνωση. Στην περίπτωση του Dictionary + Locks, η λειτουργία ανάγνωσης θα κλειδωθεί αμοιβαία αποκλειόμενη, ακόμα κι αν πρόκειται για ένα εντελώς διαφορετικό κλειδί, το οποίο προφανώς θα επιβραδύνει τη λειτουργία ανάγνωσης.
Με αυτόν τον τρόπο, το ConcurrentDictionary απέσυρε ένα παιχνίδι.
Σημείωση: Εδώ θεωρώ ότι καταλαβαίνετε πολλές έννοιες όπως Κάδος/Κόμβος/Καταχώρηση στην τάξη του λεξικού, αν όχι, συνιστάται να διαβάσετε το άρθρο του Ofir Makmal «Κατανόηση του Γενικού Λεξικού σε βάθος», το οποίο εξηγεί καλά αυτές τις έννοιες.
Το τρίτο παιχνίδι του παιχνιδιού: διαβάστε περισσότερα και γράψτε single
Τι θα συμβεί εάν χρησιμοποιήσετε Πολλαπλούς Αναγνώστες και Μεμονωμένο Συγγραφέα αντί για πλήρες κλείδωμα στο λεξικό στο Λεξικό + Κλειδαριές;
Εάν ένα νήμα δημιουργεί ένα αντικείμενο και διατηρεί μια κλειδαριά με δυνατότητα αναβάθμισης μέχρι να δημιουργηθεί το αντικείμενο, η κλειδαριά αναβαθμίζεται σε κλειδαριά εγγραφής, τότε η λειτουργία ανάγνωσης μπορεί να εκτελεστεί παράλληλα.
Μπορούμε επίσης να λύσουμε το πρόβλημα αφήνοντας μια λειτουργία ανάγνωσης σε αδράνεια για 10 δευτερόλεπτα. Αλλά αν υπάρχουν πολύ περισσότερες αναγνώσεις από ό,τι εγγραφές, θα διαπιστώσουμε ότι το ConcurrentDictionary εξακολουθεί να είναι γρήγορο επειδή εφαρμόζει αναγνώσεις χωρίς κλείδωμα.
Η χρήση του ReaderWriterLockSlim για λεξικά επιδεινώνει τις αναγνώσεις και γενικά συνιστάται η χρήση Full Lock για λεξικά αντί για το ReaderWriterLockSlim.
Έτσι, υπό αυτές τις συνθήκες, το ConcurrentDictionary κέρδισε άλλο ένα παιχνίδι.
Σημείωση: Έχω καλύψει τις τάξεις YieldReaderWriterLock και YieldReaderWriterLockSlim σε προηγούμενα άρθρα. Με τη χρήση αυτού του κλειδώματος ανάγνωσης-εγγραφής, η ταχύτητα έχει βελτιωθεί σημαντικά (τώρα έχει εξελιχθεί σε SpinReaderWriterLockSlim) και επιτρέπει την παράλληλη εκτέλεση πολλαπλών αναγνώσεων με ελάχιστο έως καθόλου αντίκτυπο. Ενώ εξακολουθώ να χρησιμοποιώ αυτόν τον τρόπο, ένα ConcurrentDictionary χωρίς κλείδωμα θα ήταν προφανώς πιο γρήγορο.
Παιχνίδι 4: Προσθέστε πολλά ζεύγη κλειδιών-τιμών
Η αναμέτρηση δεν έχει τελειώσει ακόμα.
Τι γίνεται αν έχουμε πολλές βασικές τιμές να προσθέσουμε και όλες δεν συγκρούονται και εκχωρούνται σε διαφορετικούς κάδους;
Στην αρχή, αυτή η ερώτηση ήταν περίεργη, αλλά έκανα ένα τεστ που δεν ταίριαζε απόλυτα. Χρησιμοποίησα ένα λεξικό τύπου <int, int> και το εργοστάσιο κατασκευής του αντικειμένου θα επέστρεφε ένα αρνητικό αποτέλεσμα απευθείας ως κλειδί.
Περίμενα ότι το ConcurrentDictionary θα ήταν το πιο γρήγορο, αλλά αποδείχθηκε ότι ήταν το πιο αργό. Το Dictionary + Locks, από την άλλη πλευρά, αποδίδει πιο γρήγορα. Γιατί αυτό?
Αυτό συμβαίνει επειδή το ConcurrentDictionary εκχωρεί κόμβους και τους τοποθετεί σε διαφορετικούς κάδους, οι οποίοι είναι βελτιστοποιημένοι ώστε να πληρούν τη σχεδίαση χωρίς κλείδωμα για λειτουργίες ανάγνωσης. Ωστόσο, κατά την προσθήκη στοιχείων κλειδιού-τιμής, η διαδικασία δημιουργίας ενός κόμβου γίνεται ακριβή.
Ακόμη και σε παράλληλες συνθήκες, η εκχώρηση ενός κλειδώματος κόμβου εξακολουθεί να καταναλώνει περισσότερο χρόνο από τη χρήση ενός πλήρους κλειδώματος.
Έτσι, το Dictionary + Locks κερδίζει αυτό το παιχνίδι.
Παίζοντας το πέμπτο παιχνίδι: Η συχνότητα των λειτουργιών ανάγνωσης είναι μεγαλύτερη
Ειλικρινά, αν είχαμε έναν εκπρόσωπο που θα μπορούσε να δημιουργήσει γρήγορα αντικείμενα, δεν θα χρειαζόμασταν Λεξικό. Μπορούμε να καλέσουμε απευθείας τον εκπρόσωπο για να πάρουμε το αντικείμενο, σωστά;
Στην πραγματικότητα, η απάντηση είναι επίσης ότι εξαρτάται από την κατάσταση.
Φανταστείτε ότι ο τύπος κλειδιού είναι συμβολοσειρά και περιέχει χάρτες διαδρομής για διάφορες σελίδες στον διακομιστή ιστού και η αντίστοιχη τιμή είναι ένας τύπος αντικειμένου που περιέχει την εγγραφή των τρεχόντων χρηστών που έχουν πρόσβαση στη σελίδα και τον αριθμό όλων των επισκέψεων στη σελίδα από την έναρξη λειτουργίας του διακομιστή.
Η δημιουργία ενός τέτοιου αντικειμένου είναι σχεδόν στιγμιαία. Και μετά από αυτό, δεν χρειάζεται να δημιουργήσετε ένα νέο αντικείμενο, απλώς αλλάξτε τις τιμές που είναι αποθηκευμένες σε αυτό. Έτσι, είναι δυνατό να επιτραπεί η δημιουργία ενός τρόπου δύο φορές μέχρι να χρησιμοποιηθεί μόνο μία παρουσία. Ωστόσο, επειδή το ConcurrentDictionary κατανέμει τους πόρους του κόμβου πιο αργά, η χρήση του Dictionary + Locks θα έχει ως αποτέλεσμα ταχύτερους χρόνους δημιουργίας.
Έτσι, με αυτό το παράδειγμα είναι πολύ ιδιαίτερο, βλέπουμε επίσης ότι το Dictionary + Locks αποδίδει καλύτερα υπό αυτήν την κατάσταση, παίρνοντας λιγότερο χρόνο.
Αν και η κατανομή κόμβων στο ConcurrentDictionary είναι πιο αργή, δεν προσπάθησα να βάλω 100 εκατομμύρια στοιχεία δεδομένων σε αυτό για να δοκιμάσω τον χρόνο. Γιατί αυτό προφανώς απαιτεί πολύ χρόνο.
Αλλά στις περισσότερες περιπτώσεις, μόλις δημιουργηθεί ένα στοιχείο δεδομένων, διαβάζεται πάντα. Το πώς αλλάζει το περιεχόμενο του στοιχείου δεδομένων είναι άλλο θέμα. Επομένως, δεν έχει σημασία πόσα χιλιοστά του δευτερολέπτου χρειάζονται περισσότερα για τη δημιουργία ενός στοιχείου δεδομένων, επειδή οι αναγνώσεις είναι ταχύτερες (μόλις μερικά χιλιοστά του δευτερολέπτου ταχύτερες), αλλά οι αναγνώσεις γίνονται πιο συχνά.
Έτσι, το ConcurrentDictionary κέρδισε το παιχνίδι.
Παιχνίδι 6: Δημιουργήστε αντικείμενα που καταναλώνουν διαφορετικούς χρόνους
Τι συμβαίνει εάν ο χρόνος που απαιτείται για τη δημιουργία διαφορετικών στοιχείων δεδομένων ποικίλλει;
Δημιουργήστε πολλά στοιχεία δεδομένων που καταναλώνουν διαφορετικούς χρόνους και προσθέστε τα στο λεξικό παράλληλα. Αυτό είναι το ισχυρότερο σημείο του ConcurrentDictionary.
Το ConcurrentDictionary χρησιμοποιεί έναν αριθμό διαφορετικών μηχανισμών κλειδώματος για να επιτρέψει την ταυτόχρονη προσθήκη στοιχείων δεδομένων, αλλά η λογική όπως η απόφαση ποια κλειδαριά θα χρησιμοποιηθεί, η αίτηση κλειδαριάς για αλλαγή του μεγέθους του κάδου κ.λπ., δεν βοηθά. Η ταχύτητα με την οποία τα στοιχεία δεδομένων τοποθετούνται σε έναν κάδο είναι γρήγορη μηχανή. Αυτό που πραγματικά κάνει το ConcurrentDictionary να κερδίζει είναι η ικανότητά του να δημιουργεί αντικείμενα παράλληλα.
Ωστόσο, μπορούμε πραγματικά να κάνουμε το ίδιο πράγμα. Εάν δεν μας ενδιαφέρει αν δημιουργούμε αντικείμενα παράλληλα ή αν κάποια από αυτά έχουν απορριφθεί, μπορούμε να προσθέσουμε μια κλειδαριά για να εντοπίσουμε εάν το στοιχείο δεδομένων υπάρχει ήδη, στη συνέχεια να απελευθερώσουμε την κλειδαριά, να δημιουργήσουμε το στοιχείο δεδομένων, να το πατήσουμε για να πάρουμε το κλείδωμα, να ελέγξουμε ξανά εάν υπάρχει το στοιχείο δεδομένων και αν δεν υπάρχει, προσθέστε το στοιχείο δεδομένων. Ο κώδικας μπορεί να μοιάζει κάπως έτσι:
* Σημειώστε ότι χρησιμοποιώ ένα λεξικό τύπου <int, int>.
Στην παραπάνω απλή δομή, το Dictionary + Locks αποδίδει σχεδόν εξίσου καλά με το ConcurrentDictionary κατά τη δημιουργία και την προσθήκη στοιχείων δεδομένων σε παράλληλες συνθήκες. Υπάρχει όμως και το ίδιο πρόβλημα, όπου ορισμένες τιμές μπορεί να δημιουργηθούν αλλά να μην χρησιμοποιηθούν ποτέ.
συμπέρασμα
Λοιπόν, υπάρχει συμπέρασμα;
Αυτή τη στιγμή, υπάρχουν ακόμα μερικά:
Όλες οι τάξεις λεξικών είναι πολύ γρήγορες. Παρόλο που έχω δημιουργήσει εκατομμύρια δεδομένα, εξακολουθούν να είναι γρήγορα. Κανονικά, δημιουργούμε μόνο έναν μικρό αριθμό στοιχείων δεδομένων και υπάρχουν ορισμένα χρονικά διαστήματα μεταξύ των αναγνώσεων, επομένως γενικά δεν παρατηρούμε τη χρονική επιβάρυνση της ανάγνωσης στοιχείων δεδομένων. Εάν το ίδιο αντικείμενο δεν μπορεί να δημιουργηθεί δύο φορές, μην χρησιμοποιήσετε το ConcurrentDictionary. Εάν ανησυχείτε πραγματικά για την απόδοση, το Dictionary + Locks μπορεί να εξακολουθεί να είναι μια καλή λύση. Ένας σημαντικός παράγοντας είναι ο αριθμός των στοιχείων δεδομένων που προστέθηκαν και αφαιρέθηκαν. Αλλά αν υπάρχουν πολλές λειτουργίες ανάγνωσης, είναι πιο αργή από το ConcurrentDictionary. Αν και δεν το εισήγαγα, υπάρχει στην πραγματικότητα μεγαλύτερη ελευθερία στη χρήση του σχήματος Dictionary + Locks. Για παράδειγμα, μπορείτε να κλειδώσετε μία φορά, να προσθέσετε πολλά στοιχεία δεδομένων, να διαγράψετε πολλά στοιχεία δεδομένων ή να υποβάλετε ερώτημα πολλές φορές κ.λπ., και στη συνέχεια να απελευθερώσετε το κλείδωμα. Γενικά, αποφύγετε τη χρήση του ReaderWriterLockSlim εάν υπάρχουν πολύ περισσότερες αναγνώσεις από ό,τι εγγραφές. Οι τύποι λεξικών είναι ήδη πολύ πιο γρήγοροι από το να αποκτήσετε ένα κλείδωμα ανάγνωσης σε ένα κλείδωμα ανάγνωσης-εγγραφής. Φυσικά, αυτό εξαρτάται και από τον χρόνο που καταναλώνεται για τη δημιουργία ενός αντικειμένου σε μια κλειδαριά. Έτσι, νομίζω ότι τα παραδείγματα που δίνονται είναι λίγο ακραία, αλλά δείχνουν ότι η χρήση του ConcurrentDictionary δεν είναι πάντα η καλύτερη λύση.
Νιώστε τη διαφορά
Έγραψα αυτό το άρθρο με σκοπό να αναζητήσω μια καλύτερη λύση.
Προσπαθώ ήδη να κατανοήσω βαθύτερα πώς λειτουργεί μια συγκεκριμένη τάξη λεξικού (τώρα νιώθω ότι είμαι πολύ σαφής).
Αναμφισβήτητα, ο κάδος και ο κόμβος στο ConcurrentDictionary είναι πολύ απλοί. Έκανα κάτι παρόμοιο όταν προσπάθησα να δημιουργήσω μια τάξη λεξικού. Το κανονικό μάθημα Λεξικό μπορεί να φαίνεται πιο απλό, αλλά στην πραγματικότητα είναι πιο περίπλοκο.
Στο ConcurrentDictionary, κάθε κόμβος είναι μια πλήρης κλάση. Στην κλάση Dictionary, το Node υλοποιείται χρησιμοποιώντας έναν τύπο τιμής και όλοι οι κόμβοι διατηρούνται σε έναν τεράστιο πίνακα, ενώ το Bucket χρησιμοποιείται για την ευρετηρίαση στον πίνακα. Χρησιμοποιείται επίσης στη θέση της απλής αναφοράς ενός κόμβου στον επόμενο κόμβο του (εξάλλου, ως κόμβος τύπου δομής, δεν μπορεί να περιέχει ένα μέλος κόμβου ενός τύπου δομής).
Κατά την προσθήκη και την αφαίρεση ενός λεξικού, η κλάση Dictionary δεν μπορεί απλώς να δημιουργήσει έναν νέο κόμβο, πρέπει να ελέγξει εάν υπάρχει ευρετήριο που επισημαίνει έναν κόμβο που έχει διαγραφεί και στη συνέχεια να τον επαναχρησιμοποιήσει. Ή το "Count" χρησιμοποιείται για να πάρει τη θέση του νέου Node στον πίνακα. Στην πραγματικότητα, όταν ο πίνακας είναι πλήρης, η κλάση Dictionary επιβάλλει αλλαγή μεγέθους.
Για το ConcurrentDictionary, ένας κόμβος μπορεί να θεωρηθεί ως ένα νέο αντικείμενο. Η αφαίρεση ενός κόμβου είναι απλώς η αφαίρεση της αναφοράς του. Η προσθήκη ενός νέου κόμβου μπορεί απλώς να δημιουργήσει μια νέα παρουσία κόμβου. Η αλλαγή του μεγέθους γίνεται μόνο για την αποφυγή συγκρούσεων, αλλά δεν είναι υποχρεωτική.
Έτσι, εάν η κλάση Dictionary χρησιμοποιεί σκόπιμα πιο σύνθετους αλγόριθμους για να το χειριστεί, πώς θα διασφαλίσει το ConcurrentDictionary ότι αποδίδει καλύτερα σε περιβάλλον πολλαπλών νημάτων;
Η αλήθεια είναι: η τοποθέτηση όλων των κόμβων σε έναν πίνακα είναι ο πιο γρήγορος τρόπος κατανομής και ανάγνωσης, ακόμα κι αν χρειαζόμαστε έναν άλλο πίνακα για να παρακολουθούμε πού θα βρούμε αυτά τα στοιχεία δεδομένων. Φαίνεται λοιπόν ότι η ύπαρξη του ίδιου αριθμού κάδων θα χρησιμοποιήσει περισσότερη μνήμη, αλλά τα νέα στοιχεία δεδομένων δεν χρειάζεται να ανακατανεμηθούν, δεν χρειάζονται νέοι συγχρονισμοί αντικειμένων και δεν πραγματοποιείται νέα συλλογή σκουπιδιών. Γιατί όλα είναι ήδη στη θέση τους.
Ωστόσο, η αντικατάσταση περιεχομένου σε έναν κόμβο δεν είναι ατομική λειτουργία, κάτι που είναι ένας από τους παράγοντες που καθιστούν το νήμα του ανασφαλές. Επειδή οι κόμβοι είναι όλοι αντικείμενα, δημιουργείται αρχικά ένας κόμβος και στη συνέχεια ενημερώνεται μια ξεχωριστή αναφορά για να δείχνει σε αυτόν (ατομική λειτουργία εδώ). Έτσι, το νήμα ανάγνωσης μπορεί να διαβάσει το περιεχόμενο του λεξικού χωρίς κλειδαριά και η ανάγνωση πρέπει να είναι μία από τις παλιές και νέες τιμές και δεν υπάρχει πιθανότητα ανάγνωσης μιας ημιτελούς τιμής.
Λοιπόν, η αλήθεια είναι: αν δεν χρειάζεστε κλειδαριά, η τάξη του Λεξικού είναι πιο γρήγορη στις αναγνώσεις, γιατί είναι η κλειδαριά που επιβραδύνει την ανάγνωση.
Αυτό το άρθρο έχει μεταφραστεί από το άρθρο του Paulo Zemek "Dictionary + Locking versus ConcurrentDictionary" στο CodeProject και ορισμένες δηλώσεις θα αλλάξουν για λόγους κατανόησης.
|