Πολλοί προγραμματιστές χρησιμοποιούν async και await καθημερινά χωρίς να γνωρίζουν πραγματικά τι συμβαίνει από κάτω.
Σε αυτό το άρθρο θα εξηγήσουμε από την αρχή και χωρίς κενά:
- τι είναι thread
- τι είναι thread pool
- τι είναι task
- τι κάνουν οι λέξεις async και await
- τι σημαίνει "syntactic sugar"
- τι πραγματικά κάνει ο compiler
Το άρθρο βασίζεται στη συμπεριφορά της πλατφόρμας .NET και της γλώσσας C#.
1. Τι είναι ένα Thread
Ένα thread (νήμα εκτέλεσης) είναι ο μικρότερος τρόπος με τον οποίο μπορεί να εκτελεστεί κώδικας σε ένα πρόγραμμα.
Σκέψου ένα πρόγραμμα σαν ένα εργοστάσιο.
Το εργοστάσιο είναι η εφαρμογή.
Οι εργαζόμενοι είναι τα threads.
Κάθε thread εκτελεί εντολές του προγράμματος.
Παράδειγμα:
Ένα πρόγραμμα μπορεί να έχει:
- ένα thread για το UI
- ένα thread για υπολογισμούς
- ένα thread για δίκτυο
- Κάθε thread εκτελεί κώδικα ανεξάρτητα.
2. Γιατί δεν δημιουργούμε συνεχώς Threads
Η δημιουργία ενός νέου thread είναι ακριβή διαδικασία για το λειτουργικό σύστημα.
Υπάρχουν αρκετοί λόγοι.
Memory allocation
Όταν δημιουργείται ένα thread, το λειτουργικό σύστημα πρέπει να δεσμεύσει μνήμη για αυτό.
Η μνήμη αυτή χρησιμοποιείται για:
- το stack του thread
- μεταβλητές που χρειάζεται το thread
- εσωτερικά δεδομένα του λειτουργικού συστήματος
Η διαδικασία αυτή λέγεται memory allocation, δηλαδή:
δέσμευση ενός τμήματος μνήμης για χρήση από ένα thread.
Kernel scheduling
Το λειτουργικό σύστημα πρέπει να αποφασίσει ποιο thread θα εκτελεστεί κάθε στιγμή στον επεξεργαστή.
Αυτό το κάνει ένα κομμάτι του λειτουργικού συστήματος που λέγεται kernel.
Η διαδικασία με την οποία ο kernel αποφασίζει ποιο thread θα τρέξει λέγεται:
kernel scheduling
Δηλαδή:
ο προγραμματισμός εκτέλεσης των threads από το λειτουργικό σύστημα.
Context switch
Ο επεξεργαστής μπορεί να εκτελεί ένα thread τη φορά ανά πυρήνα.
Όταν το λειτουργικό σύστημα αλλάζει thread, πρέπει να:
- αποθηκεύσει την κατάσταση του τρέχοντος thread
- φορτώσει την κατάσταση ενός άλλου thread
- συνεχίσει την εκτέλεση
Αυτή η αλλαγή λέγεται:
context switch
Δηλαδή:
αλλαγή από ένα thread σε ένα άλλο.
Η διαδικασία αυτή κοστίζει χρόνο.
3. Τι είναι το Thread Pool
Για να αποφύγουμε τη συνεχή δημιουργία threads, το runtime της πλατφόρμας .NET χρησιμοποιεί κάτι που λέγεται:
Thread Pool
Το Thread Pool είναι μια συλλογή από threads που έχουν δημιουργηθεί ήδη και περιμένουν δουλειά.
Αντί να δημιουργούμε νέο thread κάθε φορά:
- το πρόγραμμα στέλνει μια εργασία
- η εργασία μπαίνει σε μια ουρά
- ένα διαθέσιμο thread από το pool την εκτελεί
Έτσι:
- δεν δημιουργούνται συνεχώς νέα threads
- μειώνεται το κόστος
- αυξάνεται η απόδοση
4. Τι είναι το Task
Ένα Task είναι μια αναπαράσταση μιας εργασίας που θα ολοκληρωθεί στο μέλλον.
Πολύ σημαντικό:
Task δεν είναι thread.
Ένα Task είναι απλά ένα αντικείμενο που λέει:
Υπάρχει μια εργασία που θα ολοκληρωθεί αργότερα.
Παράδειγμα:
Task result = CalculateAsync();
Αυτό σημαίνει:
- η εργασία εκτελείται
- κάποια στιγμή θα επιστρέψει έναν αριθμό
Το Task απλά κρατά πληροφορίες για την κατάσταση της εργασίας.
5. Τι σημαίνει Async
Η λέξη async μπαίνει πριν από μια μέθοδο.
Παράδειγμα:
async Task<string> GetData()
{
}
Η λέξη async δηλώνει ότι:
η μέθοδος μπορεί να περιέχει await.
Δεν δημιουργεί νέο thread.
Δεν κάνει την μέθοδο αυτόματα ασύγχρονη.
Απλά επιτρέπει τη χρήση του await.
6. Τι κάνει το Await
Το await χρησιμοποιείται για να περιμένουμε την ολοκλήρωση ενός Task.
Παράδειγμα:
var data = await GetDataAsync();
Η διαφορά από την κανονική αναμονή είναι σημαντική.
Με απλή αναμονή το thread μπλοκάρει.
Με await το thread ελευθερώνεται για να κάνει άλλη δουλειά.
Η διαδικασία είναι η εξής:
- ξεκινά το Task
- αν δεν έχει ολοκληρωθεί
- η μέθοδος "παγώνει"
- το thread επιστρέφει στο Thread Pool
- όταν ολοκληρωθεί το Task
- η μέθοδος συνεχίζει από το σημείο που σταμάτησε
7. Παράδειγμα Async με καθυστέρηση
async Task Example()
{
Console.WriteLine("Start");
await Task.Delay(2000);
Console.WriteLine("End");
}
Τι συμβαίνει:
- εκτελείται το Start
- ξεκινά το Delay
- η μέθοδος σταματά προσωρινά
- το thread ελευθερώνεται
- μετά από 2 δευτερόλεπτα
- η μέθοδος συνεχίζει
- εκτελείται το End
8. Async δεν σημαίνει νέο Thread
Ένα πολύ συχνό λάθος είναι να πιστεύουμε ότι:
async = νέο thread
Αυτό δεν ισχύει.
Στις περισσότερες περιπτώσεις async χρησιμοποιείται για I/O εργασίες.
Παραδείγματα:
- HTTP requests
- database queries
- file system
- network calls
Σε αυτές τις περιπτώσεις:
το πρόγραμμα στέλνει το αίτημα και απλά περιμένει απάντηση.
Κατά τη διάρκεια της αναμονής δεν χρειάζεται thread να δουλεύει συνεχώς.
9. Τι σημαίνει Syntactic Sugar
Οι λέξεις async και await είναι κάτι που λέγεται:
syntactic sugar
Αυτό σημαίνει:
ένας πιο απλός τρόπος γραφής για κάτι που θα μπορούσε να γραφτεί πιο δύσκολα.
Πριν υπάρξει το async/await γράφαμε κώδικα με callbacks.
Παράδειγμα:
GetDataAsync().ContinueWith(t =>
{
Console.WriteLine(t.Result);
});
Με async/await:
var data = await GetDataAsync();
Console.WriteLine(data);
Ο δεύτερος τρόπος είναι πολύ πιο κατανοητός.
10. Τι κάνει ο Compiler
Όταν γράφουμε async μέθοδο, ο compiler της C# δεν την αφήνει έτσι.
Την μετατρέπει σε κάτι πιο σύνθετο που λέγεται:
state machine
Μια state machine είναι ένας μηχανισμός που:
- θυμάται σε ποιο σημείο βρισκόταν η μέθοδος
- ξέρει από πού να συνεχίσει μετά το await
Έτσι η μέθοδος μπορεί να:
- σταματήσει
- αποθηκεύσει την κατάσταση της
- συνεχίσει αργότερα
11. Πραγματικό παράδειγμα με HTTP
async Task<string> Download()
{
var client = new HttpClient();
return await client.GetStringAsync("https://example.com");
}
Τι συμβαίνει:
- στέλνεται το HTTP request
- το πρόγραμμα περιμένει απάντηση
- το thread ελευθερώνεται
- όταν έρθει η απάντηση
- η μέθοδος συνεχίζει
- επιστρέφεται το αποτέλεσμα
12. Πότε πρέπει να χρησιμοποιούμε Async
Το async είναι πολύ χρήσιμο σε εφαρμογές όπως:
- web APIs
- servers
- εφαρμογές με UI
- εφαρμογές που κάνουν πολλά network calls
Παράδειγμα framework:
ASP.NET Core
Εκεί το async επιτρέπει σε έναν server να εξυπηρετεί πολλούς χρήστες ταυτόχρονα χωρίς να χρειάζεται πολλά threads.
13. Πότε δεν είναι χρήσιμο
Το async δεν βοηθά σε εργασίες που χρησιμοποιούν έντονα τον επεξεργαστή.
Παραδείγματα:
- επεξεργασία εικόνας
- πολύπλοκοι μαθηματικοί υπολογισμοί
- machine learning
- συμπίεση δεδομένων
Σε αυτές τις περιπτώσεις χρειαζόμαστε πραγματικά threads ή παράλληλη εκτέλεση.
Συμπέρασμα
Ας συνοψίσουμε τις βασικές έννοιες.
Thread
είναι ένα νήμα εκτέλεσης κώδικα.
Thread Pool
είναι μια συλλογή από έτοιμα threads που επαναχρησιμοποιούνται.
Task
είναι μια αναπαράσταση μιας εργασίας που θα ολοκληρωθεί στο μέλλον.
async
επιτρέπει την χρήση του await.
await
περιμένει την ολοκλήρωση μιας εργασίας χωρίς να μπλοκάρει το thread.
syntactic sugar
είναι ένας πιο απλός τρόπος γραφής για κάτι πιο σύνθετο.
Top comments (0)