DEV Community

Task, Thread και async/await στο .NET

Η σωστή διαχείριση συναρτήσεων που τρέχουν παράλληλα ή ασύγχρονα είναι κρίσιμη για απόδοση και scalability σε .NET εφαρμογές.


1. Τι είναι Thread στο .NET

  • Thread: Μικρότερο execution unit που τρέχει εντός του process.
  • Κάθε thread έχει τη δική του stack memory, registers, και context.
Thread thread = new Thread(() =>
{
    Console.WriteLine("Running in a separate thread!");
});
thread.Start();

Enter fullscreen mode Exit fullscreen mode

Χαρακτηριστικά:

  • Heavyweight: κάθε thread έχει κόστος μνήμης (~1MB stack)
  • Manual management: πρέπει να δημιουργήσεις, ξεκινήσεις και να συγχρονίσεις threads
  • Parallel execution: μπορεί να τρέξει ταυτόχρονα με άλλα threads

2. Τι είναι Task στο .NET

  • Task: Αντικείμενο που αναπαριστά μια μελλοντική εργασία.
  • Διαχειρίζεται execution, scheduling και completion μέσω ThreadPool.
Task.Run(() =>
{
    Console.WriteLine("Running asynchronously via Task!");
});

Enter fullscreen mode Exit fullscreen mode

Πλεονεκτήματα Tasks έναντι raw threads:

1. Ελαφρύτερα (χρησιμοποιούν ThreadPool)
2. Εύκολο chaining (ContinueWith, await)
3. Συνδυάζουν exception handling με try/catch
4. Better scalability για I/O-bound operations


3. Τι είναι async/await

  • async/await είναι C# syntax που διευκολύνει το asynchronous programming.

  • Σου επιτρέπει να γράφεις ασύγχρονο κώδικα σαν να είναι συγχρονισμένος, χωρίς callbacks ή manual thread management.

public async Task<string> GetDataAsync()
{
    await Task.Delay(1000); // simulate I/O
    return "Hello async!";
}

public async Task RunExample()
{
    string result = await GetDataAsync();
    Console.WriteLine(result);
}

Enter fullscreen mode Exit fullscreen mode

Σημαντικά χαρακτηριστικά:

  • Δεν δημιουργεί νέο thread κάθε φορά
  • Απελευθερώνει το calling thread ενώ περιμένει το αποτέλεσμα (ιδανικό για UI ή web requests)
  • Συνδυάζεται με Task για Task-based Asynchronous Pattern (TAP)

Τι είναι το “calling thread”

Κάθε φορά που τρέχεις κώδικα στο .NET, τρέχει σε ένα thread.

Το thread από το οποίο ξεκινάει μια μέθοδος λέγεται calling thread (το “καλείων thread”).

Παράδειγμα:

public async Task DoWorkAsync()
{
    Console.WriteLine($"Start on thread {Thread.CurrentThread.ManagedThreadId}");
    await Task.Delay(1000); // async I/O
    Console.WriteLine($"End on thread {Thread.CurrentThread.ManagedThreadId}");
}

await DoWorkAsync();

Enter fullscreen mode Exit fullscreen mode

Το await DoWorkAsync() εκκινεί η εκτέλεση στο calling thread (π.χ. main thread ή thread του ASP.NET request).

Όταν φτάσει στο await Task.Delay(1000), το thread δεν μένει μπλοκαρισμένο, αλλά απελευθερώνεται για να κάνει άλλες δουλειές.

Όταν τελειώσει η καθυστέρηση, η συνέχιση του κώδικα (Console.WriteLine("End...")) μπορεί να τρέξει σε άλλο thread του ThreadPool.


Γιατί είναι σημαντικό

1. UI applications (WPF, WinForms)
Δεν θέλεις να μπλοκάρει το κύριο UI thread, γιατί η εφαρμογή “παγώνει”.
async/await επιτρέπει να συνεχίσει η μέθοδος χωρίς να μπλοκάρει το UI thread

2. ASP.NET / Web API

  • Αν περιμένεις I/O (DB, HTTP call) σε blocking τρόπο, καταναλώνεις thread από το pool, περιορίζοντας την απόδοση.
  • Με await, το calling thread ελευθερώνεται για άλλα requests.

4. Διαφορές Thread vs Task vs async/await

Feature Thread Task async/await
Execution unit Thread Task (με ThreadPool) Task-based async
Overhead High (stack memory, OS) Low (ThreadPool reuse) Minimal (compiler sugar over Task)
Control Manual start/stop Scheduler-managed Managed by compiler + Task
Exception handling Try/catch στο thread Task.Exception try/catch με await
Best for CPU-bound parallelism CPU & I/O-bound scalable work I/O-bound async programming

5. Παράδειγμα: Συνδυασμός Tasks + async/await

public async Task DownloadFilesAsync()
{
    var urls = new[] { "url1", "url2", "url3" };

    // Δημιουργία πολλαπλών tasks
    var downloadTasks = urls.Select(url => DownloadAsync(url));

    // Περιμένουμε όλα να ολοκληρωθούν
    string[] results = await Task.WhenAll(downloadTasks);

    Console.WriteLine("All files downloaded!");
}

public async Task<string> DownloadAsync(string url)
{
    await Task.Delay(1000); // simulate download
    return $"Downloaded from {url}";
}

Enter fullscreen mode Exit fullscreen mode

Τι βλέπουμε εδώ:

Ο κώδικας φαίνεται συγχρονισμένος, αλλά τρέχει ασύγχρονα

Το calling thread (π.χ. ASP.NET request thread) δεν μπλοκάρεται

ThreadPool διαχειρίζεται τα Tasks αυτόματα


6. Συμβουλές για Senior Developer

  1. Χρησιμοποιούμε Threads μόνο όταν χρειαζόμαστε πραγματικό parallel CPU-bound work
  2. Tasks + async/await είναι standard για I/O-bound (web calls, DB queries, file I/O)
  3. Αποφεύγουμε async void εκτός event handlers
  4. Χρησιμοποιούμε ConfigureAwait(false) σε libraries για καλύτερη απόδοση και αποφυγή deadlocks
  5. Monitor exception propagation: Task exceptions πρέπει πάντα να γίνονται await ή handled

7. Σημείο κλειδί

Thread → χαμηλού επιπέδου, heavyweight, parallel execution
Task → υψηλού επιπέδου, lightweight abstraction πάνω από threads
async/await → syntax sugar για Task-based asynchronous programming, ιδανικό για scalable I/O


Τι συμβαίνει μέσα στο Task και πού “τρέχει”

1. Thread vs Task – το βασικό

  • Ένα Thread είναι το πραγματικό execution unit που τρέχει instructions στον CPU.
  • Ένα Task δεν είναι thread. Είναι ένα αντικείμενο που αντιπροσωπεύει μία εργασία.

Το Task περιγράφει τι πρέπει να γίνει, αλλά δεν αποφασίζει μόνο του πού και πότε θα τρέξει.

2. Πού τρέχει ένα Task

Στο .NET:

  • Όταν κάνεις Task.Run(() => ...) ή Task.Factory.StartNew(...), ο Task scheduler τοποθετεί το Task σε ένα thread του ThreadPool.

  • Το ThreadPool είναι μια δεξαμενή threads που διαχειρίζεται η CLR για να μην δημιουργούμε συνεχώς νέα threads (heavyweight).

  • Αν το Task είναι I/O-bound και χρησιμοποιείς async/await, δεν “κρατάει” thread ενώ περιμένει το αποτέλεσμα. Το runtime ελευθερώνει το thread για άλλα tasks μέχρι να έρθει η απάντηση.

Παράδειγμα CPU-bound Task:

Task.Run(() =>
{
    Console.WriteLine($"Running on thread {Thread.CurrentThread.ManagedThreadId}");
    Thread.Sleep(1000); // καταναλώνει thread
});

Enter fullscreen mode Exit fullscreen mode

Εδώ το Task τρέχει σε ένα thread του ThreadPool και καταναλώνει το thread για 1 δευτερόλεπτο.

Παράδειγμα I/O-bound async Task:

public async Task DownloadAsync()
{
    Console.WriteLine($"Start on thread {Thread.CurrentThread.ManagedThreadId}");
    await Task.Delay(1000); // async I/O
    Console.WriteLine($"End on thread {Thread.CurrentThread.ManagedThreadId}");
}

Enter fullscreen mode Exit fullscreen mode

Το await Task.Delay(1000) δεν κρατάει thread. Όταν η καθυστέρηση τελειώσει, το continuation μπορεί να τρέξει σε άλλο thread του ThreadPool.


3. Task Scheduler & ThreadPool

  • Task Scheduler: αποφασίζει ποιο thread θα εκτελέσει κάθε Task.
  • ThreadPool: παρέχει δεξαμενή επαναχρησιμοποιούμενων threads, ώστε να μην δημιουργούνται/καταστρέφονται συνεχώς threads.
  • Κάθε Task συνήθως δεν χρειάζεται δικό του thread — αν είναι I/O-bound, η CLR μπορεί να εκτελέσει το continuation χωρίς καθόλου νέο thread μέχρι να χρειαστεί.

4. Σημεία-κλειδιά

  1. Task ≠ Thread
  2. Task: εργασία/υπόσχεση
  3. Thread: execution unit που τρέχει instructions

  4. CPU-bound Task → χρειάζεται πραγματικό thread

  5. I/O-bound async Task → μπορεί να μην κρατάει thread, απελευθερώνεται για άλλα tasks

  6. ThreadPool: διαχειρίζεται threads για Tasks, μειώνοντας overhead


5. Visual Representation (λογική)

Task (I/O-bound)

├─> Started -> ThreadPool thread (CPU work, short) -> await I/O

└─> Thread released while waiting -> continuation scheduled on any ThreadPool thread

Task (CPU-bound)

└─> Always runs on ThreadPool thread until complete


Με άλλα λόγια:

Ένα Task τρέχει μέσα σε thread, αλλά δεν δημιουργεί πάντα νέο thread, ειδικά για async/await I/O operations. Το Task είναι “abstraction” πάνω από threads και scheduling, όχι thread από μόνο του.


Async vs Multithreading στο C# .NET

30 Ερωτήσεις για .NET Senior Developer

nikosst

Top comments (0)