C #: Ποια είναι η διαφορά μεταξύ ασφαλούς με νήμα και ατομικής;


Απάντηση 1:

Τα μέσα που είναι ασφαλή με το νήμα δεν παρεμποδίζονται όταν έχουν πρόσβαση από πολλά νήματα. ατομικά μέσα αδιαίρετα, στο πλαίσιο αυτό ισοδύναμο με το αδιάλειπτο.

Για να εφαρμόσετε κλειδαριές, έχετε δύο επιλογές:

  1. Έχουν υποστήριξη υλικού για ατομικές λειτουργίες - ειδικές σύνθετες οδηγίες που εκτελούνται στο σύνολό τους, όπως το Test-and-set.Be smart (και υποφέρουν τις συνέπειες) - ο αλγόριθμος του Peterson.

Στο παράδειγμά σας στις λεπτομέρειες, και οι δύο δεν είναι ασφαλείς. αν κατάλαβα σωστά, εννοείς κάτι τέτοιο:

δημόσια τάξη Μη ασφαλής
{
    ιδιωτικό αντικείμενο ulock = νέο αντικείμενο ();

    δημόσιο int Unsafe1 {get; σειρά; } = 0.

    ιδιωτική int _unsafe2 = 0;
    δημόσιο int Unsafe2
    {
        παίρνω
        {
            κλείδωμα (ουρά)
            {
                επιστροφή _unsafe2;
            }}
        }}

        σειρά
        {
            κλείδωμα (ουρά)
            {
                _unsafe2 = αξία;
            }}
        }}
    }}
}}

Κωδικός δοκιμής:

var u = Νέο Ασφαλές ();

Parallel.For (0, 10000000, _ => {u.Unsafe1 ++;}).
Parallel.For (0, 10000000, _ => {u.Unsafe2 ++;}).

Console.WriteLine (συμβολοσειρά.Φόρμα ("{0} - {1}", u.Unsafe1, u.Unsafe2)).

Αποτέλεσμα (ένα από τα πολλά πιθανά):

4648265 - 4149827

Και για τις δύο, περισσότερες από τις μισές ενημερώσεις εξαφανίστηκαν.

Ο λόγος είναι ότι το ++ δεν είναι ατομικό - είναι στην πραγματικότητα τρεις ξεχωριστές λειτουργίες:

  1. Αποκτήστε αξία.Προσθέστε 1 στην τιμή value.Set.

Μπορούμε να το διορθώσουμε παρέχοντας μια λειτουργία αύξησης που είναι ατομική - υπάρχουν πολλοί τρόποι να το κάνετε αυτό, αλλά εδώ είναι δύο:

δημόσια τάξη Ασφαλής
{
    ιδιωτικό αντικείμενο slock = νέο αντικείμενο ();

    public int Safe1 {get; σειρά; }}
    δημόσιο κενό SafeIncrement1 ()
    {
        κλείδωμα (ουρά)
        {
            this.Safe1 ++;
        }}
    }}

    ιδιωτική int _safe2 = 0;
    δημόσιο int Safe2
    {
        παίρνω
        {
            επιστροφή _safe2;
        }}
        σειρά
        {
            _safe2 = τιμή;
        }}
    }}
    δημόσιο κενό SafeIncrement2 ()
    {
        Διασυνδεδεμένο στοιχείο (ref _safe2);
    }}
}}

Κωδικός δοκιμής:

var s = νέο Ασφαλές ();

Parallel.For (0, 10000000, _ => {s.SafeIncrement1 ();}).
Parallel.For (0, 10000000, _ => {s.SafeIncrement2 ();}).

Console.WriteLine (συμβολοσειρά.Φόρμα ("{0} - {1}", s.Safe1, s.Safe2));

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

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

  1. Οι οικείες κλειδαριές - που ονομάζονται "απαισιόδοξες ταυτόχρονες", επειδή αναλαμβάνουν τη λειτουργία, θα διακόπτονται, οπότε μην ανησυχείτε να αρχίζουν μέχρι να αποκτήσουν κάποιο κοινόχρηστο πόρο. Συγκρίνετε-και-swap, χρησιμοποιείτε μια ειδική τιμή "καναρίνι" που καταγράφεται στην αρχή, στη συνέχεια βεβαιωθείτε ότι δεν έχει αλλάξει κάτω από σας στο τέλος? η ιδέα είναι ότι εάν ένα άλλο νήμα έρχεται μαζί, θα σκοτώσει το καναρίνι, οπότε ξέρετε να δοκιμάσετε ξανά τη συναλλαγή σας από την αρχή. Αυτό απαιτεί τον δικό σας κώδικα να είναι και ατομικός - δεν μπορείτε να γράψετε ενδιάμεσα αποτελέσματα στην κοινή κατάσταση, πρέπει είτε να πετύχετε εντελώς είτε να αποτύχετε εντελώς (σαν να μην εκτελέσατε καμία ενέργεια).

Απάντηση 2:

Δύο εντελώς διαφορετικά πράγματα. Το Thread safe σημαίνει μια συνάρτηση που γράφεται με τέτοιο τρόπο ώστε να μπορεί να καλείται επανειλημμένα από πολλά διαφορετικά νήματα, χωρίς κάθε νήμα να ενοχλεί τη λειτουργία ενός άλλου νήματος (για παράδειγμα, αλλάζοντας την τιμή εάν μια μεταβλητή που χρησιμοποιεί ένα άλλο νήμα)

Ατομικά μέσα (αν πάρω εκεί που πηγαίνετε με αυτό) τη δημιουργία ενός στιγμιότυπου ενός αντικειμένου - οπότε ανεξάρτητα από το πόσο συχνά αναφέρεται, βλέπετε πάντα ότι μια περίπτωση (από οποιοδήποτε νήμα)


Απάντηση 3:

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

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

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