کتابخانه موازی وظیفه (Task Parallel Library یا TPL) بر پایه مفهومی به نام "وظیفه" (Task) طراحی شده است که نشاندهنده یک عملیات غیرهمزمان است. از برخی جنبهها، یک وظیفه شبیه به یک نخ (Thread) یا یک آیتم کاری در ThreadPool است، اما در سطحی بالاتر از انتزاع عمل میکند. اصطلاح "موازیسازی وظیفه" (Task Parallelism) به اجرای همزمان یک یا چند وظیفه مستقل اشاره دارد. وظایف (Tasks) دو مزیت اصلی ارائه میدهند:
در پشت صحنه، وظایف به صف ThreadPool ارسال میشوند که این صف به وسیله الگوریتمهایی بهینهسازی شده است که تعداد نخها را تعیین و تنظیم میکنند. این الگوریتمها تعادل بار (Load Balancing) را مدیریت میکنند تا بیشترین کارایی حاصل شود. این فرآیند وظایف را سبک و کمهزینه میکند و این امکان را میدهد که تعداد زیادی از آنها ایجاد کنید تا موازیسازی جزئیتر و دقیقتری داشته باشید.
وظایف و چارچوبی که حول آنها ساخته شده است، مجموعه گستردهای از APIها را ارائه میدهد که شامل قابلیتهایی مانند انتظار (Waiting)، لغو (Cancellation)، ادامه (Continuations)، مدیریت استثنائات قوی، گزارش وضعیت دقیق، زمانبندی سفارشی و بسیاری ویژگیهای دیگر است.
به همین دلایل، TPL بهترین گزینه برای نوشتن کدهای چندنخی، غیرهمزمان، و موازی در داتنت (.NET) است.
روش Parallel.Invoke
یک راه ساده برای اجرای همزمان چندین دستور مختلف ارائه میدهد. شما کافی است یک Action
برای هر عملیات کاری ارائه دهید. سادهترین روش برای تعریف این Actionها استفاده از عبارتهای لامبدا (Lambda Expressions) است. این عبارتهای لامبدا میتوانند یک متد نامگذاریشده را فراخوانی کنند یا کد موردنظر را به صورت مستقیم ارائه دهند.
در مثال زیر، یک فراخوانی ابتدایی به Parallel.Invoke
نشان داده شده است که دو وظیفه ایجاد و اجرا میکند که به صورت همزمان اجرا میشوند. وظیفه اول یک عبارت لامبدا است که متدی به نام DoSomeWork
را فراخوانی میکند و وظیفه دوم نیز متدی به نام DoSomeOtherWork
را فراخوانی میکند:
Parallel.Invoke(() => DoSomeWork(), () => DoSomeOtherWork());
نکته : این مستندات از عبارات لامبدا (Lambda Expressions) برای تعریف نمایندهها (Delegates) در TPL استفاده میکند. اگر با عبارات لامبدا در C# یا Visual Basic آشنایی ندارید، پیشنهاد میکنم به مستندات Lambda Expressions in PLINQ and TPL مراجعه کنید.
نکته : تعداد نمونههای وظیفه (Task Instances) که در پسزمینه توسط متد Invoke
ایجاد میشوند، لزوماً برابر با تعداد نمایندههایی نیست که ارائه دادهاید. TPL ممکن است بهینهسازیهای مختلفی انجام دهد، به خصوص زمانی که تعداد نمایندهها زیاد باشد.
یک وظیفه که مقدار بازگشتی ندارد، با کلاس System.Threading.Tasks.Task
نمایش داده میشود. وظیفهای که یک مقدار بازگشتی دارد، با کلاس System.Threading.Tasks.Task<TResult>
نمایش داده میشود که از کلاس Task
به ارث میبرد. شیء وظیفه (Task Object) مدیریت جزئیات زیرساخت را انجام میدهد و متدها و خواصی را ارائه میدهد که در طول عمر وظیفه از نخ فراخوانی قابل دسترسی هستند. به عنوان مثال، شما میتوانید در هر زمانی به ویژگی Status
یک وظیفه دسترسی داشته باشید تا مشخص کنید که آیا وظیفه شروع به اجرا کرده، به پایان رسیده، لغو شده یا استثنایی (Exception) رخ داده است. وضعیت وظیفه با یک شمارش به نام TaskStatus
نمایش داده میشود.
هنگامی که یک وظیفه ایجاد میکنید، باید یک نماینده (Delegate) به آن بدهید که کدی را که وظیفه اجرا خواهد کرد، در بر میگیرد. این نماینده میتواند به صورت یک نماینده نامگذاریشده، یک متد ناشناس یا یک عبارت لامبدا (Lambda Expression) تعریف شود. عبارات لامبدا میتوانند شامل فراخوانی به یک متد نامگذاریشده باشند، همانطور که در مثال زیر نشان داده شده است. این مثال شامل فراخوانی متد Task.Wait
است تا اطمینان حاصل شود که وظیفه قبل از پایان برنامه در حالت کنسول اجرا را کامل میکند.
Task taskA = new Task( () => Console.WriteLine("Hello from taskA."));
taskA.Start();
taskA.Wait();
شما میتوانید از متدهای Task.Run
برای ایجاد و شروع یک وظیفه به صورت همزمان استفاده کنید. متدهای Run
همیشه از زمانبند پیشفرض وظایف استفاده میکنند، حتی اگر نخ فعلی از زمانبند دیگری استفاده کند.
متدهای Run
گزینه مناسبی هستند زمانی که نیازی به کنترل بیشتر روی ایجاد و زمانبندی وظایف ندارید.
Task taskA = Task.Run( () => Console.WriteLine("Hello from taskA."));
taskA.Wait();
شما همچنین میتوانید از متد TaskFactory.StartNew
برای ایجاد و شروع یک وظیفه به صورت همزمان استفاده کنید. همانطور که در مثال زیر نشان داده شده است، میتوانید از این متد زمانی استفاده کنید که:
Task.AsyncState
آن را بازیابی کنید.Task<Double>.Factory.StartNew(() => DoComputation(1000.0));
برای اطلاعات بیشتر، به مستندات زیر مراجعه کنید.
زمانی که از یک عبارت لامبدا برای ایجاد یک نماینده استفاده میکنید، به تمامی متغیرهایی که در آن نقطه از کد شما قابل مشاهده هستند دسترسی دارید. با این حال، در برخی موارد، بهویژه در داخل حلقهها، لامبدا متغیر را بهطور مورد انتظار ذخیره نمیکند. لامبدا فقط مرجع (Reference) متغیر را ذخیره میکند، نه مقدار آن، چون این متغیر بعد از هر بار تکرار تغییر میکند.
شما میتوانید با فراهم کردن یک شیء وضعیت (state object) به یک وظیفه از طریق سازنده آن، به مقدار در هر تکرار دسترسی پیدا کنید.
این وضعیت بهعنوان یک آرگومان به نماینده وظیفه (task delegate) ارسال میشود و میتوان از طریق ویژگی Task.AsyncState
آن را از شیء وظیفه دسترسی کرد.
Task ID
هر وظیفه یک شناسه عددی دریافت میکند که آن را بهطور منحصر به فرد در دامنه برنامه شناسایی میکند و میتوان از طریق ویژگی Task.Id
به آن دسترسی پیدا کرد. این شناسه برای مشاهده اطلاعات وظایف در پنجرههای Parallel Stacks و Tasks در دیباگر ویژوال استودیو مفید است. شناسه بهطور تنبل (lazy) ایجاد میشود، به این معنی که تا زمانی که درخواست نشود، ایجاد نمیشود. بنابراین، ممکن است هر بار که برنامه اجرا میشود، شناسه متفاوتی به وظیفه اختصاص یابد. برای اطلاعات بیشتر در مورد نحوه مشاهده شناسههای وظیفه در دیباگر، به مستندات زیر مراجعه کنید.
Task creation options
بیشتر APIهایی که وظایف ایجاد میکنند، اضافهبارهایی دارند که یک پارامتر از نوع TaskCreationOptions
را قبول میکنند. با مشخص کردن یک یا چند گزینه از این موارد، شما به زمانبند وظیفه میگویید که چگونه وظیفه را روی استخر نخ زمانبندی کند. این گزینهها ممکن است با استفاده از عملگر OR بیتی ترکیب شوند.
مثال زیر یک وظیفه را نشان میدهد که گزینههای LongRunning
و PreferFairness
را دارد.
var task3 = new Task(() => MyLongRunningMethod(), TaskCreationOptions.LongRunning | TaskCreationOptions.PreferFairness); task3.Start();
Tasks, threads, and culture
هر نخ یک فرهنگ و فرهنگ UI مرتبط دارد که به ترتیب توسط ویژگیهای Thread.CurrentCulture
و Thread.CurrentUICulture
تعریف میشود. فرهنگ یک نخ در عملیاتی مانند قالببندی، تجزیه و تحلیل، مرتبسازی و مقایسه رشتهها استفاده میشود. فرهنگ UI یک نخ در جستجوی منابع استفاده میشود.
فرهنگ سیستم فرهنگ پیشفرض و فرهنگ UI پیشفرض نخ را تعریف میکند. با این حال، میتوانید یک فرهنگ پیشفرض برای تمام نخها در یک دامنه برنامه از طریق ویژگیهای CultureInfo.DefaultThreadCurrentCulture
و CultureInfo.DefaultThreadCurrentUICulture
مشخص کنید. اگر فرهنگ یک نخ را بهطور صریح تنظیم کنید و یک نخ جدید راهاندازی کنید، نخ جدید فرهنگ نخ فراخوانی را به ارث نمیبرد؛ در عوض، فرهنگ آن فرهنگ پیشفرض سیستم است. با این حال، در برنامهنویسی مبتنی بر وظیفه (Task)، وظایف از فرهنگ نخ فراخوانی استفاده میکنند، حتی اگر وظیفه بهصورت غیرهمزمان روی نخ دیگری اجرا شود.
مثال زیر یک توضیح ساده ارائه میدهد. این مثال فرهنگ فعلی برنامه را به فرانسوی (فرانسه) تغییر میدهد. اگر فرانسوی (فرانسه) قبلاً فرهنگ فعلی باشد، به انگلیسی (ایالات متحده) تغییر میدهد. سپس یک نماینده به نام formatDelegate
فراخوانی میشود که برخی از اعداد را بهعنوان مقادیر ارزی در فرهنگ جدید فرمت میکند. چه نماینده بهطور همزمان یا غیرهمزمان توسط یک وظیفه فراخوانی شود، وظیفه از فرهنگ نخ فراخوانی استفاده میکند.
نکته : در نسخههای پیش از .NET Framework 4.6، فرهنگ یک وظیفه توسط فرهنگ نخی که وظیفه روی آن اجرا میشود تعیین میشود، نه فرهنگ نخ فراخوانی. برای وظایف غیرهمزمان، فرهنگ استفادهشده توسط وظیفه ممکن است با فرهنگ نخ فراخوانی متفاوت باشد.
Creating task continuations
متدهای Task.ContinueWith
و Task<TResult>.ContinueWith
به شما این امکان را میدهند که یک وظیفه را مشخص کنید که پس از اتمام وظیفه قبلی اجرا شود. نماینده وظیفه ادامهدهنده یک مرجع به وظیفه قبلی دریافت میکند تا بتواند وضعیت آن را بررسی کند. و با بازیابی مقدار ویژگی Task<TResult>.Result
، میتوانید از خروجی وظیفه قبلی بهعنوان ورودی برای وظیفه ادامهدهنده استفاده کنید.
چون Task.ContinueWith
یک متد نمونهای است، شما میتوانید فراخوانیهای متد را به هم متصل کنید، به جای اینکه برای هر وظیفه قبلی یک شیء Task<TResult>
ایجاد کنید.
Creating detached child tasks
زمانی که کدی که در یک وظیفه در حال اجرا است، یک وظیفه جدید ایجاد میکند و گزینه AttachedToParent
را مشخص نمیکند، وظیفه جدید بهطور خاص با وظیفه والد هماهنگ نمیشود. این نوع وظیفه غیرهماهنگ به نام "وظیفه تو در تو جدا شده" یا "وظیفه فرزند جدا شده" شناخته میشود.
Creating child tasks
زمانی که کدی که در یک وظیفه در حال اجرا است، یک وظیفه با گزینه AttachedToParent
ایجاد میکند، وظیفه جدید به عنوان "وظیفه فرزند متصل" به وظیفه والد شناخته میشود. شما میتوانید از گزینه AttachedToParent
برای بیان موازیسازی وظایف ساختاریافته استفاده کنید، زیرا وظیفه والد بهطور ضمنی منتظر اتمام تمام وظایف فرزند متصل میماند. در مثال زیر، یک وظیفه والد ایجاد میشود که ۱۰ وظیفه فرزند متصل ایجاد میکند. در این مثال، متد Task.Wait
برای انتظار برای اتمام وظیفه والد فراخوانی میشود و نیازی به انتظار صریح برای اتمام وظایف فرزند متصل نیست.
Waiting for tasks to finish
انتظار برای اتمام وظایف
انواع System.Threading.Tasks.Task
و System.Threading.Tasks.Task<TResult>
چندین باراضافه از متدهای Task.Wait
را فراهم میکنند که به شما امکان میدهند برای اتمام یک وظیفه صبر کنید. علاوه بر این، باراضافههای متدهای ایستا Task.WaitAll
و Task.WaitAny
به شما این امکان را میدهند که برای اتمام هرکدام یا همه وظایف در یک آرایه از وظایف صبر کنید.
بهطور معمول، شما برای یکی از این دلایل برای اتمام یک وظیفه صبر میکنید:
Main
(نقطه ورودی برنامه) خاتمه مییابند.مثال زیر الگوی پایهای را نشان میدهد که شامل مدیریت استثنا نیست:
برای مثالی که مدیریت استثناها را نشان میدهد، به "مدیریت استثناها" مراجعه کنید.
برخی از باراضافهها به شما این امکان را میدهند که یک زمانسنج تعیین کنید، و برخی دیگر یک پارامتر اضافی به نام CancellationToken
دریافت میکنند تا بتوانید خود عمل انتظار را هم به صورت برنامهنویسی و هم به پاسخ به ورودی کاربر لغو کنید.
وقتی برای یک وظیفه صبر میکنید، بهطور ضمنی برای تمام فرزندان آن وظیفه که با استفاده از گزینه TaskCreationOptions.AttachedToParent
ایجاد شدهاند، صبر میکنید. متد Task.Wait
بلافاصله اگر وظیفه قبلاً تکمیل شده باشد، بازمیگردد. متد Task.Wait
هر استثنایی که توسط یک وظیفه پرتاب شده باشد را پرتاب میکند، حتی اگر متد Task.Wait
پس از اتمام وظیفه فراخوانی شده باشد.
Composing tasks
کلاسهای Task و Task<TResult> چندین متد را برای کمک به ترکیب چندین تسک فراهم میکنند. این متدها الگوهای رایج را پیادهسازی کرده و از ویژگیهای زبانهای C#، Visual Basic و F# برای برنامهنویسی غیرهمزمان بهخوبی استفاده میکنند. در این بخش متدهای WhenAll، WhenAny، Delay و FromResult توضیح داده شدهاند.
Task.WhenAll
متد Task.WhenAll بهصورت غیرهمزمان منتظر میماند تا چندین شیء Task یا Task<TResult> تمام شوند. این متد نسخههای اضافهشدهای دارد که به شما این امکان را میدهد تا منتظر مجموعههای غیر یکنواختی از تسکها باشید. بهعنوان مثال، میتوانید منتظر بمانید تا چندین شیء Task و Task<TResult> از یک فراخوانی متد به پایان برسند.
Task.WhenAny
متد Task.WhenAny بهصورت غیرهمزمان منتظر میماند تا یکی از چندین شیء Task یا Task<TResult> تمام شود. مشابه متد Task.WhenAll، این متد نسخههای اضافهشدهای دارد که به شما این امکان را میدهد تا منتظر مجموعههای غیر یکنواختی از تسکها باشید. متد WhenAny بهویژه در سناریوهای زیر مفید است:
عملیاتهای اضافی: فرض کنید الگوریتم یا عملیاتی دارید که میتواند به طرق مختلف انجام شود. میتوانید از متد WhenAny برای انتخاب عملیاتی که زودتر تمام میشود استفاده کنید و سپس عملیاتهای باقیمانده را لغو کنید.
عملیاتهای درهم: میتوانید چندین عملیات را شروع کنید که باید تمام شوند و از متد WhenAny برای پردازش نتایج هر عملیات به محض اتمام آن استفاده کنید. پس از اتمام یک عملیات، میتوانید یک یا چند تسک را آغاز کنید.
عملیاتهای محدود شده: میتوانید از متد WhenAny برای گسترش سناریو قبلی با محدود کردن تعداد عملیاتهای همزمان استفاده کنید.
عملیاتهای منقضیشده: میتوانید از متد WhenAny برای انتخاب بین یک یا چند تسک و یک تسک که بعد از زمان خاصی تمام میشود استفاده کنید، مانند تسکی که از متد Delay برمیگردد. متد Delay در بخش بعدی توضیح داده شده است.
Task.Delay
متد Task.Delay شیء Task ایجاد میکند که پس از مدت زمان مشخصی به پایان میرسد. میتوانید از این متد برای ساخت حلقههایی که دادهها را بهطور دورهای بررسی میکنند، تعیین زمانگذاریها، به تأخیر انداختن پردازش ورودی کاربر و غیره استفاده کنید.
Task<TResult>.FromResult
با استفاده از متد Task.FromResult میتوانید شیء Task<TResult> ایجاد کنید که یک نتیجه پیشمحاسبهشده را نگه میدارد. این متد زمانی مفید است که عملیاتی غیرهمزمان انجام میدهید که شیء Task<TResult> باز میگرداند و نتیجه آن Task<TResult> قبلاً محاسبه شده باشد. برای مثال، از FromResult میتوان برای دریافت نتایج عملیات دانلود غیرهمزمان که در کش نگهداری میشوند، استفاده کرد.
Handling exceptions in tasks
هنگامی که یک تسک یک یا چند استثنا پرتاب میکند، استثناها در یک استثنای AggregateException بسته میشوند. این استثنا به رشتهای که با تسک ملحق شده است، منتقل میشود. معمولاً این رشتهای است که منتظر پایان تسک یا دسترسی به ویژگی Result است. این رفتار سیاست .NET Framework را اعمال میکند که طبق آن، تمام استثناهای بدون پردازش بهطور پیشفرض باید فرآیند را خاتمه دهند. کد فراخوانی میتواند استثناها را با استفاده از هر یک از موارد زیر در یک بلوک try/catch مدیریت کند:
متد Wait
متد WaitAll
متد WaitAny
ویژگی Result
رشته ملحقشده همچنین میتواند استثناها را با دسترسی به ویژگی Exception قبل از جمعآوری زبالهها مدیریت کند. با دسترسی به این ویژگی، از انتقال استثناهای بدون پردازش که باعث خاتمه فرآیند هنگام نهایی شدن شیء میشود، جلوگیری میکنید.
برای اطلاعات بیشتر در مورد استثناها و تسکها، به مدیریت استثناها مراجعه کنید.
Canceling tasks
کلاس Task از لغو همکاری پشتیبانی میکند و بهطور کامل با کلاسهای System.Threading.CancellationTokenSource و System.Threading.CancellationToken که در .NET Framework 4 معرفی شدهاند، یکپارچه است. بسیاری از سازندههای کلاس System.Threading.Tasks.Task یک شیء CancellationToken را بهعنوان پارامتر ورودی میپذیرند. بسیاری از اضافهشدههای متدهای StartNew و Run نیز شامل پارامتر CancellationToken هستند.
شما میتوانید توکن را ایجاد کرده و درخواست لغو را در زمان بعدی با استفاده از کلاس CancellationTokenSource صادر کنید. توکن را بهعنوان آرگومان به تسک ارسال کرده و همچنین از همان توکن در دلیگیت کاربر خود استفاده کنید که وظیفه پاسخ به درخواست لغو را انجام میدهد.
برای اطلاعات بیشتر، به لغو تسکها و نحوه لغو یک تسک و فرزندان آن مراجعه کنید.
The TaskFactory class
کلاس TaskFactory متدهای ایستا را فراهم میکند که الگوهای رایج برای ایجاد و شروع تسکها و تسکهای ادامهای را در بر میگیرد.
رایجترین الگو، StartNew است که در یک بیانیه، تسک را ایجاد و شروع میکند.
هنگامی که تسکهای ادامهای از چندین پیشنیاز ایجاد میکنید، از متدهای ContinueWhenAll یا ContinueWhenAny یا معادلهای آنها در کلاس Task<TResult> استفاده کنید. برای اطلاعات بیشتر، به زنجیرهای کردن تسکها با استفاده از تسکهای ادامهای مراجعه کنید.
برای گنجاندن متدهای BeginX و EndX مدل برنامهنویسی غیرهمزمان در یک شیء Task یا Task<TResult>، از متدهای FromAsync استفاده کنید. برای اطلاعات بیشتر، به TPL و برنامهنویسی غیرهمزمان سنتی .NET Framework مراجعه کنید.
و TaskFactory پیشفرض میتواند بهعنوان یک ویژگی ایستا در کلاس Task یا Task<TResult> دسترسی داشته باشد. همچنین میتوانید یک TaskFactory را مستقیماً نمونهسازی کرده و گزینههای مختلفی از جمله یک توکن لغو، گزینههای TaskCreationOptions، گزینههای TaskContinuationOptions یا یک TaskScheduler را مشخص کنید. هر گزینهای که هنگام ایجاد کارخانه تسک مشخص کنید، به تمام تسکهایی که ایجاد میکند اعمال میشود، مگر اینکه تسک با استفاده از enumeration TaskCreationOptions ایجاد شود، در این صورت گزینههای تسک گزینههای کارخانه تسک را لغو میکنند.
Tasks without delegates
در برخی موارد، ممکن است بخواهید از یک تسک برای دربر گرفتن عملیاتی غیرهمزمان که توسط یک مؤلفه خارجی انجام میشود به جای دلیگیت کاربر خود استفاده کنید. اگر عملیات بر اساس الگوی Begin/End مدل برنامهنویسی غیرهمزمان باشد، میتوانید از متدهای FromAsync استفاده کنید. اگر اینطور نباشد، میتوانید از شیء TaskCompletionSource<TResult> برای پیچیدن عملیات در یک تسک استفاده کنید و به این ترتیب از برخی مزایای برنامهنویسی تسکها بهرهمند شوید. بهعنوان مثال، پشتیبانی از انتقال استثنا و تسکهای ادامهای. برای اطلاعات بیشتر، به TaskCompletionSource<TResult> مراجعه کنید.
Custom schedulers
بیشتر توسعهدهندگان برنامه یا کتابخانه نیازی به دانستن اینکه تسک روی کدام پردازنده اجرا میشود، چگونه کار خود را با سایر تسکها هماهنگ میکند یا چگونه روی System.Threading.ThreadPool زمانبندی میشود، ندارند. آنها فقط نیاز دارند که تسک بهطور کارآمد بر روی کامپیوتر میزبان اجرا شود. اگر نیاز به کنترل دقیقتری بر جزئیات زمانبندی دارید، TPL به شما این امکان را میدهد که برخی تنظیمات را روی زمانبند پیشفرض تسک پیکربندی کنید و حتی به شما اجازه میدهد که یک زمانبند سفارشی ارائه دهید. برای اطلاعات بیشتر، به TaskScheduler مراجعه کنید.
Related data structures
ث TPL چندین نوع جدید عمومی را فراهم میکند که در سناریوهای موازی و ترتیبی مفید هستند. این شامل. چندین کلاس مجموعه thread-safe، سریع و مقیاسپذیر در فضای نام System.Collections.Concurrent و چندین نوع همگامسازی جدید است. برای مثال، System.Threading.Semaphore و System.Threading.ManualResetEventSlim که برای برخی از انواع بارها کارآمدتر از نسخههای قبلی خود هستند. انواع جدید دیگر در .NET Framework 4، مانند System.Threading.Barrier و System.Threading.SpinLock، عملکردهایی را فراهم میکنند که در نسخههای قبلی موجود نبودند. برای اطلاعات بیشتر، به ساختار دادهها برای برنامهنویسی موازی مراجعه کنید.
Custom task types
ما توصیه میکنیم که از کلاسهای System.Threading.Tasks.Task یا System.Threading.Tasks.Task<TResult> ارثبری نکنید. بهجای آن، توصیه میکنیم که از ویژگی AsyncState برای ارتباط دادن دادهها یا وضعیت اضافی با شیء Task یا Task<TResult> استفاده کنید. همچنین میتوانید از متدهای افزونه برای گسترش عملکرد کلاسهای Task و Task<TResult> استفاده کنید. برای اطلاعات بیشتر در مورد متدهای افزونه، به متدهای افزونه مراجعه کنید.
رفرنس :