.NET: Εργαλεία για εργασία με multithreading και asynchrony. Μέρος 1

Δημοσιεύω το πρωτότυπο άρθρο για το Habr, η μετάφραση του οποίου είναι αναρτημένη στο εταιρικό blog post.

Η ανάγκη να κάνουμε κάτι ασύγχρονα, χωρίς να περιμένουμε το αποτέλεσμα εδώ και τώρα, ή να μοιράσουμε μεγάλη εργασία σε πολλές μονάδες που το εκτελούσαν, υπήρχε πριν από την εμφάνιση των υπολογιστών. Με την έλευση τους αυτή η ανάγκη έγινε πολύ απτή. Τώρα, το 2019, πληκτρολογώ αυτό το άρθρο σε φορητό υπολογιστή με επεξεργαστή Intel Core 8 πυρήνων, στον οποίο εκτελούνται παράλληλα περισσότερες από εκατό διεργασίες και ακόμη περισσότερα νήματα. Σε κοντινή απόσταση, υπάρχει ένα ελαφρώς άθλιο τηλέφωνο, αγορασμένο πριν από μερικά χρόνια, έχει επεξεργαστή 8 πυρήνων. Οι θεματικοί πόροι είναι γεμάτοι άρθρα και βίντεο όπου οι συντάκτες τους θαυμάζουν τα φετινά κορυφαία smartphone που διαθέτουν επεξεργαστές 16 πυρήνων. Το MS Azure παρέχει μια εικονική μηχανή με επεξεργαστή 20 πυρήνων και 128 TB RAM για λιγότερο από 2 $/ώρα. Δυστυχώς, είναι αδύνατο να εξαχθεί το μέγιστο και να αξιοποιηθεί αυτή η δύναμη χωρίς να μπορούμε να διαχειριστούμε την αλληλεπίδραση των νημάτων.

Λεξιλόγιο

Επεξεργάζομαι, διαδικασία - Αντικείμενο λειτουργικού συστήματος, απομονωμένος χώρος διευθύνσεων, περιέχει νήματα.
Νήμα - ένα αντικείμενο OS, η μικρότερη μονάδα εκτέλεσης, μέρος μιας διαδικασίας, τα νήματα μοιράζονται τη μνήμη και άλλους πόρους μεταξύ τους μέσα σε μια διεργασία.
Πολυεπεξεργασία - Ιδιότητα OS, δυνατότητα εκτέλεσης πολλών διεργασιών ταυτόχρονα
Πολυπύρηνος - μια ιδιότητα του επεξεργαστή, η δυνατότητα χρήσης πολλών πυρήνων για την επεξεργασία δεδομένων
Πολυεπεξεργασία - μια ιδιότητα ενός υπολογιστή, η δυνατότητα ταυτόχρονης φυσικής εργασίας με πολλούς επεξεργαστές
Multithreading — μια ιδιότητα μιας διεργασίας, η ικανότητα διανομής της επεξεργασίας δεδομένων μεταξύ πολλών νημάτων.
Παραλληλισμός - εκτέλεση πολλών ενεργειών σωματικά ταυτόχρονα ανά μονάδα χρόνου
Ασύγχρονη — εκτέλεση μιας πράξης χωρίς αναμονή για την ολοκλήρωση αυτής της επεξεργασίας· το αποτέλεσμα της εκτέλεσης μπορεί να υποβληθεί σε επεξεργασία αργότερα.

Μεταφορά

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

Κατά την προετοιμασία του πρωινού το πρωί (CPU) Έρχομαι στην κουζίνα (Υπολογιστής). Έχω 2 χέρια (Πυρήνες). Υπάρχουν πολλές συσκευές στην κουζίνα (IO): φούρνος, βραστήρας, τοστιέρα, ψυγείο. Ανοίγω το γκάζι, του βάζω ένα τηγάνι και του ρίχνω λάδι χωρίς να περιμένω να ζεσταθεί (ασύγχρονα, Non-Blocking-IO-Wait), βγάζω τα αυγά από το ψυγείο και τα σπάω σε πιατέλα και μετά τα χτυπάω με το ένα χέρι (Νήμα #1), και δεύτερο (Νήμα #2) κρατώντας το πιάτο (Κοινόχρηστος πόρος). Τώρα θα ήθελα να ανοίξω τον βραστήρα, αλλά δεν έχω αρκετά χέρια (Νήμα πείνα) Σε αυτό το διάστημα ζεσταίνεται το τηγάνι (Επεξεργασία του αποτελέσματος) στο οποίο ρίχνω ότι έχω χτυπήσει. Απλώνω τον βραστήρα και τον ανάβω και βλέπω χαζά το νερό να βράζει μέσα του (Blocking-IO-Wait), αν και σε αυτό το διάστημα θα μπορούσε να είχε πλύνει το πιάτο όπου χτυπούσε την ομελέτα.

Μαγείρεψα μια ομελέτα χρησιμοποιώντας μόνο 2 χέρια και δεν έχω περισσότερα, αλλά ταυτόχρονα, τη στιγμή που χτυπούσα την ομελέτα, έγιναν 3 επεμβάσεις ταυτόχρονα: χτύπημα της ομελέτας, κράτημα του πιάτου, ζέσταμα του τηγανιού. Η CPU είναι το γρηγορότερο μέρος του υπολογιστή, το IO είναι αυτό που τις περισσότερες φορές όλα επιβραδύνονται, επομένως συχνά μια αποτελεσματική λύση είναι να απασχολείτε τη CPU με κάτι κατά τη λήψη δεδομένων από το IO.

Συνεχίζοντας τη μεταφορά:

  • Αν στη διαδικασία προετοιμασίας μιας ομελέτας, θα προσπαθούσα να αλλάξω και ρούχα, αυτό θα ήταν ένα παράδειγμα multitasking. Μια σημαντική απόχρωση: οι υπολογιστές είναι πολύ καλύτεροι σε αυτό από τους ανθρώπους.
  • Μια κουζίνα με πολλούς σεφ, για παράδειγμα σε ένα εστιατόριο - ένας υπολογιστής πολλαπλών πυρήνων.
  • Πολλά εστιατόρια σε ένα food court σε ένα εμπορικό κέντρο - κέντρο δεδομένων

Εργαλεία .NET

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

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

Ξεκινώντας ένα νήμα

Η κλάση Thread είναι η πιο βασική κλάση στο .NET για εργασία με νήματα. Ο κατασκευαστής δέχεται έναν από τους δύο αντιπροσώπους:

  • ThreadStart — Δεν υπάρχουν παράμετροι
  • ParametrizedThreadStart - με μία παράμετρο αντικειμένου τύπου.

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

new Thread(...).Start(...);

Η κλάση ThreadPool αντιπροσωπεύει την έννοια του pool. Στο .NET, το thread pool είναι ένα κομμάτι μηχανικής και οι προγραμματιστές της Microsoft έχουν καταβάλει μεγάλη προσπάθεια για να διασφαλίσουν ότι λειτουργεί βέλτιστα σε μια μεγάλη ποικιλία σεναρίων.

Γενική έννοια:

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

Για να χρησιμοποιήσετε ένα νήμα από το pool, υπάρχει μια μέθοδος QueueUserWorkItem που δέχεται έναν πληρεξούσιο τύπου WaitCallback, ο οποίος έχει την ίδια υπογραφή με το ParametrizedThreadStart και η παράμετρος που μεταβιβάζεται σε αυτό εκτελεί την ίδια λειτουργία.

ThreadPool.QueueUserWorkItem(...);

Η λιγότερο γνωστή μέθοδος συγκέντρωσης νημάτων RegisterWaitForSingleObject χρησιμοποιείται για την οργάνωση μη αποκλειστικών λειτουργιών IO. Ο πληρεξούσιος που μεταβιβάστηκε σε αυτήν τη μέθοδο θα κληθεί όταν το WaitHandle που μεταβιβάστηκε στη μέθοδο είναι "Released".

ThreadPool.RegisterWaitForSingleObject(...)

Το .NET διαθέτει χρονόμετρο νήματος και διαφέρει από τους χρονοδιακόπτες WinForms/WPF στο ότι ο χειριστής του θα καλείται σε ένα νήμα που έχει ληφθεί από το pool.

System.Threading.Timer

Υπάρχει επίσης ένας μάλλον εξωτικός τρόπος για να στείλετε έναν εκπρόσωπο για εκτέλεση σε ένα νήμα από το pool - η μέθοδος BeginInvoke.

DelegateInstance.BeginInvoke

Θα ήθελα να σταθώ εν συντομία στη συνάρτηση στην οποία μπορούν να κληθούν πολλές από τις παραπάνω μεθόδους - CreateThread από το Kernel32.dll Win32 API. Υπάρχει τρόπος, χάρη στον μηχανισμό των εξωτερικών μεθόδων, να καλέσετε αυτή τη συνάρτηση. Έχω δει μια τέτοια κλήση μόνο μία φορά σε ένα τρομερό παράδειγμα κώδικα παλαιού τύπου, και το κίνητρο του συγγραφέα που έκανε ακριβώς αυτό εξακολουθεί να παραμένει ένα μυστήριο για μένα.

Kernel32.dll CreateThread

Προβολή και εντοπισμός σφαλμάτων νημάτων

Τα νήματα που δημιουργούνται από εσάς, όλα τα στοιχεία τρίτων και το .NET pool μπορούν να προβληθούν στο παράθυρο Threads του Visual Studio. Αυτό το παράθυρο θα εμφανίζει πληροφορίες νήματος μόνο όταν η εφαρμογή βρίσκεται σε κατάσταση εντοπισμού σφαλμάτων και σε λειτουργία διακοπής. Εδώ μπορείτε να δείτε εύκολα τα ονόματα στοίβων και τις προτεραιότητες κάθε νήματος και να αλλάξετε τον εντοπισμό σφαλμάτων σε ένα συγκεκριμένο νήμα. Χρησιμοποιώντας την ιδιότητα Priority της κλάσης Thread, μπορείτε να ορίσετε την προτεραιότητα ενός νήματος, το οποίο το OC και το CLR θα αντιληφθούν ως σύσταση κατά τη διαίρεση του χρόνου του επεξεργαστή μεταξύ των νημάτων.

.NET: Εργαλεία για εργασία με multithreading και asynchrony. Μέρος 1

Παράλληλη βιβλιοθήκη εργασιών

Το Task Parallel Library (TPL) εισήχθη στο .NET 4.0. Τώρα είναι το πρότυπο και το κύριο εργαλείο για την εργασία με την ασύγχρονη. Κάθε κώδικας που χρησιμοποιεί μια παλαιότερη προσέγγιση θεωρείται παλαιού τύπου. Η βασική μονάδα του TPL είναι η κλάση Task από τον χώρο ονομάτων System.Threading.Tasks. Μια εργασία είναι μια αφαίρεση πάνω από ένα νήμα. Με τη νέα έκδοση της γλώσσας C#, αποκτήσαμε έναν κομψό τρόπο εργασίας με τελεστές Tasks - async/wait. Αυτές οι έννοιες επέτρεψαν τη σύνταξη ασύγχρονου κώδικα σαν να ήταν απλός και σύγχρονος, αυτό επέτρεψε ακόμη και σε άτομα με μικρή κατανόηση της εσωτερικής λειτουργίας των νημάτων να γράφουν εφαρμογές που τα χρησιμοποιούν, εφαρμογές που δεν παγώνουν όταν εκτελούν μεγάλες λειτουργίες. Η χρήση async/wait είναι ένα θέμα για ένα ή και πολλά άρθρα, αλλά θα προσπαθήσω να μάθω την ουσία του με λίγες προτάσεις:

  • Το async είναι ένας τροποποιητής μιας μεθόδου που επιστρέφει Task or void
  • και το await είναι ένας τελεστής αναμονής εργασιών που δεν αποκλείει.

Για άλλη μια φορά: ο τελεστής αναμονής, στη γενική περίπτωση (υπάρχουν εξαιρέσεις), θα απελευθερώσει περαιτέρω το τρέχον νήμα της εκτέλεσης και όταν η Εργασία ολοκληρώσει την εκτέλεσή της και το νήμα (στην πραγματικότητα, θα ήταν πιο σωστό να πούμε το πλαίσιο , αλλά περισσότερα για αυτό αργότερα) θα συνεχίσει να εκτελεί τη μέθοδο περαιτέρω. Μέσα στο .NET, αυτός ο μηχανισμός υλοποιείται με τον ίδιο τρόπο όπως η απόδοση απόδοσης, όταν η γραπτή μέθοδος μετατρέπεται σε μια ολόκληρη κλάση, η οποία είναι μια μηχανή κατάστασης και μπορεί να εκτελεστεί σε ξεχωριστά κομμάτια ανάλογα με αυτές τις καταστάσεις. Οποιοσδήποτε ενδιαφέρεται μπορεί να γράψει οποιονδήποτε απλό κώδικα χρησιμοποιώντας asynс/await, να μεταγλωττίσει και να δει τη συναρμολόγηση χρησιμοποιώντας JetBrains dotPeek με ενεργοποιημένο τον κώδικα που δημιουργείται από τον μεταγλωττιστή.

Ας δούμε τις επιλογές για την εκκίνηση και τη χρήση του Task. Στο παρακάτω παράδειγμα κώδικα, δημιουργούμε μια νέα εργασία που δεν κάνει τίποτα χρήσιμο (Thread.Sleep(10000)), αλλά στην πραγματική ζωή αυτό θα πρέπει να είναι μια περίπλοκη εργασία έντασης CPU.

using TCO = System.Threading.Tasks.TaskCreationOptions;

public static async void VoidAsyncMethod() {
    var cancellationSource = new CancellationTokenSource();

    await Task.Factory.StartNew(
        // Code of action will be executed on other context
        () => Thread.Sleep(10000),
        cancellationSource.Token,
        TCO.LongRunning | TCO.AttachedToParent | TCO.PreferFairness,
        scheduler
    );

    //  Code after await will be executed on captured context
}

Δημιουργείται μια εργασία με διάφορες επιλογές:

  • Το LongRunning είναι μια υπόδειξη ότι η εργασία δεν θα ολοκληρωθεί γρήγορα, πράγμα που σημαίνει ότι ίσως αξίζει να σκεφτείτε να μην πάρετε ένα νήμα από το pool, αλλά να δημιουργήσετε ένα ξεχωριστό για αυτήν την Εργασία, ώστε να μην βλάψετε άλλους.
  • AttachedToParent - Οι εργασίες μπορούν να ταξινομηθούν σε μια ιεραρχία. Εάν χρησιμοποιήθηκε αυτή η επιλογή, τότε η Εργασία μπορεί να βρίσκεται σε κατάσταση όπου η ίδια έχει ολοκληρώσει και περιμένει την εκτέλεση των παιδιών της.
  • PreferFairness - σημαίνει ότι θα ήταν καλύτερο να εκτελούνται οι Εργασίες που στάλθηκαν για εκτέλεση νωρίτερα πριν από αυτές που αποστέλλονται αργότερα. Αλλά αυτό είναι απλώς μια σύσταση και τα αποτελέσματα δεν είναι εγγυημένα.

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

Η τελευταία παράμετρος είναι ένα αντικείμενο προγραμματιστή τύπου TaskScheduler. Αυτή η κλάση και οι απόγονοί της έχουν σχεδιαστεί για να ελέγχουν στρατηγικές για τη διανομή Tasks σε νήματα· από προεπιλογή, η εργασία θα εκτελείται σε ένα τυχαίο νήμα από το pool.

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

Η μέθοδος έχει επισημανθεί ως ασύγχρονη κενή, πράγμα που σημαίνει ότι μπορεί να χρησιμοποιήσει τον τελεστή αναμονής, αλλά ο κωδικός κλήσης δεν θα μπορεί να περιμένει για την εκτέλεση. Εάν μια τέτοια δυνατότητα είναι απαραίτητη, τότε η μέθοδος πρέπει να επιστρέψει την Εργασία. Οι μέθοδοι με την ένδειξη async void είναι αρκετά συνηθισμένες: κατά κανόνα, πρόκειται για χειριστές συμβάντων ή άλλες μεθόδους που λειτουργούν στην αρχή της πυρκαγιάς και της λήθης. Εάν πρέπει όχι μόνο να δώσετε την ευκαιρία να περιμένετε μέχρι το τέλος της εκτέλεσης, αλλά και να επιστρέψετε το αποτέλεσμα, τότε πρέπει να χρησιμοποιήσετε το Task.

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

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

public static async void AnotherMethod() {

    int result = await AsyncMethod(); // good

    result = AsyncMethod().Result; // bad

    AsyncMethod().Wait(); // bad

    IEnumerable<Task> tasks = new Task[] {
        AsyncMethod(), OtherAsyncMethod()
    };

    await Task.WhenAll(tasks); // good
    await Task.WhenAny(tasks); // good

    Task.WaitAll(tasks.ToArray()); // bad
}

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

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

Ένα άλλο μειονέκτημα αυτής της προσέγγισης είναι ο πολύπλοκος χειρισμός σφαλμάτων. Το γεγονός είναι ότι τα σφάλματα στον ασύγχρονο κώδικα κατά τη χρήση του ασύγχρονου/αναμονής είναι πολύ εύκολο να χειριστούν - συμπεριφέρονται το ίδιο σαν ο κώδικας να ήταν σύγχρονος. Ενώ αν εφαρμόσουμε τον σύγχρονο εξορκισμό αναμονής σε μια Εργασία, η αρχική εξαίρεση μετατρέπεται σε AggregateException, π.χ. Για να χειριστείτε την εξαίρεση, θα πρέπει να εξετάσετε τον τύπο InnerException και να γράψετε μόνοι σας μια αλυσίδα if μέσα σε ένα μπλοκ catch ή να χρησιμοποιήσετε το catch κατά την κατασκευή, αντί για την αλυσίδα των μπλοκ catch που είναι πιο οικεία στον κόσμο της C#.

Το τρίτο και τελευταίο παράδειγμα επισημαίνονται επίσης ως κακά για τον ίδιο λόγο και περιέχουν όλα τα ίδια προβλήματα.

Οι μέθοδοι WhenAny και WhenAll είναι εξαιρετικά βολικές για την αναμονή μιας ομάδας Εργασιών· τυλίγουν μια ομάδα Εργασιών σε μία, η οποία θα ενεργοποιηθεί είτε όταν ενεργοποιηθεί για πρώτη φορά μια Εργασία από την ομάδα είτε όταν όλες έχουν ολοκληρώσει την εκτέλεσή τους.

Σταμάτημα νημάτων

Για διάφορους λόγους, μπορεί να χρειαστεί να σταματήσετε τη ροή μετά την έναρξη της. Υπάρχουν διάφοροι τρόποι για να γίνει αυτό. Η κλάση Thread έχει δύο μεθόδους που ονομάζονται κατάλληλα: αποτυχία и Διακοπή. Το πρώτο δεν συνιστάται ιδιαίτερα για χρήση, γιατί μετά την κλήση του σε οποιαδήποτε τυχαία στιγμή, κατά την επεξεργασία οποιασδήποτε εντολής, θα γίνει εξαίρεση ThreadAbortedException. Δεν περιμένετε μια τέτοια εξαίρεση όταν αυξάνεται οποιαδήποτε ακέραια μεταβλητή, σωστά; Και όταν χρησιμοποιείτε αυτήν τη μέθοδο, αυτή είναι μια πολύ πραγματική κατάσταση. Εάν χρειάζεται να αποτρέψετε το CLR από το να δημιουργήσει μια τέτοια εξαίρεση σε μια συγκεκριμένη ενότητα κώδικα, μπορείτε να την τυλίξετε σε κλήσεις Thread.BeginCriticalRegion, Thread.EndCriticalRegion. Οποιοσδήποτε κωδικός γραμμένος σε ένα τελικό μπλοκ είναι τυλιγμένος σε τέτοιες κλήσεις. Για το λόγο αυτό, στα βάθη του κώδικα πλαισίου μπορείτε να βρείτε μπλοκ με κενή δοκιμή, αλλά όχι άδειο τέλος. Η Microsoft αποθαρρύνει αυτή τη μέθοδο τόσο πολύ που δεν την συμπεριέλαβε στον πυρήνα .net.

Η μέθοδος Interrupt λειτουργεί πιο προβλέψιμα. Μπορεί να διακόψει το νήμα με μια εξαίρεση ThreadInterruptedException μόνο κατά τις στιγμές εκείνες που το νήμα βρίσκεται σε κατάσταση αναμονής. Εισέρχεται σε αυτήν την κατάσταση ενώ βρίσκεται σε κατάσταση αναμονής για WaitHandle, κλείδωμα ή αφού καλέσετε το Thread.Sleep.

Και οι δύο επιλογές που περιγράφονται παραπάνω είναι κακές λόγω της μη προβλεψιμότητάς τους. Η λύση είναι να χρησιμοποιήσετε μια δομή CancellationToken και τάξη CancellationTokenSource. Το θέμα είναι το εξής: δημιουργείται μια παρουσία της κλάσης CancellationTokenSource και μόνο αυτός που την κατέχει μπορεί να σταματήσει τη λειτουργία καλώντας τη μέθοδο Ματαίωση. Μόνο το CancellationToken μεταβιβάζεται στην ίδια τη λειτουργία. Οι κάτοχοι CancellationToken δεν μπορούν να ακυρώσουν οι ίδιοι τη λειτουργία, αλλά μπορούν μόνο να ελέγξουν εάν η λειτουργία έχει ακυρωθεί. Υπάρχει μια ιδιότητα Boolean για αυτό Ζητείται ακύρωση και μέθοδος ThrowIfCancelRequested. Το τελευταίο θα ρίξει μια εξαίρεση TaskCancelledException αν η μέθοδος Cancel κλήθηκε στην παρουσία του CancellationToken που παπαγαλίστηκε. Και αυτή είναι η μέθοδος που προτείνω να χρησιμοποιήσετε. Αυτή είναι μια βελτίωση σε σχέση με τις προηγούμενες επιλογές αποκτώντας πλήρη έλεγχο σε ποιο σημείο μπορεί να ματαιωθεί μια λειτουργία εξαίρεσης.

Η πιο βάναυση επιλογή για τη διακοπή ενός νήματος είναι να καλέσετε τη συνάρτηση Win32 API TerminateThread. Η συμπεριφορά του CLR μετά την κλήση αυτής της συνάρτησης μπορεί να είναι απρόβλεπτη. Στο MSDN γράφονται τα εξής σχετικά με αυτή τη λειτουργία: «Το TerminateThread είναι μια επικίνδυνη λειτουργία που πρέπει να χρησιμοποιείται μόνο στις πιο ακραίες περιπτώσεις. "

Μετατροπή παλαιού τύπου API σε Task Based χρησιμοποιώντας τη μέθοδο FromAsync

Εάν είστε αρκετά τυχεροί να δουλέψετε σε ένα έργο που ξεκίνησε μετά την εισαγωγή του Tasks και έπαψε να προκαλεί σιωπηλή φρίκη στους περισσότερους προγραμματιστές, τότε δεν θα χρειαστεί να αντιμετωπίσετε πολλά παλιά API, τόσο τρίτων όσο και με αυτά της ομάδας σας έχει βασανίσει στο παρελθόν. Ευτυχώς, η ομάδα του .NET Framework μας φρόντισε, αν και ίσως ο στόχος ήταν να φροντίσουμε τον εαυτό μας. Όπως και να έχει, το .NET διαθέτει μια σειρά από εργαλεία για ανώδυνη μετατροπή κώδικα γραμμένου σε παλιές προσεγγίσεις ασύγχρονου προγραμματισμού στη νέα. Ένα από αυτά είναι η μέθοδος FromAsync του TaskFactory. Στο παράδειγμα κώδικα παρακάτω, αναδιπλώνω τις παλιές ασύγχρονες μεθόδους της κλάσης WebRequest σε μια Εργασία χρησιμοποιώντας αυτήν τη μέθοδο.

object state = null;
WebRequest wr = WebRequest.CreateHttp("http://github.com");
await Task.Factory.FromAsync(
    wr.BeginGetResponse,
    we.EndGetResponse
);

Αυτό είναι απλώς ένα παράδειγμα και είναι απίθανο να χρειαστεί να το κάνετε αυτό με ενσωματωμένους τύπους, αλλά οποιοδήποτε παλιό έργο είναι απλώς γεμάτο με μεθόδους BeginDoSomething που επιστρέφουν μεθόδους IAsyncResult και EndDoSomething που το λαμβάνουν.

Μετατροπή API παλαιού τύπου σε Task Based χρησιμοποιώντας την κλάση TaskCompletionSource

Ένα άλλο σημαντικό εργαλείο που πρέπει να λάβετε υπόψη είναι η τάξη TaskCompletionSource. Όσον αφορά τις λειτουργίες, τον σκοπό και την αρχή λειτουργίας, μπορεί να θυμίζει κάπως τη μέθοδο RegisterWaitForSingleObject της κλάσης ThreadPool, για την οποία έγραψα παραπάνω. Χρησιμοποιώντας αυτήν την κλάση, μπορείτε εύκολα και άνετα να τυλίξετε παλιά ασύγχρονα API στο Tasks.

Θα πείτε ότι έχω ήδη μιλήσει για τη μέθοδο FromAsync της κλάσης TaskFactory που προορίζεται για αυτούς τους σκοπούς. Εδώ θα πρέπει να θυμηθούμε ολόκληρη την ιστορία της ανάπτυξης ασύγχρονων μοντέλων στο .net που έχει προσφέρει η Microsoft τα τελευταία 15 χρόνια: πριν από το Asynchronous Pattern (TAP) υπήρχε το Asynchronous Programming Pattern (APP), το οποίο αφορούσε μεθόδους ΞεκινήστεDoSomething επιστρέφει IAsyncResult και μεθόδους ΤέλοςDoSomething που το αποδέχεται και για την κληρονομιά αυτών των ετών η μέθοδος FromAsync είναι απλά τέλεια, αλλά με την πάροδο του χρόνου, αντικαταστάθηκε από το Asynchronous Pattern που βασίζεται σε συμβάντα (EAP), το οποίο προϋπέθετε ότι ένα συμβάν θα εμφανιζόταν όταν ολοκληρωνόταν η ασύγχρονη λειτουργία.

Το TaskCompletionSource είναι τέλειο για την αναδίπλωση Tasks και παλαιού τύπου API που έχουν δημιουργηθεί γύρω από το μοντέλο συμβάντος. Η ουσία της δουλειάς του είναι η εξής: ένα αντικείμενο αυτής της κλάσης έχει μια δημόσια ιδιότητα τύπου Task, η κατάσταση της οποίας μπορεί να ελεγχθεί μέσω των μεθόδων SetResult, SetException κ.λπ. της κλάσης TaskCompletionSource. Σε μέρη όπου ο τελεστής αναμονής εφαρμόστηκε σε αυτήν την Εργασία, θα εκτελεστεί ή θα αποτύχει με εξαίρεση ανάλογα με τη μέθοδο που εφαρμόζεται στο TaskCompletionSource. Εάν εξακολουθεί να μην είναι ξεκάθαρο, ας δούμε αυτό το παράδειγμα κώδικα, όπου κάποιο παλιό API EAP είναι τυλιγμένο σε μια Εργασία χρησιμοποιώντας ένα TaskCompletionSource: όταν ενεργοποιηθεί το συμβάν, η Εργασία θα μεταφερθεί στην κατάσταση Ολοκληρωμένη και τη μέθοδο που εφάρμοσε τον τελεστή αναμονής σε αυτό το Task θα συνεχίσει την εκτέλεσή του έχοντας λάβει το αντικείμενο αποτέλεσμα.

public static Task<Result> DoAsync(this SomeApiInstance someApiObj) {

    var completionSource = new TaskCompletionSource<Result>();
    someApiObj.Done += 
        result => completionSource.SetResult(result);
    someApiObj.Do();

    result completionSource.Task;
}

Συμβουλές & κόλπα TaskCompletionSource

Η αναδίπλωση παλαιών API δεν είναι το μόνο που μπορεί να γίνει χρησιμοποιώντας το TaskCompletionSource. Η χρήση αυτής της κλάσης ανοίγει μια ενδιαφέρουσα δυνατότητα σχεδιασμού διαφόρων API σε Εργασίες που δεν καταλαμβάνουν νήματα. Και η ροή, όπως θυμόμαστε, είναι ένας ακριβός πόρος και ο αριθμός τους είναι περιορισμένος (κυρίως από την ποσότητα της μνήμης RAM). Αυτός ο περιορισμός μπορεί να επιτευχθεί εύκολα με την ανάπτυξη, για παράδειγμα, μιας φορτωμένης διαδικτυακής εφαρμογής με πολύπλοκη επιχειρηματική λογική. Ας εξετάσουμε τις δυνατότητες για τις οποίες μιλώ όταν εφαρμόζουμε ένα τέτοιο κόλπο όπως το Long-Polling.

Εν ολίγοις, η ουσία του κόλπου είναι η εξής: πρέπει να λαμβάνετε πληροφορίες από το API για ορισμένα συμβάντα που συμβαίνουν από την πλευρά του, ενώ το API, για κάποιο λόγο, δεν μπορεί να αναφέρει το συμβάν, αλλά μπορεί μόνο να επιστρέψει την κατάσταση. Ένα παράδειγμα αυτών είναι όλα τα API που δημιουργήθηκαν πάνω από το HTTP πριν από την εποχή του WebSocket ή όταν ήταν αδύνατο για κάποιο λόγο να χρησιμοποιηθεί αυτή η τεχνολογία. Ο πελάτης μπορεί να ζητήσει από τον διακομιστή HTTP. Ο διακομιστής HTTP δεν μπορεί ο ίδιος να ξεκινήσει την επικοινωνία με τον πελάτη. Μια απλή λύση είναι η δημοσκόπηση του διακομιστή χρησιμοποιώντας ένα χρονόμετρο, αλλά αυτό δημιουργεί ένα πρόσθετο φορτίο στον διακομιστή και μια πρόσθετη καθυστέρηση κατά μέσο όρο TimerInterval / 2. Για να το ξεπεράσετε αυτό, επινοήθηκε ένα τέχνασμα που ονομάζεται Long Polling, το οποίο περιλαμβάνει την καθυστέρηση της απόκρισης από ο διακομιστής μέχρι να λήξει το χρονικό όριο ή να συμβεί ένα συμβάν. Εάν το συμβάν έχει συμβεί, τότε υποβάλλεται σε επεξεργασία, εάν όχι, τότε το αίτημα αποστέλλεται ξανά.

while(!eventOccures && !timeoutExceeded)  {

  CheckTimout();
  CheckEvent();
  Thread.Sleep(1);
}

Αλλά μια τέτοια λύση θα αποδειχθεί τρομερή μόλις αυξηθεί ο αριθμός των πελατών που περιμένουν την εκδήλωση, γιατί... Κάθε τέτοιος πελάτης καταλαμβάνει ένα ολόκληρο νήμα περιμένοντας ένα συμβάν. Ναι, και έχουμε μια επιπλέον καθυστέρηση 1 ms όταν ενεργοποιείται το συμβάν, τις περισσότερες φορές αυτό δεν είναι σημαντικό, αλλά γιατί να κάνουμε το λογισμικό χειρότερο από ό,τι μπορεί; Εάν αφαιρέσουμε το Thread.Sleep(1), τότε μάταια θα φορτώσουμε έναν πυρήνα επεξεργαστή 100% αδρανές, περιστρέφοντας σε έναν άχρηστο κύκλο. Χρησιμοποιώντας το TaskCompletionSource, μπορείτε εύκολα να δημιουργήσετε ξανά αυτόν τον κώδικα και να λύσετε όλα τα προβλήματα που προσδιορίστηκαν παραπάνω:

class LongPollingApi {

    private Dictionary<int, TaskCompletionSource<Msg>> tasks;

    public async Task<Msg> AcceptMessageAsync(int userId, int duration) {

        var cs = new TaskCompletionSource<Msg>();
        tasks[userId] = cs;
        await Task.WhenAny(Task.Delay(duration), cs.Task);
        return cs.Task.IsCompleted ? cs.Task.Result : null;
    }

    public void SendMessage(int userId, Msg m) {

        if (tasks.TryGetValue(userId, out var completionSource))
            completionSource.SetResult(m);
    }
}

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

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

ValueTask: γιατί και πώς

Οι τελεστές async/wait, όπως ο τελεστής επιστροφής απόδοσης, δημιουργούν μια μηχανή κατάστασης από τη μέθοδο, και αυτή είναι η δημιουργία ενός νέου αντικειμένου, το οποίο σχεδόν πάντα δεν είναι σημαντικό, αλλά σε σπάνιες περιπτώσεις μπορεί να δημιουργήσει πρόβλημα. Αυτή η περίπτωση μπορεί να είναι μια μέθοδος που καλείται πολύ συχνά, μιλάμε για δεκάδες και εκατοντάδες χιλιάδες κλήσεις ανά δευτερόλεπτο. Εάν μια τέτοια μέθοδος είναι γραμμένη με τέτοιο τρόπο ώστε στις περισσότερες περιπτώσεις να επιστρέφει ένα αποτέλεσμα παρακάμπτοντας όλες τις μεθόδους αναμονής, τότε το .NET παρέχει ένα εργαλείο για τη βελτιστοποίηση - τη δομή ValueTask. Για να το καταλάβουμε, ας δούμε ένα παράδειγμα χρήσης του: υπάρχει μια κρυφή μνήμη στην οποία πηγαίνουμε πολύ συχνά. Υπάρχουν κάποιες τιμές σε αυτό και, στη συνέχεια, απλώς τις επιστρέφουμε· αν όχι, τότε πηγαίνουμε σε κάποια αργή IO για να τις λάβουμε. Θέλω να κάνω το τελευταίο ασύγχρονα, πράγμα που σημαίνει ότι ολόκληρη η μέθοδος αποδεικνύεται ασύγχρονη. Έτσι, ο προφανής τρόπος γραφής της μεθόδου είναι ο εξής:

public async Task<string> GetById(int id) {

    if (cache.TryGetValue(id, out string val))
        return val;
    return await RequestById(id);
}

Λόγω της επιθυμίας να βελτιστοποιήσετε λίγο και ενός ελαφρού φόβου για το τι θα δημιουργήσει η Roslyn κατά τη σύνταξη αυτού του κώδικα, μπορείτε να ξαναγράψετε αυτό το παράδειγμα ως εξής:

public Task<string> GetById(int id) {

    if (cache.TryGetValue(id, out string val))
        return Task.FromResult(val);
    return RequestById(id);
}

Πράγματι, η βέλτιστη λύση σε αυτήν την περίπτωση θα ήταν η βελτιστοποίηση του hot-path, δηλαδή η απόκτηση μιας τιμής από το λεξικό χωρίς περιττές εκχωρήσεις και φορτίο στο GC, ενώ σε εκείνες τις σπάνιες περιπτώσεις που χρειάζεται ακόμα να πάμε στο IO για δεδομένα , όλα θα παραμείνουν ένα συν/πλην με τον παλιό τρόπο:

public ValueTask<string> GetById(int id) {

    if (cache.TryGetValue(id, out string val))
        return new ValueTask<string>(val);
    return new ValueTask<string>(RequestById(id));
}

Ας ρίξουμε μια πιο προσεκτική ματιά σε αυτό το κομμάτι κώδικα: εάν υπάρχει μια τιμή στη μνήμη cache, δημιουργούμε μια δομή, διαφορετικά η πραγματική εργασία θα τυλιχθεί σε μια ουσιαστική. Ο κωδικός κλήσης δεν ενδιαφέρεται σε ποια διαδρομή εκτελέστηκε αυτός ο κώδικας: Το ValueTask, από άποψη σύνταξης C#, θα συμπεριφέρεται όπως μια κανονική εργασία σε αυτήν την περίπτωση.

Task Schedulers: διαχείριση στρατηγικών εκκίνησης εργασιών

Το επόμενο API που θα ήθελα να εξετάσω είναι η τάξη Χρονοδιάγραμμα εργασιών και τα παράγωγά του. Ανέφερα ήδη παραπάνω ότι το TPL έχει τη δυνατότητα να διαχειρίζεται στρατηγικές για τη διανομή Tasks σε νήματα. Τέτοιες στρατηγικές ορίζονται στους απογόνους της κλάσης TaskScheduler. Σχεδόν οποιαδήποτε στρατηγική μπορεί να χρειαστείτε μπορεί να βρεθεί στη βιβλιοθήκη. ParallelExtensionsExtras, που αναπτύχθηκε από τη Microsoft, αλλά δεν είναι μέρος του .NET, αλλά παρέχεται ως πακέτο Nuget. Ας δούμε εν συντομία μερικά από αυτά:

  • CurrentThreadTaskScheduler — εκτελεί Tasks στο τρέχον νήμα
  • LimitedConcurrencyLevelTaskScheduler — περιορίζει τον αριθμό των Tasks που εκτελούνται ταυτόχρονα από την παράμετρο N, η οποία είναι αποδεκτή στον κατασκευαστή
  • OrderedTaskScheduler — ορίζεται ως LimitedConcurrencyLevelTaskScheduler(1), επομένως οι εργασίες θα εκτελούνται διαδοχικά.
  • WorkStealingTaskScheduler - εργαλεία δουλειά-κλοπή προσέγγιση για την κατανομή εργασιών. Ουσιαστικά είναι ένα ξεχωριστό ThreadPool. Επιλύει το πρόβλημα ότι στο .NET ThreadPool είναι μια στατική κλάση, μία για όλες τις εφαρμογές, πράγμα που σημαίνει ότι η υπερφόρτωση ή η εσφαλμένη χρήση της σε ένα μέρος του προγράμματος μπορεί να οδηγήσει σε παρενέργειες σε ένα άλλο. Επιπλέον, είναι εξαιρετικά δύσκολο να κατανοήσουμε την αιτία τέτοιων ελαττωμάτων. Οτι. Ενδέχεται να υπάρχει ανάγκη χρήσης ξεχωριστών WorkStealingTaskSchedulers σε μέρη του προγράμματος όπου η χρήση του ThreadPool μπορεί να είναι επιθετική και απρόβλεπτη.
  • QueuedTaskScheduler — σας επιτρέπει να εκτελείτε εργασίες σύμφωνα με τους κανόνες ουράς προτεραιότητας
  • ThreadPerTaskScheduler — δημιουργεί ένα ξεχωριστό νήμα για κάθε Εργασία που εκτελείται σε αυτό. Μπορεί να είναι χρήσιμο για εργασίες που χρειάζονται απρόβλεπτα μεγάλο χρόνο για να ολοκληρωθούν.

Υπάρχει μια καλή αναλυτική άρθρο σχετικά με το Task Schedulers στο ιστολόγιο της microsoft.

Για βολικό εντοπισμό σφαλμάτων για οτιδήποτε σχετίζεται με το Tasks, το Visual Studio διαθέτει ένα παράθυρο Tasks. Σε αυτό το παράθυρο μπορείτε να δείτε την τρέχουσα κατάσταση της εργασίας και να μεταβείτε στη γραμμή κώδικα που εκτελείται αυτήν τη στιγμή.

.NET: Εργαλεία για εργασία με multithreading και asynchrony. Μέρος 1

Το PLinq και η τάξη Parallel

Εκτός από τα Tasks και όλα όσα λέγονται για αυτά, υπάρχουν δύο ακόμη ενδιαφέροντα εργαλεία στο .NET: το PLinq (Linq2Parallel) και η κλάση Parallel. Το πρώτο υπόσχεται παράλληλη εκτέλεση όλων των λειτουργιών Linq σε πολλαπλά νήματα. Ο αριθμός των νημάτων μπορεί να ρυθμιστεί χρησιμοποιώντας τη μέθοδο επέκτασης WithDegreeOfParallelism. Δυστυχώς, τις περισσότερες φορές το PLinq στην προεπιλεγμένη λειτουργία του δεν έχει αρκετές πληροφορίες σχετικά με τα εσωτερικά στοιχεία της πηγής δεδομένων σας για να παρέχει σημαντικό κέρδος ταχύτητας, από την άλλη πλευρά, το κόστος της προσπάθειας είναι πολύ χαμηλό: απλά πρέπει να καλέσετε τη μέθοδο AsParallel πριν την αλυσίδα μεθόδων Linq και εκτέλεση δοκιμών απόδοσης. Επιπλέον, είναι δυνατό να μεταβιβάσετε πρόσθετες πληροφορίες στο PLinq σχετικά με τη φύση της πηγής δεδομένων σας χρησιμοποιώντας τον μηχανισμό Partitions. Μπορείτε να διαβάσετε περισσότερα εδώ и εδώ.

Η στατική κλάση Parallel παρέχει μεθόδους για την παράλληλη επανάληψη μέσω μιας συλλογής Foreach, την εκτέλεση ενός βρόχου For και την εκτέλεση πολλαπλών εκπροσώπων σε παράλληλη κλήση. Η εκτέλεση του τρέχοντος νήματος θα σταματήσει μέχρι να ολοκληρωθούν οι υπολογισμοί. Ο αριθμός των νημάτων μπορεί να διαμορφωθεί περνώντας το ParallelOptions ως τελευταίο όρισμα. Μπορείτε επίσης να καθορίσετε το TaskScheduler και το CancellationToken χρησιμοποιώντας επιλογές.

Ευρήματα

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

Συμπεράσματα:

  • Πρέπει να γνωρίζετε τα εργαλεία για την εργασία με νήματα, τον ασύγχρονο και τον παραλληλισμό για να χρησιμοποιήσετε τους πόρους των σύγχρονων υπολογιστών.
  • Το .NET διαθέτει πολλά διαφορετικά εργαλεία για αυτούς τους σκοπούς
  • Δεν εμφανίστηκαν όλα ταυτόχρονα, επομένως μπορείτε συχνά να βρείτε παλαιού τύπου, ωστόσο, υπάρχουν τρόποι μετατροπής παλαιών API χωρίς μεγάλη προσπάθεια.
  • Η εργασία με νήματα στο .NET αντιπροσωπεύεται από τις κλάσεις Thread και ThreadPool
  • Οι μέθοδοι Thread.Abort, Thread.Interrupt και Win32 API TerminateThread είναι επικίνδυνες και δεν συνιστώνται για χρήση. Αντίθετα, είναι καλύτερο να χρησιμοποιήσετε τον μηχανισμό CancellationToken
  • Η ροή είναι πολύτιμος πόρος και η προσφορά του είναι περιορισμένη. Θα πρέπει να αποφεύγονται καταστάσεις όπου τα νήματα είναι απασχολημένα αναμένοντας συμβάντα. Για αυτό είναι βολικό να χρησιμοποιήσετε την κλάση TaskCompletionSource
  • Τα πιο ισχυρά και προηγμένα εργαλεία .NET για εργασία με παραλληλισμό και ασυγχρονισμό είναι τα Tasks.
  • Οι τελεστές c# async/wait εφαρμόζουν την έννοια της μη αποκλειστικής αναμονής
  • Μπορείτε να ελέγξετε την κατανομή των εργασιών στα νήματα χρησιμοποιώντας κλάσεις που προέρχονται από το TaskScheduler
  • Η δομή ValueTask μπορεί να είναι χρήσιμη για τη βελτιστοποίηση των hot-paths και της κυκλοφορίας μνήμης
  • Τα παράθυρα Tasks και Threads του Visual Studio παρέχουν πολλές πληροφορίες χρήσιμες για τον εντοπισμό σφαλμάτων πολλαπλών νημάτων ή ασύγχρονου κώδικα
  • Το PLinq είναι ένα ωραίο εργαλείο, αλλά μπορεί να μην έχει αρκετές πληροφορίες σχετικά με την πηγή δεδομένων σας, αλλά αυτό μπορεί να διορθωθεί χρησιμοποιώντας τον μηχανισμό διαμερισμάτων
  • Για να συνεχιστεί ...

Πηγή: www.habr.com

Προσθέστε ένα σχόλιο