فرشید جهان منش
فرشید جهان منش
خواندن ۱ دقیقه·۴ سال پیش

Progress Reporting در c#


سلام.گاهی ما نیاز داریم که وقتی یک عملیات async انجام میدیم ، در حین اون عملیات ، یک سری گزارش ها از اون عملیات دریافت کنیم. یک راه ساده برای این منظور اینه که یک <Action<T پاس بدیم به اون تابع Async خودمون که اون ، هر موقع که نیاز بود (یعنی هر موقع قرار بود گزارشی ثبت بشه) ، از این تابع استفاده کنه.

خب یک پروژه کنسولی ایجاد میکنم.یک کلاس داخلش ایجاد میکنم با نام report و دو تابع داخلش قرار میدم. یک تابع با نام DoTask و یک تابع با نام ReportSomeWork.

قراره توی تابع reportSomeWork بیایم و درصد اعداد بین صفر تا هزار رو نشون بدیم. کلا به شکل زیر میشه


خب داخل تابع DoTask اول میایم و آیدی تردی که داره این تابع رو انجام میده چاپ میکنیم. بعدش یک اکشن ایجاد میکنیم با نام progress که ورودیش یک عدد int هست (متغیر i) و داخلش ، در مرحله اول میایم و id ترد ای که داره اون رو اجرا میکنه رو چاپ میکنیم (بعدا میگم چرا) و بعدشم متغیر i رو پرینت میکنیم. حالا در ادامه تابع ReportSomeWork رو صدا میزنیم و اون اکشنی که ساختیم رو بهش پاس میدیم و await میکنیم .

داخل تابع ReportSomeWork ، یک task ایجاد میکنیم و اون قراره برای ما یک عملیاتی رو انجام بده. از عدد صفر تا 1000 جلو میره و هر جا که بر 10 بخش پذیر بود ، عدد تقسیم بر ده رو به اکشنمون پاس میده . و بعدشم دوباره ایدی تردی که داره این task رو اجرا میکنه رو چاپ میکنیم (یکم جلوتر علتشو میگم).


خب حالا یک شی از این کلاس داخل متد main می سازیم و تابع DoTask رو فراخوانی می کنیم و روش wait میکنیم.



حالا اگر پروژه رو اجرا کنیم ، میتونیم خروجی رو ببینیم.

خب ایدی تردی که داره شروع به اجرای متد DoTask میکنه ، 1 هست.

وقتی اکشن ما اجرا میشه ، تردی که اون رو اجرا میکنه آیدیش 4 هست. دقیقا همین ترد هم هست که داره اون عملیات حلقه for رو انجام میده . یعنی میخوام بگم اون worker thread ای که داخل تابع ReportSomeWork و توسط Task.Run اجرا میشه (task.run ترد ایجاد نمیکنه بلکه از ترد پول یک ترد میگیره)،همون تردی هست که اکشن رو انجام میده. خب این موضوع توی Console App مشکلی نداره (بدیلس عدم وجود ui thread)ولی وقتی میریم مثلا سراغ Windows Form مشکلش مشخص میشه.

خب یک پروژه ویندوز فرمی ایجاد میکنیم. توی فرم دیفالت یک لیبل قرار میدیم.

دو تابعی که قبلا نوشته بودیم رو داخل این فرم کپی میکنیم (البته با یک تغییر کوچیک)

تنها تغییری که دادیم اینه که داخل task.run یک try catch گذاشتیم تا اگر اروری رخ داد ، اون رو catch کنیم.

خب توی form1_load هم تابع خودمون رو صدا میزنیم.

خب حالا اگر پروژه رو اجرا کنیم و توی Catch ، یک بریک پوینت بزاریم ، اکسپشن رو میبینیم

متن خطا رو پایین میزارم.

Cross-thread operation not valid: Control 'label1' accessed from a thread other than the thread it was created on

حالا ماجرا چیه؟ بطور کلی element ها و control ها توی پروژه هایی مثل win form ، WPF ، UWP و ... فقط توسط تردی که اونها رو ایجاد کرده قابل دسترس هستند (UI Thread) نه بقیه ترد ها. این مشکلیه که توی console بهش برنخوردیم ولی اینجا می بینیم که ایراد میگیره بهمون.

حالا راهکارش چیه؟

یک راهکار ابتداییش اینه که از متد Invoke استفاده کنیم. خب یکم کد رو تغییر میدم. به این تکنیک marshal میگن.(البته راه درست تر اینه که از synchronization context استفاده کنیم ولی خب الان محل بحث ما خیلی نیست و مقالات زیادی دربارش وجود داره توی سطح نت (فارسی رو نمیدونم!))


خب دو قسمتی که تغییراتی داخلشون اعمال کردم رو نشون دادم. متد invoke یک دلیگیت میگیره و اون رو میره سر Ui Thread اجرا میکنه.
برای اینکه کارمون مشخص تر باشه ، یک thread.sleep هم زدم.
خب حالا اگر پروژه رو ران کنیم میبینیم که به درستی داره اعداد رو نمایش میده.



خب الان یک بحثی اینجا هست. کدی که ما تا اینجا نوشتیم رو میشه راحت توی پروژه هایی از نوع های دگ هم استفاده کرد؟! جوابش رو دیدیم. کد برای اجرا توی نسخه winform نسبت به console ، نیاز به تغییرات داشت. حالا این بسط پیدا میکنه بین بقیه پروژه ها هم (حالا ممکنه فقط تغییر در حد دو سه خط کد باشه (البته مگه کل این کد چند خطه!)).


خب قطعا راه حل بهتریم هست. CLR دو تا type برای حل این مشکل ارائه میکنه. یک اینترفیس بنام IProgress و یک کلاس هم که این اینترفیس رو پیاده سازی کرده با نام Progress که هر دو هم جنریک هستن. سورس کد هر دو رو میزارم اگر کسی دوست داشت مطالعه کنه. (Progress و IProgress)
کد رو به سبک زیر تغییر میدیم.


حالا پروژه رو اجرا میکنیم و میبینیم که بازم به درستی اجرا میشه. IProgress یک تابع داره با نام Report که یک شی از ما میگیره و اون رو میده به دلیگیتی که براش تعریف کردیم و اون دلیگیت رو سر ui thread بر اساس synchronization context اجرا میکنه. این کلاس capture میکنه synchronization context رو اگر وجود داشته باشه. (عکس زیر از سورس کد کلاس اومده که لینکش یکم بالا تر هست)


تا یادم نرفته بگم کلاس progress یک event ای هم داره با نام ProgressChanged که هر وقت تغییری اتفاق افتاد ، این رویداد فراخوانی میشه.


امیدوارم خوشتون اومده باشه. سورسش رو میتونید از طریق این لینک ببینید.

موفق باشید

csharpc sharpdotnetdot netdotnet core
یک عدد برنامه نویس دات نت .علاقه مند به تکنولوژی های روز. جوون و پر از انگیزه :) همینا برای شناختم کافی نیست؟!
شاید از این پست‌ها خوشتان بیاید