1. Giriş
İndi vaxtıdır başa düşək — Task və Thread prinsipcə nədə fərqlənir? Niyə C# illərdir ki, birbaşa thread idarəçiliyinə deyil, Task-lara üstünlük verir? Hansı situasiyalarda əl ilə thread yaratmağa davam etmək olar, hansı hallarda isə task-larla işləmək kifayətdir (və lazımdır)?
Əgər "thread" və "task" sözləri beyninizin qaranlıq guşəsində bir az qarışır və ürəyiniz daha sürətlə döyünürsə — narahat olmayın, tək deyilsiniz. Hətta təcrübəli proqramçılar da paralellik və asinxronluq mövzusunda bəzən çaşırlar.
Gəlin hər şeyi rəflərə düzək. Başlayaq!
Task-ın yaranmasının qısa tarixi
Keçmişdə (.NET 4.0-dan əvvəl) paralel və ya "fon" rejimində kod işlətməyin ən açıq yolu yeni thread yaratmaq idi. Məsələn, new Thread(() => { ... }).Start(); Thread-lər sadədir. Amma onların pis tərəfi odur ki, hər şey sizin çiyninizdədir. Resurs ayırma, həyat dövrü, istisna emalı, sinxronizasiya, monitorinq, miqyaslana bilənlik — bunların hamısı developer-in qayğısıdır. Proqramlaşdırmada bir az tembellik yaxşıdır!
Vəziyyət Task ilə dəyişdi — bu, System.Threading.Tasks.Task adlanan namespace-dən gəlir. Task — thread deyil. Bu daha abstrakt və çevik anlayışdır. Gələcəkdə (bəlkə paralel) icra olunacaq işi təsvir edir.
2. Thread — "Sade thread"
Thread — aşağı səviyyəli icra vahididir, əməliyyat sisteminin ayrılmış resurslarını (öz stack-i, icra kontekstini və s.) təmsil edir. Əgər thread-i əl ilə yaradırsınızsa, onun işə düşməsi, bitməsi və həyatının bütün incəlikləri sizin məsuliyyətinizdir.
using System;
using System.Threading;
class Program
{
static void Main()
{
Thread thread = new Thread(() => {
Console.WriteLine("Salam thread-dən!");
});
thread.Start();
thread.Join(); // Thread-in bitməsini gözləyirik
}
}
- Burada biz thread yaratdıq və o öz stack-də lambda icra edir.
- Thread-i işə saldıqdan sonra onun bitməsini gözləmək üçün Join() çağırırıq.
Nəyin pərdəarxası var?
- Hər bir thread yaddaş tutumuna malikdir (stack, təxminən 1 MB).
- .NET-də minlərlə thread-i əl ilə yaratmaq tövsiyə olunmur — sistem əziyyət çəkər.
- Join() çağırmağı unutmusunuzsa, əsas thread uşaqdan əvvəl bitə bilər və proqram "kəsilə" bilər.
- Thread daxilindəki istisnalar avtomatik yuxarı çıxmır — onları xüsusi şəkildə tutmaq lazımdır!
- Thread-i "gözəl" şəkildə ləğv etmək çətindir (heç bir Stop() metodu yoxdur!).
3. Task — "Yeni nəsil task-lar"
Task — daha ağıllı abstraksiyadır, "gələcəkdə icra olunacaq iş"i təmsil edir. Task-lar adətən ThreadPool üzərində işləyir, bu da çoxlu sayda thread yaratmaqdan daha effektivdir. Siz onların yaradılmasını əl ilə idarə etmirsiniz, pool sizin üçün bunu sürətli və ağıllı şəkildə edir, yüklənməyə uyğun olaraq thread sayını miqyaslayır.
using System;
using System.Threading.Tasks;
class Program
{
static async Task Main()
{
Task task = Task.Run(() =>
{
Console.WriteLine("Salam Task-dan!");
});
await task; // Task-in bitməsini gözləyirik
}
}
- Burada task mütləq ayrı bir thread-də işə düşəcəyini zəmanət etmir, amma adətən pool-dan bir thread istifadə olunur.
- Task-in bitməsini await ilə asanlıqla gözləyə bilərsiniz (asinxron metodda) və ya sinxron kontekstdə task.Wait() ilə.
4. Task və Thread arasındakı fərq nədir?
Gəlin fərqləri sıraya düzək — nə üçün hansı məqsədlə istifadə olunur və hansı (görünməz) tələlər var.
| Thread | Task | |
|---|---|---|
| Abstraksiya | OS thread-i | İş/Task (abstraksiya, hansı ki mümkündir thread istifadə etsin) |
| İşə salma | Through new Thread(...).Start() | Through Task.Run(...), Task.Factory.StartNew(...), async-metodlar |
| Birbaşa idarəetmə | Bəli (start, Join, prioritet və s.) | Yox, .NET idarə edir |
| Thread pool | Xeyr, thread hər dəfə yeni yaradılır | Bəli, adətən ThreadPool istifadə olunur |
| Resursların idarəsi | Öz stack ayrılır | Resurslar pool tərəfindən reuse olunur |
| Miqyaslana bilənlik | Pis: 1000+ thread üçün səmərəsiz | Ağıllı: minlərlə task yaxşı işləyir |
| İnteraksiya | OS səviyyəsində ayrı thread | Hazırkı thread-in davamı ola bilər, ya da ThreadPool-da ola bilər |
| İstisnalar | Səlahiyyətli tutulmalıdır, əks halda "itə bilər" | İstisnalar Task-də saxlanılır; onları await və ya .Wait() zamanı tuta bilərsiniz |
| Ləğv etmə | Standart yol yoxdur | Bəli, CancellationToken vasitəsilə dəstək var |
| İşin nəticəsini almaq | Join() ilə gözləmək | await, .Wait(), .Result |
| İstifadə məqsədi | Xüsusi hallar — UI thread-ləri, long-lived thread-lər | Daha çox fon/paralel işlər üçün |
5. Nə vaxt nədən istifadə etmək?
Qəbul edilə bilər ki, Thread nə vaxt?
Açığı desəm, müasir .NET kodunda əl ilə thread yaratmaq olduqca nadir hallarda tələb olunur. Aşağıdakı hallarda məntiqlidir:
- Çox uzun müddət işləyən thread yaratmaq lazımdır (məsələn, radio siqnalının serializasiyası və ya hardware-dən məlumat emalı) və bu thread "xüsusi" olmalıdır: aşağı prioritet, öz culture, ayrıca ad.
- Bəzi aşağı səviyyəli API-lərlə inteqrasiya zamanı, hansı ki manual thread idarəsi tələb edir.
- Ciddi spesifik hallarda, məsələn özəl task scheduler-lər yaradarkən.
Qalan bütün hallarda — Task daha düzgün və müasir seçimdir.
Task nə vaxt?
Təxminən həmişə, iş "fon"da və ya paralel icra olunmalıdırsa:
- Pula uyğun olan hər cür fon hesablama (məsələn server sorğusunun emalı, fayl parsing, mail göndərmə).
- Asinxron əməliyyatların işə salınması (async/await) — mexanizm Task və ya Task<T> qaytarır.
- Task-ların birləşdirilməsi, davamlılıqların (continuations) emalı, zəncirlərlə işləmək.
- Ləğv etmənin, gözləmə və nəticələrin toplanmasının sadəliyi: Task CancellationToken-u dəstəkləyir və müasir API-larla yaxşı inteqrasiya olunur.
- Asinxron I/O əməliyyatları: şəbəkə sorğuları, fayllarla işləmə, verilənlər bazası əməliyyatları.
Müqayisə
| Ssenari | Thread | Task |
|---|---|---|
| Long-lived thread (məsələn, öz servisin) | Bəli | Xeyr |
| Qısa tapşırıqların kütləvi icrası | Xeyr | Bəli |
| Asinxron I/O-əməliyyatları (await) | Xeyr | Bəli |
| Task-ların birləşdirilməsi, ləğvi, zəncirlər | Xeyr | Bəli |
| Prioritet və culture-in incə tənzimlənməsi | Bəli (amma nadir) | Xeyr, standart task-lar üçün yox |
| CPU arasında sadə iş bölgüsü | Bəzən | Bəli |
6. Faydalı nüanslar
Task həmişə thread deyil!
Ən güclü magiya budur: əgər siz Task-ı asinxron I/O əməliyyatları üçün istifadə edirsinizsə, yeni thread ümumiyyətlə yaradılmır! Hər şey IO Completion Ports və ya digər platform primitivləri üzərində "səssizcə" gedir. Gözləmə vaxtı heç bir thread məşğul olmur!
Task və asinxronluq (I/O-bound) — await magiyası
using System;
using System.Net.Http;
using System.Threading.Tasks;
class Program
{
static async Task Main()
{
// Asinxron olaraq saytın məzmununu yükləyirik (I/O-bound)
HttpClient client = new HttpClient();
string data = await client.GetStringAsync("https://www.dotnetfoundation.org");
Console.WriteLine($"Alınan simvolların sayı: {data.Length}");
}
}
- Burada task (Task<string>) asinxron I/O əməliyyatını inkapsulyasiya edir.
- Thread bloklanmır — o işləməyə davam edir, yükləmə bitəndə metodun icrası davam edir.
- Belə iş üçün əl ilə thread yaratmaq tamamilə artıq və səmərəsizdir.
Task və ThreadPool
Siz Task.Run(...) yazanda və ya asinxron API istifadə edəndə (await), .NET adətən xüsusi thread pool — ThreadPool-u istifadə edir. Bu, əvvəlcədən yaradılmış və ehtiyatda gözləyən thread-lərin toplusudur ki, onlar hər hansı gələn işi tez cəlb edə bilirlər. Az yüklə thread-lər boşda qalır, çox yüklə isə yeni thread-lər ağıllı şəkildə artırılır. Nəticədə tətbiqləriniz task sayı üzrə miqyaslana bilir, sistemə artıq yük düşmür.
new Thread ilə yaradılan thread adətən sistemdə ayrıca "sakin" olur — iş bitəndən sonra pool-a qayıtmır, sadəcə ölür. Bu səbəbdən Task mass-paralellük üçün daha səmərəlidir.
7. Tipik səhvlər və tələlər
Əgər qəflətən retro-programmist olmaq və hər şeyi thread-lərlə yazmaq qərarına gəlsəniz, gözəl macəralar sizi gözləyir: yaddaş sızmaları, mürəkkəb sinxronizasiya, işin ləğv edilə bilməməsi, "asılı" thread-lər (zombi proseslər), istisnaların tutularaq emalı üçün xüsusi API-lərdən istifadə ehtiyacı.
Ən əsası yadınızda saxlayın: "Task" — rahatdır, təhlükəsizdir və müasir tərzdədir. C# ilə işləyərkən əksər hallarda manual thread idarəsinə qayıtmağa heç bir səbəb yoxdur.
GO TO FULL VERSION