27-Bob: Compute-Bound Asinxron Operatsiyalar
CLR thread pool, tasklar, Parallel sinfining statik metodlari, Parallel LINQ, taymerlar va thread pool boshqaruvi
Ushbu bobda siz operatsiyalarni asinxron tarzda bajarish uchun mavjud bo'lgan turli usullar haqida gaplashamiz. Asinxron compute-bound operatsiyani bajarganda, siz uni boshqa threadlar yordamida bajarasiz. Compute-bound operatsiyalarga ba'zi misollar: kodni kompilyatsiya qilish, imlo tekshirish, grammatika tekshirish, elektron jadval qayta hisoblash, audio yoki video ma'lumotlarni transkod qilish va rasm eskizini (thumbnail) yaratish. Compute-bound operatsiyalar moliyaviy va muhandislik ilovalarida keng tarqalgan.
Aksariyat ilovalar ko'p vaqtini xotiradagi ma'lumotlarni qayta ishlashga yoki hisob-kitoblarni bajarishga sarflamaydi. Buni Task Manager'ni ochib, Performance tabini tanlash orqali tekshirish mumkin. Agar CPU foydalanish darajasi 100 foizdan past bo'lsa (ko'pincha shunday), demak jarayonlaringiz mashinangizdagi barcha CPU yadrolari tomonidan taqdim etilgan hisoblash quvvatini to'liq ishlatmayapti. CPU foydalanish 100 foizdan past bo'lganda, ba'zi (agar hammasi bo'lmasa) threadlar umuman ishlamayapti. Buning o'rniga, ular biror kiritish yoki chiqish operatsiyasini kutmoqda. Masalan, bu threadlar taymerning tugashini, ma'lumotlar bazasi, veb-xizmat, fayl, tarmoq yoki boshqa qurilmadan ma'lumot o'qilishini yoki yozilishini, yoki klaviatura bosishlari, sichqoncha harakatlarini yoki tugma bosishlarini kutmoqda.
Biroq, I/O-ga yo'naltirilgan ilovalarda ham ba'zan qabul qilingan ma'lumotlar ustida hisob-kitob bajariladi va bu hisob-kitobni parallellashtirish ilovaning o'tkazuvchanligini sezilarli darajada oshirishi mumkin. Ushbu bob umumiy til runtime'ining (CLR) thread pool'ini va u qanday ishlashini hamda undan qanday foydalanishni tanishtiradi. Bu ma'lumot juda muhim, chunki thread pool — sezgir va kengayuvchan ilovalar loyihalash va amalga oshirish imkonini beruvchi asosiy texnologiya. Keyin ushbu bob thread pool orqali compute-bound operatsiyalarni bajarish imkonini beruvchi turli mexanizmlarni ko'rsatadi.
CLR Thread Pool bilan Tanishuv
Oldingi bobda aytilganidek, thread yaratish va yo'q qilish — vaqt jihatidan qimmat operatsiya. Bundan tashqari, ko'p threadlarga ega bo'lish xotira resurslarini isrof qiladi va shuningdek bajariladigan threadlar orasida rejalashtirish va kontekst almashish tufayli ishlashga salbiy ta'sir qiladi.
Bu vaziyatni yaxshilash uchun CLR o'zining thread pool'ini boshqarish kodiga ega. Thread pool'ni ilovangiz foydalanishi uchun mavjud bo'lgan threadlar to'plami deb tasavvur qilishingiz mumkin. Har bir CLR uchun bitta thread pool mavjud; bu thread pool shu CLR tomonidan boshqariladigan barcha AppDomain'lar o'rtasida umumiy. Agar bitta jarayonda bir nechta CLR yuklansan, har bir CLR o'zining thread pool'iga ega.
CLR initsializatsiya bo'lganda, thread pool'da hech qanday thread yo'q. Ichki tarzda, thread pool operatsiya so'rovlari navbatini (queue) yuritadi. Ilovangiz asinxron operatsiya bajarmoqchi bo'lganda, siz thread pool navbatiga yozuv qo'shadigan biror metodni chaqirasiz. Thread pool kodi bu navbatdan yozuvlarni ajratib oladi va ularni thread pool threadiga yuboradi. Agar thread pool'da threadlar bo'lmasa, yangi thread yaratiladi. Thread yaratish ishlash xarajatiga ega (yuqorida muhokama qilinganidek). Biroq, thread pool threadi o'z vazifasini tugatganda, thread yo'q qilinmaydi; buning o'rniga, thread pool'ga qaytariladi va u yerda boshqa so'rovga javob berishni kutib, bo'sh turadi. Thread o'zini yo'q qilmagani uchun qo'shimcha ishlash xarajati yo'q.
Agar ilovangiz thread pool'ga ko'p so'rov yuborsa, thread pool barcha so'rovlarni faqat bitta thread bilan xizmat ko'rsatishga harakat qiladi. Biroq, agar ilovangiz thread pool threadi ko'tara oladigan tezlikdan tezroq so'rovlar navbatga qo'ysa, qo'shimcha threadlar yaratiladi. Oxir-oqibat ilovangiz barcha so'rovlarini oz miqdordagi threadlar bilan bajara oladigan nuqtaga yetadi, shuning uchun thread pool ko'p thread yaratishga hojat qolmaydi.
Agar ilovangiz thread pool'ga so'rov yuborishni to'xtatsa, pool'da hech narsa qilmayotgan ko'p threadlar bo'lishi mumkin. Bu xotira resurslarini isrof qiladi. Thread pool threadi ma'lum vaqt davomida (CLR versiyalariga qarab o'zgarishi mumkin) hech narsa qilmasdan tursa, thread uyg'onadi va resurslarni bo'shatish uchun o'zini yo'q qiladi. Thread o'zini yo'q qilayotgani uchun ishlash xarajati mavjud. Biroq, bu muhim emas, chunki thread o'zini yo'q qilmoqda, ya'ni ilovangiz ko'p ish bajarmayapti.
Thread pool'ning ajoyib tomoni shundaki, u resurslarni isrof qilmaslik uchun kam thread saqlash va ko'p protsessorlar, hyperthreaded protsessorlar va ko'p yadroli protsessorlardan foydalanish uchun ko'proq thread saqlash o'rtasidagi keskinlikni boshqaradi. Thread pool evristik (heuristic) bo'lib, agar ilovangiz ko'p vazifalar bajarishi kerak bo'lsa va CPU'lar mavjud bo'lsa, thread pool ko'proq threadlar yaratadi. Agar ilovangizning ish yuki kamaysa, thread pool threadlari o'zlarini yo'q qiladi.
Oddiy Compute-Bound Operatsiyani Bajarish
Thread pool'ga asinxron compute-bound operatsiyani navbatga qo'yish uchun, odatda ThreadPool sinfi tomonidan aniqlangan quyidagi metodlardan birini chaqirasiz:
static Boolean QueueUserWorkItem(WaitCallback callBack);
static Boolean QueueUserWorkItem(WaitCallback callBack, Object state);
Bu metodlar "ish elementi" (work item) va ixtiyoriy holat ma'lumotlarini thread pool navbatiga qo'yadi va keyin barcha metodlar darhol qaytadi. Ish elementi shunchaki callback parametri bilan aniqlangan metod — u thread pool threadi tomonidan chaqiriladi. Metodga state (holat ma'lumotlari) argumenti orqali bitta parametr uzatilishi mumkin. state parametrisiz QueueUserWorkItem versiyasi callback metodga null uzatadi. Oxir-oqibat pool'dagi biror thread ish elementini qayta ishlaydi va metodingiz chaqiriladi. Siz yozadigan callback metod System.Threading.WaitCallback delegat turiga mos kelishi kerak:
delegate void WaitCallback(Object state);
WaitCallback delegatining, TimerCallback delegatining (ushbu bobning "Davriy Compute-Bound Operatsiyani Bajarish" bo'limida muhokama qilinadi) va ParameterizedThreadStart delegatining (26-bob, "Thread Asoslari"da muhokama qilingan) imzolari bir xil. Agar siz bu imzoga mos metod aniqlasangiz, metodni ThreadPool.QueueUserWorkItem yordamida, System.Threading.Timer obyekti yordamida yoki System.Threading.Thread obyekti yordamida chaqirish mumkin.
Quyidagi kod thread pool threadiga metodni asinxron chaqirishni ko'rsatadi:
using System;
using System.Threading;
public static class Program {
public static void Main() {
Console.WriteLine("Main thread: queuing an asynchronous operation");
ThreadPool.QueueUserWorkItem(ComputeBoundOp, 5);
Console.WriteLine("Main thread: Doing other work here...");
Thread.Sleep(10000); // Simulating other work (10 seconds)
Console.WriteLine("Hit <Enter> to end this program...");
Console.ReadLine();
}
// This method's signature must match the WaitCallback delegate
private static void ComputeBoundOp(Object state) {
// This method is executed by a thread pool thread
Console.WriteLine("In ComputeBoundOp: state={0}", state);
Thread.Sleep(1000); // Simulates other work (1 second)
// When this method returns, the thread goes back
// to the pool and waits for another task
}
}
Bu kodni kompilyatsiya qilib ishga tushirganda, quyidagi natijani olaman:
Main thread: queuing an asynchronous operation
Main thread: Doing other work here...
In ComputeBoundOp: state=5
Ba'zan esa quyidagi natija chiqadi:
Main thread: queuing an asynchronous operation
In ComputeBoundOp: state=5
Main thread: Doing other work here...
Natijadagi satrlar tartibining farqi shundan kelib chiqadiki, ikki metod bir-biriga nisbatan asinxron ishlaydi. Windows rejalashtiruvchisi (scheduler) qaysi threadni birinchi rejalashtirish kerakligini aniqlaydi yoki agar ilova ko'p CPU'li mashinada ishlayotgan bo'lsa, ikkala threadni bir vaqtda rejalashtirishi mumkin.
Agar callback metod ishlov berilmagan istisno (exception) tashlasa, CLR jarayonni tugatadi (agar xost o'z siyosatini qo'llamasa). Ishlov berilmagan istisnolar 20-bobda, "Istisnolar va Holat Boshqaruvi"da muhokama qilingan.
Windows Store ilovalari uchun System.Threading.ThreadPool sinfi ommaviy ravishda ochilmagan. Biroq, u System.Threading.Tasks nomlar fazosidagi turlardan foydalanganingizda bilvosita ishlatiladi (ushbu bobning keyingi "Tasklar" bo'limiga qarang).
Execution Kontekstlar (Bajarish Kontekstlari)
Har bir thread o'zi bilan bog'liq execution kontekst (bajarish konteksti) ma'lumotlar strukturasiga ega. Execution kontekst xavfsizlik sozlamalari (siqilgan stek, Threadning Principal xossasi va Windows identifikatsiyasi), xost sozlamalari (System.Threading.HostExecutionContextManagerga qarang) va mantiqiy chaqiruv kontekst ma'lumotlari (System.Runtime.Remoting.Messaging.CallContextning LogicalSetData va LogicalGetData metodlariga qarang) kabi narsalarni o'z ichiga oladi. Thread kod bajarayotganda, ba'zi operatsiyalar threadning execution kontekst sozlamalari qiymatlari bilan ta'sirlanadi. Bu, ayniqsa, xavfsizlik sozlamalari uchun to'g'ri.
Thread boshqa (yordamchi) threaddan vazifalar bajarish uchun foydalanganda, bosh threadning execution konteksti yordamchi threadga oqishi (ko'chirilishi) kerak. Bu yordamchi thread(lar) tomonidan bajariladigan har qanday operatsiyalar bir xil xavfsizlik sozlamalari va xost sozlamalari bilan bajarilishini ta'minlaydi.
Standart holda, CLR bosh threadning execution kontekstini har qanday yordamchi threadlarga avtomatik oqishga majbur qiladi. Bu kontekst ma'lumotlarini yordamchi threadga uzatadi, lekin bu ishlash xarajatiga ega, chunki execution kontekstda juda ko'p ma'lumot mavjud va bularning barchasini yig'ish va yordamchi threadga ko'chirish ancha vaqt oladi.
System.Threading nomlar fazosida, threadning execution kontekstini bir threaddan boshqasiga oqishini boshqarish imkonini beruvchi ExecutionContext sinfi mavjud:
public sealed class ExecutionContext : IDisposable, ISerializable {
[SecurityCritical] public static AsyncFlowControl SuppressFlow();
public static void RestoreFlow();
public static Boolean IsFlowSuppressed();
// Less commonly used methods are not shown
}
Bu sinfdan execution kontekstning oqishini to'xtatish uchun foydalanishingiz mumkin, bu orqali ilovangiz ishlashini yaxshilashingiz mumkin. Server ilovasi uchun ishlash yaxshilanishi ancha sezilarli bo'lishi mumkin. Client ilovasi uchun ko'p ishlash yaxshilanishi yo'q va SuppressFlow metodi [SecurityCritical] atributi bilan belgilangan bo'lib, ba'zi client ilovalarida (masalan, Microsoft Silverlight) chaqirib bo'lmaydi. Execution kontekst oqishini faqat yordamchi thread kontekst ma'lumotlariga muhtoj bo'lmasa to'xtatishingiz kerak.
Quyidagi misol CLR thread pool'iga ish elementi navbatga qo'yilganda execution kontekst oqishini to'xtatishning ma'lumotlarga ta'sirini ko'rsatadi:
public static void Main() {
// Put some data into the Main thread's logical call context
CallContext.LogicalSetData("Name", "Jeffrey");
// Initiate some work to be done by a thread pool thread
// The thread pool thread can access the logical call context data
ThreadPool.QueueUserWorkItem(
state => Console.WriteLine("Name={0}", CallContext.LogicalGetData("Name")));
// Now, suppress the flowing of the Main thread's execution context
ExecutionContext.SuppressFlow();
// Initiate some work to be done by a thread pool thread
// The thread pool thread CANNOT access the logical call context data
ThreadPool.QueueUserWorkItem(
state => Console.WriteLine("Name={0}", CallContext.LogicalGetData("Name")));
// Restore the flowing of the Main thread's execution context in case
// it employs more thread pool threads in the future
ExecutionContext.RestoreFlow();
...
Console.ReadLine();
}
Yuqoridagi kodni kompilyatsiya qilib ishga tushirganda, quyidagi natijani olaman:
Name=Jeffrey
Name=
Bu muhokama ThreadPool.QueueUserWorkItem chaqirish paytida execution kontekst oqishini to'xtatishga qaratilgan bo'lsa-da, bu texnika Task obyektlaridan foydalanganda (ushbu bobning "Tasklar" bo'limida muhokama qilinadi) va asinxron I/O operatsiyalarini boshlashda (28-bob, "I/O-Bound Asinxron Operatsiyalar"da muhokama qilinadi) ham foydalidir.
Kooperativ Bekor Qilish va Timeout
Microsoft .NET Framework operatsiyalarni bekor qilish uchun standart shablon taklif etadi. Bu shablon kooperativ (hamkorlikka asoslangan), ya'ni bekor qilinishini xohlagan operatsiya aniq bekor qilinishni qo'llab-quvvatlashi kerak. Boshqacha aytganda, operatsiyani bajarayotgan kod va operatsiyani bekor qilishga harakat qiladigan kod ikkalasi ham ushbu bo'limda ko'rsatilgan turlardan foydalanishi kerak. Uzoq davom etadigan compute-bound operatsiyalar bekor qilishni taklif qilgani yaxshi, shuning uchun o'zingizning compute-bound operatsiyalaringizga ham bekor qilishni qo'shishni o'ylashingiz kerak.
Operatsiyani bekor qilish uchun avval System.Threading.CancellationTokenSource obyekti yaratish kerak:
public sealed class CancellationTokenSource : IDisposable { // A reference type
public CancellationTokenSource();
public Boolean IsCancellationRequested { get; }
public CancellationToken Token { get; }
public void Cancel(); // Internally, calls Cancel passing false
public void Cancel(Boolean throwOnFirstException);
...
}
Bu obyekt bekor qilish bilan bog'liq barcha holatlarni o'z ichiga oladi.
CancellationToken Strukturasi
CancellationTokenSource obyektini yaratganingizdan so'ng, uning Token xossasini so'rab bir yoki bir nechta CancellationToken (qiymat turi) instansiyalarini olishingiz va ularni bekor qilinishini xohlagan operatsiyalaringizga uzatishingiz mumkin:
public struct CancellationToken { // A value type
public static CancellationToken None { get; } // Very convenient
public Boolean IsCancellationRequested { get; } // Called by non-Task invoked operations
public void ThrowIfCancellationRequested(); // Called by Task-invoked operations
// WaitHandle is signaled when the CancellationTokenSource is canceled
public WaitHandle WaitHandle { get; }
// GetHashCode, Equals, operator== and operator!= members are not shown
public Boolean CanBeCanceled { get; } // Rarely used
public CancellationTokenRegistration Register(Action<Object> callback, Object state,
Boolean useSynchronizationContext); // Simpler overloads not shown
}
CancellationToken instansiyasi engil qiymat turi bo'lib, bitta private maydonga ega: o'zining CancellationTokenSource obyektiga havola. Compute-bound operatsiyaning tsikli CancellationTokenning IsCancellationRequested xossasini davriy tekshirishi mumkin, bu tsikl tugatilishi kerakligini bildiradi. Bu yerning afzalligi shuki, CPU vaqti natijasi sizni qiziqtirmaydigan operatsiyaga sarflanmaydi.
Keling, bularning barchasini misol kod bilan birlashtiraylik:
internal static class CancellationDemo {
public static void Main() {
CancellationTokenSource cts = new CancellationTokenSource();
// Pass the CancellationToken and the number-to-count-to into the operation
ThreadPool.QueueUserWorkItem(o => Count(cts.Token, 1000));
Console.WriteLine("Press <Enter> to cancel the operation.");
Console.ReadLine();
cts.Cancel(); // If Count returned already, Cancel has no effect on it
// Cancel returns immediately, and the method continues running here...
Console.ReadLine();
}
private static void Count(CancellationToken token, Int32 countTo) {
for (Int32 count = 0; count <countTo; count++) {
if (token.IsCancellationRequested) {
Console.WriteLine("Count is cancelled");
break; // Exit the loop to stop the operation
}
Console.WriteLine(count);
Thread.Sleep(200); // For demo, waste some time
}
Console.WriteLine("Count is done");
}
}
Agar operatsiyani bajarishni va bekor qilinishini oldini olishni xohlasangiz, CancellationTokenning statik None xossasidan qaytarilgan tokenni uzatishingiz mumkin. Bu maxsus CancellationToken instansiyasi hech qanday CancellationTokenSource obyekti bilan bog'liq emas (uning private maydoni null), shuning uchun hech qanday kod Cancel ni chaqira olmaydi va operatsiya token'ning IsCancellationRequested xossasini so'raganida doimo false qaytaradi.
CancellationTokenSource Register va Bog'lash
CancellationTokenSourcening Register metodi orqali bekor qilish sodir bo'lganda chaqiriladigan bir yoki bir nechta callback metodlarni ro'yxatdan o'tkazishingiz mumkin. Bu metodga Action<Object> delegat, delegatga uzatiladigan holat qiymati va chaqiruvchi threadning SynchronizationContext obyektidan foydalanish kerakligini ko'rsatuvchi Boolean uzatasiz.
CancellationTokenning Register metodi CancellationTokenRegistrationni qaytaradi:
public struct CancellationTokenRegistration :
IEquatable<CancellationTokenRegistration>, IDisposable {
public void Dispose();
// GetHashCode, Equals, operator== and operator!= members are not shown
}
Ro'yxatdan o'tkazilgan callback'ni CancellationTokenSource dan olib tashlash uchun Dispose ni chaqirishingiz mumkin. Mana bitta CancellationTokenSource bilan ikkita callback ro'yxatdan o'tkazishni ko'rsatadigan kod:
var cts = new CancellationTokenSource();
cts.Token.Register(() => Console.WriteLine("Canceled 1"));
cts.Token.Register(() => Console.WriteLine("Canceled 2"));
// To test, let's just cancel it now and have the 2 callbacks execute
cts.Cancel();
Bu kodni ishga tushirganda, Cancel metodi chaqirilishi bilanoq quyidagi natija chiqadi:
Canceled 2
Canceled 1
Nihoyat, bir nechta CancellationTokenSource obyektlarini bog'lash orqali yangi CancellationTokenSource yaratish mumkin. Bu yangi obyekt bog'langan obyektlarning istalgan birortasi bekor qilinganda bekor qilinadi:
// Create a CancellationTokenSource
var cts1 = new CancellationTokenSource();
cts1.Token.Register(() => Console.WriteLine("cts1 canceled"));
// Create another CancellationTokenSource
var cts2 = new CancellationTokenSource();
cts2.Token.Register(() => Console.WriteLine("cts2 canceled"));
// Create a new CancellationTokenSource that is canceled when cts1 or ct2 is canceled
var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cts1.Token, cts2.Token);
linkedCts.Token.Register(() => Console.WriteLine("linkedCts canceled"));
Ma'lum vaqt o'tgandan keyin operatsiyani bekor qilish ham ko'pincha foydali. Masalan, server ilovasi mijozning so'roviga asoslanib ba'zi hisob-kitoblarni boshlaydi. Lekin server ilovasi mijozga ikki soniya ichida javob berishi kerak. CancellationTokenSource sizga ma'lum vaqtdan so'ng avtomatik bekor qilish imkonini beradi:
public sealed class CancellationTokenSource : IDisposable { // A reference type
public CancellationTokenSource(Int32 millisecondsDelay);
public CancellationTokenSource(TimeSpan delay);
public void CancelAfter(Int32 millisecondsDelay);
public void CancelAfter(TimeSpan delay);
...
}
Tasklar (Vazifalar)
ThreadPoolning QueueUserWorkItem metodini chaqirish asinxron compute-bound operatsiyani boshlash uchun juda oddiy. Biroq, bu texnika ko'p cheklovlarga ega. Eng katta muammo shundaki, operatsiya tugaganini bilishning o'rnatilgan usuli yo'q va operatsiya tugaganda qaytarish qiymatini olishning imkoni yo'q. Bu cheklovlarni va boshqalarni bartaraf etish uchun Microsoft tasklar (vazifalar) kontseptsiyasini joriy qildi va siz ularni System.Threading.Tasks nomlar fazosidagi turlar orqali ishlatasiz.
Shuning uchun, ThreadPoolning QueueUserWorkItem metodini chaqirish o'rniga, tasklar orqali ham xuddi shunday qilishingiz mumkin:
ThreadPool.QueueUserWorkItem(ComputeBoundOp, 5); // Calling QueueUserWorkItem
new Task(ComputeBoundOp, 5).Start(); // Equivalent of preceding using Task
Task.Run(() => ComputeBoundOp(5)); // Another equivalent
Ikkinchi qatorda men Task obyektini yaratmoqdaman va darhol taskni rejalashtirishga topshirish uchun Start ni chaqirmoqdaman. Siz Task obyektini yaratib, keyin Start ni keyinroq chaqirishingiz mumkin. Task yaratish va darhol Start ni chaqirish keng tarqalgani uchun, oldingi kodning oxirgi qatorida ko'rsatilganidek, Taskning qulay statik Run metodini chaqirishingiz mumkin.
Task yaratayotganda, siz konstruktorga bajariladigan operatsiyani ko'rsatuvchi Action yoki Action<Object> delegatini uzatasiz. Task yaratish yoki Run ni chaqirish paytida ixtiyoriy ravishda CancellationToken uzatishingiz mumkin, bu Taskni rejalanishidan oldin bekor qilish imkonini beradi.
Konstruktorga qanday bajarilishini boshqaruvchi TaskCreationOptions bayroqlarini ham uzatishingiz mumkin:
[Flags, Serializable]
public enum TaskCreationOptions {
None = 0x0000,// The default
// Hints to the TaskScheduler that you want this task to run sooner than later.
PreferFairness = 0x0001,
// Hints to the TaskScheduler that it should more aggressively create thread pool threads.
LongRunning = 0x0002,
// Always honored: Associates a Task with its parent Task (discussed shortly)
AttachedToParent = 0x0004,
// If a task attempts to attach to this parent task, it is a normal task, not a child task.
DenyChildAttach = 0x0008,
// Forces child tasks to use the default scheduler as opposed to the parent's scheduler.
HideScheduler = 0x0010
}
Taskni Tugashini Kutish va Natijasini Olish
Tasklar bilan ularning tugashini kutish va keyin natijalarini olish mumkin. Aytaylik, n katta bo'lganda hisoblash jihatidan intensiv Sum metodimiz bor:
private static Int32 Sum(Int32 n) {
Int32 sum = 0;
for (; n > 0; n--)
checked { sum += n; } // if n is large, this will throw System.OverflowException
return sum;
}
Endi Task<TResult> obyektini (Taskdan hosil bo'lgan) yaratishimiz mumkin va compute-bound operatsiyaning qaytarish turi uchun generik TResult argumentini uzatishimiz mumkin. Endi, taskni ishga tushirgandan so'ng, uning tugashini kutishimiz va natijasini olishimiz mumkin:
// Create a Task (it does not start running now)
Task<Int32> t = new Task<Int32>(n => Sum((Int32)n), 1000000000);
// You can start the task sometime later
t.Start();
// Optionally, you can explicitly wait for the task to complete
t.Wait(); // FYI: Overloads exist accepting timeout/CancellationToken
// You can get the result (the Result property internally calls Wait)
Console.WriteLine("The Sum is: " + t.Result); // An Int32 value
Thread Wait metodini chaqirganda, tizim ishga tushirishni kutayotgan yoki allaqachon bajarilishni boshlagan Task borligini tekshiradi. Agar bor bo'lsa, Wait ni chaqirgan thread Task tugagunicha bloklanadi. Lekin agar Task hali bajarilishni boshlamagan bo'lsa, tizim (TaskScheduler ga qarab) Wait ni chaqirgan thread yordamida Taskni bajarishi mumkin. Bu holda, Wait ni chaqirgan thread bloklanmaydi — u Taskni bajaradi va darhol qaytadi. Bu yaxshi, chunki hech qanday thread bloklanmadi. Lekin yomon tomondan, agar thread thread sinxronizatsiya qulfini olgan bo'lsa va keyin Task ham o'sha qulfni olishga harakat qilsa, deadlock yuzaga kelishi mumkin!
Agar compute-bound task ishlov berilmagan istisno tashlasa, istisno yutib yuboriladi, to'plamda saqlanadi va thread pool threadiga pool'ga qaytishga ruxsat beriladi. Wait metodi yoki Result xossasi chaqirilganda, bu a'zolar System.AggregateException obyektini tashlaydi.
AggregateException turi istisno obyektlari to'plamini kapsulalash uchun ishlatiladi (bu ota task bir nechta bola tasklarni ishga tushirganda sodir bo'lishi mumkin). U ReadOnlyCollection<Exception> qaytaruvchi InnerExceptions xossasiga ega. AggregateException shuningdek Flatten metodini taklif etadi — u yangi AggregateException yaratadi va Handle metodi — u AggregateExceptiondagi har bir istisno uchun callback metodini chaqiradi.
Agar siz hech qachon Wait yoki Result ni chaqirmasangiz yoki Taskning Exception xossasini so'ramasangiz, kodingiz ushbu istisno sodir bo'lganini hech qachon ko'rmaydi. Kuzatilmagan istisnolarni aniqlashga yordam berish uchun, TaskSchedulerning statik UnobservedTaskException hodisasiga callback metodini ro'yxatdan o'tkazishingiz mumkin. Bu hodisa CLR'ning finalizator threadi tomonidan kuzatilmagan istisnoga ega Task garbage collect qilinayotganda chaqiriladi.
Bitta taskni kutishdan tashqari, Task sinfi Task obyektlari massivida kutish imkonini beruvchi ikkita statik metodni ham taklif etadi. Taskning statik WaitAny metodi — massivdagi Task obyektlarining istalgan birortasi tugatilguncha chaqiruvchi threadni bloklaydi. Taskning statik WaitAll metodi esa massivdagi barcha Task obyektlari tugatilguncha chaqiruvchi threadni bloklaydi.
Taskni Bekor Qilish
CancellationTokenSource yordamida Taskni bekor qilish mumkin. Avval Sum metodimizni CancellationToken qabul qiladigan qilib o'zgartiramiz:
private static Int32 Sum(CancellationToken ct, Int32 n) {
Int32 sum = 0;
for (; n > 0; n--) {
// The following line throws OperationCanceledException when Cancel
// is called on the CancellationTokenSource referred to by the token
ct.ThrowIfCancellationRequested();
checked { sum += n; } // if n is large, this will throw System.OverflowException
}
return sum;
}
Bu kodda compute-bound operatsiyaning tsikli CancellationTokenning ThrowIfCancellationRequested metodini chaqirish orqali operatsiyaning bekor qilinganini davriy tekshiradi. Bu metod CancellationTokenSource bekor qilingan bo'lsa, OperationCanceledException tashlaydi. Istisnoning sababi shundaki, ThreadPoolning QueueUserWorkItem ish elementlaridan farqli ravishda, tasklarda tugallangan va bekor qilingan taskni ajratish usuli bo'lishi kerak.
Endi CancellationTokenSource va Task obyektlarini quyidagicha yaratamiz:
CancellationTokenSource cts = new CancellationTokenSource();
Task<Int32> t = Task.Run(() => Sum(cts.Token, 1000000000), cts.Token);
// Sometime later, cancel the CancellationTokenSource to cancel the Task
cts.Cancel(); // This is an asynchronous request, the Task may have completed already
try {
// If the task got canceled, Result will throw an AggregateException
Console.WriteLine("The sum is: " + t.Result); // An Int32 value
}
catch (AggregateException x) {
// Consider any OperationCanceledException objects as handled.
// Any other exceptions cause a new AggregateException containing
// only the unhandled exceptions to be thrown
x.Handle(e => e is OperationCanceledException);
// If all the exceptions were handled, the following executes
Console.WriteLine("Sum was canceled");
}
Boshqa Task Tugatilganda Avtomatik Yangi Task Boshlash
Kengayuvchan dasturiy ta'minot yozish uchun threadlaringiz bloklanmasligi kerak. Bu Wait yoki Result ni chaqirish — hali tugamagan task natijasini so'rash — ko'pchilik hollarda thread pool'da yangi thread yaratishiga olib kelishini bildiradi. Yaxshiyamki, task tugaganini bilishning yaxshiroq usuli bor. Task tugaganda, u boshqa taskni boshlashi mumkin:
// Create and start a Task, continue with another task
Task<Int32> t = Task.Run(() => Sum(CancellationToken.None, 10000));
// ContinueWith returns a Task but you usually don't care
Task cwt = t.ContinueWith(task => Console.WriteLine("The sum is: " + task.Result));
Endi Sumni bajarayotgan task tugaganda, bu task natijani ko'rsatuvchi boshqa taskni (thread pool threadida ham) boshlaydi. Oldingi kodni bajarayotgan thread ikkala taskning birortasini ham kutmaydi; u boshqa kodni bajarishi yoki thread pool'ga qaytishi mumkin.
ContinueWith yangi Task obyektiga havola qaytarishini ham e'tibor bering. ContinueWith ni bitta Task obyektida bir nechta marta chaqirish mumkin. Task tugaganda, barcha ContinueWith tasklari thread pool'ga navbatga qo'yiladi.
ContinueWith ni chaqirganingizda TaskContinuationOptions bayroqlarini ham belgilashingiz mumkin:
[Flags, Serializable]
public enum TaskContinuationOptions {
None = 0x0000,// The default
PreferFairness = 0x0001,
LongRunning = 0x0002,
AttachedToParent = 0x0004,
DenyChildAttach = 0x0008,
HideScheduler = 0x0010,
// Prevents completion of the continuation until the antecedent has completed.
LazyCancellation = 0x0020,
// This flag indicates that you want the thread that executed the first task to also
// execute the ContinueWith task. If the first task has already completed, then the
// thread calling ContinueWith will execute the ContinueWith task.
ExecuteSynchronously = 0x80000,
// These flags indicate under what circumstances to run the ContinueWith task
NotOnRanToCompletion = 0x10000,
NotOnFaulted = 0x20000,
NotOnCanceled = 0x40000,
// These flags are convenient combinations of the above three flags
OnlyOnCanceled = NotOnRanToCompletion | NotOnFaulted,
OnlyOnFaulted = NotOnRanToCompletion | NotOnCanceled,
OnlyOnRanToCompletion = NotOnFaulted | NotOnCanceled,
}
Mana barcha bayroqlarni ishlatadigan misol:
// Create and start a Task, continue with multiple other tasks
Task<Int32> t = Task.Run(() => Sum(10000));
// Each ContinueWith returns a Task but you usually don't care
t.ContinueWith(task => Console.WriteLine("The sum is: " + task.Result),
TaskContinuationOptions.OnlyOnRanToCompletion);
t.ContinueWith(task => Console.WriteLine("Sum threw: " + task.Exception.InnerException),
TaskContinuationOptions.OnlyOnFaulted);
t.ContinueWith(task => Console.WriteLine("Sum was canceled"),
TaskContinuationOptions.OnlyOnCanceled);
Task Bola Tasklar Boshlashi Mumkin
Nihoyat, tasklar ota/bola munosabatlarini qo'llab-quvvatlaydi:
Task<Int32[]> parent = new Task<Int32[]>(() => {
var results = new Int32[3]; // Create an array for the results
// This task creates and starts 3 child tasks
new Task(() => results[0] = Sum(10000), TaskCreationOptions.AttachedToParent).Start();
new Task(() => results[1] = Sum(20000), TaskCreationOptions.AttachedToParent).Start();
new Task(() => results[2] = Sum(30000), TaskCreationOptions.AttachedToParent).Start();
// Returns a reference to the array (even though the elements may not be initialized yet)
return results;
});
// When the parent and its children have run to completion, display the results
var cwt = parent.ContinueWith(
parentTask => Array.ForEach(parentTask.Result, Console.WriteLine));
// Start the parent Task so it can start its children
parent.Start();
Bu yerda ota task uchta Task obyektini yaratib ishga tushiradi. Standart holda, boshqa task tomonidan yaratilgan Task obyektlari yaratuvchi task bilan hech qanday aloqasi bo'lmagan yuqori darajali tasklar. Biroq, TaskCreationOptions.AttachedToParent bayrog'i Taskni yaratuvchi Task bilan bog'laydi, shunda yaratuvchi task barcha bolalari (va nabiralari) tugamaguncha tugatilgan hisoblanmaydi.
Task Ichki Tuzilishi
Har bir Task obyektida task holatini tashkil etuvchi maydonlar to'plami mavjud. Ular orasida Int32 ID (Taskning faqat o'qish uchun Id xossasiga qarang), taskning bajarilish holatini ifodalovchi Int32, Task yaratilganda ko'rsatilgan TaskSchedulerga havola, callback metodga havola, callback metodga uzatiladigan obyektga havola, ExecutionContextga havola va ManualResetEventSlim obyektiga havola bor.
Task va Task<TResult> sinflari IDisposable interfeysini amalga oshiradi. Bugungi kunda Dispose metodi faqat ManualResetEventSlim obyektini yopadi. Biroq, dasturchilar Task obyektida Disposeni aniq chaqirmasligini tavsiya qilaman; buning o'rniga, garbage collector kerak bo'lganda resurslarni tozalashiga ruxsat bering.
Har bir Task obyektida taskning noyob ID'sini ifodalovchi Int32 maydoni borligini sezasiz. Task obyektini yaratganingizda, maydon nolga initsializatsiya qilinadi. Id xossasini birinchi marta so'raganingizda, xossa bu maydonga noyob Int32 qiymatini tayinlaydi va qaytaradi. Task ID'lari 1 dan boshlanadi va har bir ID tayinlanishi bilan 1 ga oshadi.
Task obyektining hayot sikli davomida siz Taskning faqat o'qish uchun Status xossasini so'rash orqali qayerdaligini bilib olishingiz mumkin:
public enum TaskStatus {
// These flags indicate the state of a Task during its lifetime:
Created, // Task created explicitly; you can manually Start() this task
WaitingForActivation, // Task created implicitly; it starts automatically
WaitingToRun, // The task was scheduled but isn't running yet
Running, // The task is actually running
// The task is waiting for children to complete before it considers itself complete
WaitingForChildrenToComplete,
// A task's final state is one of these:
RanToCompletion,
Canceled,
Faulted
}
Birinchi marta Task obyektini yaratganingizda, uning statusi Created. Keyinchalik, task boshlanganda statusi WaitingToRunga o'zgaradi. Task aslida threadda ishlaganda statusi Runningga o'zgaradi. Task ishlashni to'xtatib, bola tasklar tugashini kutayotganda, status WaitingForChildrenToCompletega o'zgaradi. Task to'liq tugatilganda, uchta yakuniy holatlardan biriga kiradi: RanToCompletion, Canceled yoki Faulted.
Qulaylik uchun, Task bir nechta faqat o'qish Boolean xossalarni taklif etadi: IsCanceled, IsFaulted va IsCompleted. IsCompleted Task RanToCompletion, Canceled yoki Faulted holatida bo'lganda true qaytarishini e'tibor bering. Task muvaffaqiyatli tugaganini aniqlashning eng oson usuli:
if (task.Status == TaskStatus.RanToCompletion) ...
Task Factories (Task Fabrikalari)
Ba'zan bir xil konfiguratsiyaga ega bo'lgan bir nechta Task obyektlarini yaratishingiz mumkin. Har bir Task konstruktoriga bir xil parametrlarni qayta-qayta uzatmaslik uchun umumiy konfiguratsiyani kapsulalaydigan task fabrikasi yaratishingiz mumkin. System.Threading.Tasks nomlar fazosida TaskFactory turi va TaskFactory<TResult> turi aniqlangan.
Agar void qaytaruvchi tasklar yaratmoqchi bo'lsangiz, TaskFactory konstrukt qilasiz. Agar ma'lum qaytarish turiga ega tasklar yaratmoqchi bo'lsangiz, TaskFactory<TResult> konstrukt qilasiz. Fabrikalarga CancellationToken, TaskScheduler, TaskCreationOptions va TaskContinuationOptions sozlamalarini uzatasiz.
Mana TaskFactory foydalanishni ko'rsatuvchi misol kod:
Task parent = new Task(() => {
var cts = new CancellationTokenSource();
var tf = new TaskFactory<Int32>(cts.Token, TaskCreationOptions.AttachedToParent,
TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default);
// This task creates and starts 3 child tasks
var childTasks = new[] {
tf.StartNew(() => Sum(cts.Token, 10000)),
tf.StartNew(() => Sum(cts.Token, 20000)),
tf.StartNew(() => Sum(cts.Token, Int32.MaxValue)) // Too big, throws OverflowException
};
// If any of the child tasks throw, cancel the rest of them
for (Int32 task = 0; task < childTasks.Length; task++)
childTasks[task].ContinueWith(
t => cts.Cancel(), TaskContinuationOptions.OnlyOnFaulted);
// When all children are done, get the maximum value returned from the
// non-faulting/canceled tasks. Then pass the maximum value to another
// task that displays the maximum result
tf.ContinueWhenAll(
childTasks,
completedTasks =>
completedTasks.Where(t => t.Status == TaskStatus.RanToCompletion).Max(t => t.Result),
CancellationToken.None)
.ContinueWith(t =>Console.WriteLine("The maximum is: " + t.Result),
TaskContinuationOptions.ExecuteSynchronously);
});
// When the children are done, show any unhandled exceptions too
parent.ContinueWith(p => {
// I put all this text in a StringBuilder and call Console.WriteLine just once
// because this task could execute concurrently with the task above & I don't
// want the tasks' output interspersed
StringBuilder sb = new StringBuilder(
"The following exception(s) occurred:" + Environment.NewLine);
foreach (var e in p.Exception.Flatten().InnerExceptions)
sb.AppendLine(" "+ e.GetType().ToString());
Console.WriteLine(sb.ToString());
}, TaskContinuationOptions.OnlyOnFaulted);
// Start the parent Task so it can start its children
parent.Start();
Task Schedulers (Task Rejalashtiruvchilar)
Task infratuzilmasi juda moslashuvchan va TaskScheduler obyektlari bu moslashuvchanlikning katta qismi. TaskScheduler obyekti rejalashtirilgan tasklarni bajarish va task ma'lumotlarini Visual Studio debugger'iga ko'rsatish uchun javobgar. FCL ikkita TaskSchedulerdan hosila bo'lgan turlarni taqdim etadi: thread pool task rejalashtiruvchi va sinxronizatsiya kontekst task rejalashtiruvchisi.
Standart holda, barcha ilovalar thread pool task rejalashtiruvchisidan foydalanadi. Bu task rejalashtiruvchisi tasklarni thread pool'ning worker threadlariga rejalashtiradi. Standart task rejalashtiruvchiga havolani TaskSchedulerning statik Default xossasi orqali olishingiz mumkin.
Sinxronizatsiya kontekst task rejalashtiruvchisi odatda grafik foydalanuvchi interfeysiga ega ilovalar uchun ishlatiladi — masalan, Windows Forms, Windows Presentation Foundation (WPF), Silverlight va Windows Store ilovalari. Bu task rejalashtiruvchisi barcha tasklarni ilovaning GUI threadiga rejalashtiradi, shunda barcha task kodi UI komponentlarini (tugmalar, menyu elementlari va h.k.) muvaffaqiyatli yangilashi mumkin.
Mana sinxronizatsiya kontekst task rejalashtiruvchisining ishlatilishini ko'rsatuvchi oddiy Windows Forms ilovasi:
internal sealed class MyForm : Form {
private readonly TaskScheduler m_syncContextTaskScheduler;
public MyForm() {
// Get a reference to a synchronization context task scheduler
m_syncContextTaskScheduler = TaskScheduler.FromCurrentSynchronizationContext();
Text = "Synchronization Context Task Scheduler Demo";
Visible = true; Width = 600; Height = 100;
}
private CancellationTokenSource m_cts;
protected override void OnMouseClick(MouseEventArgs e) {
if (m_cts != null) { // An operation is in flight, cancel it
m_cts.Cancel();
m_cts = null;
} else { // An operation is not in flight, start it
Text = "Operation running";
m_cts = new CancellationTokenSource();
// This task uses the default task scheduler and executes on a thread pool thread
Task<Int32> t = Task.Run(() => Sum(m_cts.Token, 20000), m_cts.Token);
// These tasks use the sync context task scheduler and execute on the GUI thread
t.ContinueWith(task => Text = "Result: " + task.Result,
CancellationToken.None, TaskContinuationOptions.OnlyOnRanToCompletion,
m_syncContextTaskScheduler);
t.ContinueWith(task => Text = "Operation canceled",
CancellationToken.None, TaskContinuationOptions.OnlyOnCanceled,
m_syncContextTaskScheduler);
t.ContinueWith(task => Text = "Operation faulted",
CancellationToken.None, TaskContinuationOptions.OnlyOnFaulted,
m_syncContextTaskScheduler);
}
base.OnMouseClick(e);
}
}
Microsoft Parallel Extensions Extras paketida mavjud bo'lgan ba'zi boshqa task rejalashtiruvchilari:
- IOTaskScheduler — Bu task rejalashtiruvchisi tasklarni thread pool'ning I/O threadlariga rejalashtiradi.
- LimitedConcurrencyLevelTaskScheduler — Bu task rejalashtiruvchisi bir vaqtda n tadan ortiq taskni bajarishga ruxsat bermaydi.
- OrderedTaskScheduler — Bu task rejalashtiruvchisi bir vaqtda faqat bitta taskni bajarishga ruxsat beradi. Bu sinf
LimitedConcurrencyLevelTaskSchedulerdan hosila bo'lgan va n uchun 1 uzatadi. - PrioritizingTaskScheduler — Bu task rejalashtiruvchisi tasklarni CLR thread pool'iga rejalashtiradi. Keyin
Prioritizeni chaqirib task oddiy tasklardan oldin qayta ishlanishi kerakligini ko'rsatishingiz mumkin. - ThreadPerTaskScheduler — Bu task rejalashtiruvchisi har bir task uchun alohida thread yaratadi va ishga tushiradi; u thread pool'ni umuman ishlatmaydi.
Parallel'ning Statik For, ForEach va Invoke Metodlari
Tasklarning yaxshilangan ishlashidan potentsial foyda ko'rishi mumkin bo'lgan ba'zi keng tarqalgan dasturlash stsenariylari mavjud. Dasturlashni soddalashtirish uchun statik System.Threading.Tasks.Parallel sinfi bu keng tarqalgan stsepariylarni kapsulalaydi va ichki tarzda Task obyektlarini ishlatadi. Masalan, to'plamdagi barcha elementlarni qayta ishlash o'rniga:
// One thread performs all this work sequentially
for (Int32 i = 0; i < 1000; i++) DoWork(i);
Bu ishni bajarishda bir nechta thread pool threadlarini yordamga olishingiz mumkin:
// The thread pool's threads process the work in parallel
Parallel.For(0, 1000, i => DoWork(i));
Xuddi shunday, to'plam uchun:
// One thread performs all this work sequentially
foreach (var item in collection) DoWork(item);
// The thread pool's threads process the work in parallel
Parallel.ForEach(collection, item => DoWork(item));
Agar kodingizda For yoki ForEach ishlatish mumkin bo'lsa, For ni ishlatish tavsiya etiladi, chunki u tezroq bajariladi.
Nihoyat, bir nechta metodlarni ketma-ket bajarish o'rniga ularni parallel bajarishingiz mumkin:
// One thread executes all the methods sequentially
Method1();
Method2();
Method3();
// The thread pool's threads execute the methods in parallel
Parallel.Invoke(
() => Method1(),
() => Method2(),
() => Method3());
Barcha Parallel metodlari chaqiruvchi threadni ham ishga qo'shadi, bu resurs foydalanish nuqtai nazaridan yaxshi. Agar operatsiya ishlov berilmagan istisno tashlasa, chaqirgan Parallel metodi oxir-oqibat AggregateException tashlaydi.
Hamma joyda for tsikllarni Parallel.For bilan almashtirmang. Parallel metodini chaqirganingizda, ish elementlarining bir vaqtda bajarilishi yaxshi degan taxmin mavjud. Shuning uchun, agar ish ketma-ket tartibda qayta ishlanishi kerak bo'lsa, Parallel metodlaridan foydalanmang. Umumiy ma'lumotlarni o'zgartiradigan ish elementlaridan ham qoching, chunki bir nechta thread tomonidan bir vaqtda manipulyatsiya qilinsa, ma'lumotlar buzilishi mumkin.
Parallelning For, ForEach va Invoke metodlarining barcha overloadlari ParallelOptions obyektini qabul qiladigan overloadlarga ega:
public class ParallelOptions{
public ParallelOptions();
// Allows cancellation of the operation
public CancellationToken CancellationToken { get; set; } // Default=CancellationToken.None
// Allows you to specify the maximum number of work items
// that can be operated on concurrently
public Int32 MaxDegreeOfParallelism { get; set; } // Default=-1 (# of available CPUs)
// Allows you to specify which TaskScheduler to use
public TaskScheduler TaskScheduler { get; set; } // Default=TaskScheduler.Default
}
Parallelning For va ForEach metodlari ParallelLoopResult instansiyasini qaytaradi:
public struct ParallelLoopResult {
// Returns false if the operation was ended prematurely
public Boolean IsCompleted { get; }
public Int64? LowestBreakIteration{ get; }
}
Parallel Language Integrated Query (PLINQ)
Microsoft'ning Language Integrated Query (LINQ) xususiyati ma'lumotlar to'plamlari ustida so'rovlar bajarish uchun qulay sintaksis taklif etadi. LINQ yordamida elementlarni osonlik bilan filtrlash, saralash, proektsiya qilish va boshqa ko'p narsalarni qilish mumkin. LINQ to Objects dan foydalanganingizda, faqat bitta thread ma'lumotlar to'plamdagi barcha elementlarni ketma-ket qayta ishlaydi; buni ketma-ket so'rov deb ataymiz.
Parallel LINQ yordamida ketma-ket so'rovingizni parallel so'rovga aylantirish mumkin. Parallel so'rov ichki tarzda tasklardan foydalanadi (standart TaskSchedulerga navbatga qo'yiladi) va to'plama elementlarini bir nechta CPU'lar bo'yicha tarqatadi, shunda bir nechta element bir vaqtda qayta ishlanadi.
Statik System.Linq.ParallelEnumerable sinfi barcha Parallel LINQ funksionalligini amalga oshiradi. Ketma-ket so'rovni parallel so'rovga aylantirish uchun ParallelEnumerablening AsParallel kengaytma metodidan foydalanasiz:
public static ParallelQuery<TSource> AsParallel<TSource>(this IEnumerable<TSource> source)
public static ParallelQuery AsParallel(this IEnumerable source)
Mana parallel so'rovga aylantrilgan ketma-ket so'rov misoli. Bu so'rov assembly ichidagi barcha eskirgan (obsolete) metodlarni qaytaradi:
private static void ObsoleteMethods(Assembly assembly) {
var query =
from type in assembly.GetExportedTypes().AsParallel()
from method in type.GetMethods(BindingFlags.Public |
BindingFlags.Instance | BindingFlags.Static)
let obsoleteAttrType = typeof(ObsoleteAttribute)
where Attribute.IsDefined(method, obsoleteAttrType)
orderby type.FullName
let obsoleteAttrObj = (ObsoleteAttribute)
Attribute.GetCustomAttribute(method, obsoleteAttrType)
select String.Format("Type={0}\nMethod={1}\nMessage={2}\n",
type.FullName, method.ToString(), obsoleteAttrObj.Message);
// Display the results
foreach (var result in query) Console.WriteLine(result);
}
So'rov ichida parallel operatsiyalardan ketma-ket operatsiyalarga qaytish uchun ParallelEnumerablening AsSequential metodini chaqirishingiz mumkin. Parallel LINQ elementlarni bir nechta thread yordamida qayta ishlaganligi sababli, elementlar tartibsiz qaytariladi. Tartibni saqlamoqchi bo'lsangiz, AsOrdered metodini chaqirishingiz mumkin, lekin bu ishlashga ta'sir qiladi.
Parallel LINQ so'rovni qanday qayta ishlashni tahlil qiladi va ba'zan ketma-ket qayta ishlash yaxshiroq natija berishi mumkin. Parallel qayta ishlashni majburlash uchun WithExecutionMode metodini chaqirishingiz mumkin:
public enum ParallelExecutionMode {
Default = 0, // Let Parallel LINQ decide to best process the query
ForceParallelism = 1 // Force the query to be processed in parallel
}
Davriy Compute-Bound Operatsiyani Bajarish
System.Threading nomlar fazosida Timer sinfi aniqlangan bo'lib, undan foydalanib thread pool threadiga metodni davriy ravishda chaqirish mumkin. Timer sinfining instansiyasini yaratganingizda, siz belgilagan kelajakdagi vaqtda callback metodingiz chaqirilishini thread pool'ga aytasiz:
public sealed class Timer : MarshalByRefObject, IDisposable {
public Timer(TimerCallback callback, Object state, Int32 dueTime, Int32 period);
public Timer(TimerCallback callback, Object state, UInt32 dueTime, UInt32 period);
public Timer(TimerCallback callback, Object state, Int64 dueTime, Int64 period);
public Timer(TimerCallback callback, Object state, Timespan dueTime, TimeSpan period);
}
callback parametri thread pool threadi tomonidan chaqiriladigan metodni aniqlaydi. Callback metod System.Threading.TimerCallback delegat turiga mos kelishi kerak:
delegate void TimerCallback(Object state);
state parametri callback metod har safar chaqirilganda unga holat ma'lumotlarini uzatish imkonini beradi. dueTime parametri CLR ga callback metodingizni birinchi marta chaqirishdan oldin necha millisekund kutish kerakligini aytadi. period parametri har bir keyingi callback metod chaqirilishi orasida necha millisekund kutish kerakligini belgilaydi. period uchun Timeout.Infinite (-1) uzatsangiz, thread pool threadi callback metodini faqat bir marta chaqiradi.
Ichki tarzda, thread pool barcha Timer obyektlari uchun foydalaniladigan faqat bitta threadga ega. Bu thread keyingi Timer obyektining vaqti kelganini biladi. Thread pool'ning QueueUserWorkItem metodi ichki chaqiriladi va callback metodingiz chaqiriladi. Agar callback metodingiz uzoq vaqt olsa, taymer yana ishga tushishi va bir nechta thread pool threadlari bir vaqtda callback metodingizni bajarishiga olib kelishi mumkin. Buni hal qilish uchun quyidagicha qilishni tavsiya qilaman: period parametri uchun Timeout.Infinite belgilang. Keyin callback metodingiz ichida yangi boshlanish vaqti va yana Timeout.Infinite belgilab Change metodini chaqiring.
public sealed class Timer : MarshalByRefObject, IDisposable {
public Boolean Change(Int32 dueTime, Int32 period);
public Boolean Change(UInt32 dueTime, UInt32 period);
public Boolean Change(Int64 dueTime, Int64 period);
public Boolean Change(TimeSpan dueTime, TimeSpan period);
}
Mana taymerni darhol boshlaydigan va keyin har ikki soniyada metodni chaqiradigan kod:
internal static class TimerDemo {
private static Timer s_timer;
public static void Main() {
Console.WriteLine("Checking status every 2 seconds");
// Create the Timer ensuring that it never fires. This ensures that
// s_timer refers to it BEFORE Status is invoked by a thread pool thread
s_timer = new Timer(Status, null, Timeout.Infinite, Timeout.Infinite);
// Now that s_timer is assigned to, we can let the timer fire knowing
// that calling Change in Status will not throw a NullReferenceException
s_timer.Change(0, Timeout.Infinite);
Console.ReadLine(); // Prevent the process from terminating
}
// This method's signature must match the TimerCallback delegate
private static void Status(Object state) {
// This method is executed by a thread pool thread
Console.WriteLine("In Status at {0}", DateTime.Now);
Thread.Sleep(1000); // Simulates other work (1 second)
// Just before returning, have the Timer fire again in 2 seconds
s_timer.Change(2000, Timeout.Infinite);
// When this method returns, the thread goes back
// to the pool and waits for another work item
}
}
Agar davriy bajarilishi kerak bo'lgan operatsiya bo'lsa, Taskning statik Delay metodi va C#ning async / await kalit so'zlaridan foydalanib boshqacha qilish ham mumkin (28-bobda batafsil muhokama qilinadi):
internal static class DelayDemo {
public static void Main() {
Console.WriteLine("Checking status every 2 seconds");
Status();
Console.ReadLine(); // Prevent the process from terminating
}
// This method can take whatever parameters you desire
private static async void Status() {
while (true) {
Console.WriteLine("Checking status at {0}", DateTime.Now);
// Put code to check status here...
// At end of loop, delay 2 seconds without blocking a thread
await Task.Delay(2000); // await allows thread to return
// After 2 seconds, some thread will continue after await to loop around
}
}
}
Juda Ko'p Taymerlar, Juda Kam Vaqt
Afsuski, FCL aslida bir nechta taymer bilan birga keladi va aksariyat dasturchilarga har bir taymerni nima noyob qilishini tushunish oson emas. Keling, tushuntirishga harakat qilaman:
- System.Threading.Timer sinfi — Bu oldingi bo'limda muhokama qilingan taymer va u thread pool threadida davriy fon vazifalarini bajarmoqchi bo'lganingizda foydalanish uchun eng yaxshi taymer.
- System.Windows.Forms.Timer sinfi — Bu sinf instansiyasini yaratish Windows'ga taymerni chaqiruvchi thread bilan bog'lashni aytadi. Taymer tugaganda, Windows threadning xabar navbatiga taymer xabarini (
WM_TIMER) kiritadi. Thread xabarlarni ajratib oladigan va callback metodga uzatadigan xabar nasosini bajarishi kerak. Barcha ish bitta thread tomonidan bajarilishini e'tibor bering — taymerni o'rnatgan thread callback metodini bajaradigan thread bilan bir xil. Bu shuningdek taymer metodingiz bir nechta thread tomonidan bir vaqtda bajarilmasligini bildiradi. - System.Windows.Threading.DispatcherTimer sinfi — Bu sinf Silverlight va WPF ilovalari uchun
System.Windows.Forms.Timersinfining ekvivalenti. - Windows.UI.Xaml.DispatcherTimer sinfi — Bu sinf Windows Store ilovalari uchun
System.Windows.Forms.Timersinfining ekvivalenti. - System.Timers.Timer sinfi — Bu taymer aslida
System.Threading.Timeratrofidagi o'ramdir (wrapper). Men bu sinfdan foydalanishni tavsiya etmayman. Buning o'rnigaSystem.Threading.Timersinfidan foydalaning.
Timer obyekti garbage collect qilinganda, uning finalizatsiya kodi taymerini bekor qilish uchun thread pool'ga xabar beradi, shunda u endi ishga tushmaydi. Shuning uchun Timer obyektidan foydalanganingizda, o'zgaruvchi Timer obyektini tirik ushlab turishiga ishonch hosil qiling, aks holda callback metodingiz chaqirilishni to'xtatadi. Bu 21-bobda, "Garbage Collection va Debugging" bo'limida muhokama qilingan va ko'rsatilgan.
Thread Pool O'z Threadlarini Qanday Boshqaradi
Endi thread pool kodi worker va I/O threadlarini qanday boshqarishi haqida gaplashaman. Biroq, ichki amalga oshirish CLR ning har bir versiyasida jiddiy o'zgargan va kelajak versiyalarda ham o'zgarishda davom etishi sababli, ko'p tafsilotlarga kirmayman. Thread pool'ni "qora quti" sifatida ko'rish eng yaxshi, chunki u har qanday bitta ilova uchun mukammal emas — u turli-tuman ilovalar bilan ishlash uchun mo'ljallangan umumiy maqsadli thread-rejalashtirish algoritmdir; ba'zi ilovalar uchun yaxshiroq, boshqalari uchun kamroq yaxshi ishlaydi.
Men sizga thread pool'ga ishonishingizni qat'iy tavsiya qilaman, chunki CLR'da keladigan thread pool'dan yaxshiroq ishlaydigan thread pool'ni yaratish juda qiyin bo'ladi. Vaqt o'tishi bilan, aksariyat ilovalar thread pool kodi threadlarni qanday boshqarishini ichki o'zgartirishi bilan yaxshilanishi kerak.
Thread Pool Chegaralarini Sozlash
CLR dasturchilarga thread pool yaratadigan threadlarning maksimal sonini belgilash imkonini beradi. Biroq, pool'dagi threadlar soniga yuqori cheklov qo'ymaslik kerak, chunki ochlik (starvation) yoki deadlock yuzaga kelishi mumkin. Tasavvur qiling, 1000 ta ish elementini navbatga qo'yasiz va ularning barchasi 1001-elementdagi signal kutayapti. Agar maksimal 1000 ta thread belgilagan bo'lsangiz, 1001-element hech qachon bajarilmaydi va barcha 1000 thread abadiy bloklanadi.
CLR jamoasi thread pool'ga ruxsat berilgan standart maksimal thread sonini doimiy ravishda oshirgan. Hozirgi standart maksimum taxminan 1000 ta thread bo'lib, bu 32-bit jarayon uchun amalda chegarasiz deyiladi. 64-bit jarayon esa nazariy jihatdan yuz minglab threadlarni yaratishi mumkin.
System.Threading.ThreadPool sinfi thread pool'dagi threadlar sonini boshqarish uchun bir nechta statik metodlarni taklif etadi: GetMaxThreads, SetMaxThreads, GetMinThreads, SetMinThreads va GetAvailableThreads. Men bu metodlarning hech birini chaqirmasligingizni qat'iy tavsiya qilaman. Thread pool chegaralari bilan o'ynash odatda ilovani yaxshiroq emas, balki yomonroq ishlashga olib keladi.
Worker Threadlar Qanday Boshqariladi
27-1 rasmda thread pool'ning worker threadlar qismini tashkil etuvchi turli ma'lumotlar strukturalari ko'rsatilgan. ThreadPool.QueueUserWorkItem metodi va Timer sinfi doimo ish elementlarini global navbatga qo'yadi. Worker threadlar bu navbatdan elementlarni birinchi kirgan birinchi chiqadi (FIFO) algoritmi yordamida ajratib oladi va qayta ishlaydi. Bir nechta worker threadlar global navbatdan bir vaqtda element olishi mumkin bo'lganligi sababli, ikkita yoki undan ko'p worker threadlar bir xil ish elementini olmasligini ta'minlash uchun barcha worker threadlar thread sinxronizatsiya qulfini raqobatlashadi.
Thread pool global navbatga ega. Bundan tashqari, har bir worker thread o'zining lokal navbatiga ega. Worker thread Taskni rejalashtirganda, Task threadning lokal navbatiga qo'shiladi. Worker thread ishga tayyor bo'lganda, avval o'z lokal navbatini tekshiradi. Lokal navbatdan Tasklar oxirgi kirgan birinchi chiqadi (LIFO) algoritmi bilan ajratiladi. Worker threadi o'z lokal navbatiga kirish uchun yagona thread bo'lganligi sababli, thread sinxronizatsiya qulfi kerak emas va navbatdan Tasklarni qo'shish/olib tashlash juda tez.
Thread pool'lar navbatga qo'yilgan elementlarning qayta ishlanish tartibini hech qachon kafolatlamagan, ayniqsa bir nechta thread bir vaqtda elementlarni qayta ishlashi mumkinligi sababli. Biroq, bu yon ta'sir muammoni yanada kuchaytiradi. Siz ilovangizning navbatga qo'yilgan ish elementlari yoki tasklar qanday tartibda bajarilishiga hech qanday kutilmaga ega bo'lmasligiga ishonch hosil qilishingiz kerak.
Agar worker thread o'z lokal navbati bo'sh ekanini ko'rsa, u boshqa worker threadning lokal navbatidan Taskni "o'g'irlash"ga (steal) harakat qiladi. Tasklar lokal navbatning dumidan (tail) FIFO tartibida o'g'irlanadi, bu ozgina thread sinxronizatsiya qulfini talab qiladi. Barcha lokal navbatlar bo'sh bo'lsa, worker thread global navbatdan (qulfi bilan) elementni FIFO tartibida ajratadi. Agar global navbat ham bo'sh bo'lsa, worker thread biror narsa paydo bo'lishini kutib uxlaydi. Agar uzoq vaqt uxlasa, u uyg'onadi va thread tomonidan foydalanilgan resurslarni (kernel obyekti, steklar, TEB) tizimga qaytarish uchun o'zini yo'q qiladi.
Thread pool tezda worker threadlarni yaratadi, shunda worker threadlar soni ThreadPool.SetMinThreads metodiga uzatilgan qiymatga teng bo'ladi. Bu metodni hech qachon chaqirmasligingiz tavsiya etiladi, bu holda standart qiymat jarayoningizning foydalanishi mumkin bo'lgan CPU'lar soniga teng. Odatda jarayoningiz mashindagi barcha CPU'lardan foydalanishga ruxsat etiladi, shuning uchun thread pool tezda mashindagi CPU'lar sonigacha worker threadlarini yaratadi. Shundan keyin thread pool ish elementlarini tugatish tezligini kuzatadi va agar elementlar tugashga uzoq vaqt olayotgan bo'lsa, ko'proq worker threadlarni yaratadi. Agar elementlar tez tugay boshlasa, worker threadlar yo'q qilinadi.