۱۳۸۷/۱۲/۱۴

مروري بر كاربرد DoEvents


چند روز قبل هنگام استفاده از DoEvents در يك برنامه windows forms ، ناگهان پيغام stack overflow ظاهر شد! براي علت يابي و رفع آن كمي جستجو كردم كه خلاصه‌ي آن به شرح زير است:


DoEvents چيست؟

DoEvents يكي از متدهاي كلاس Application در فضاي نام System.Windows.Forms است.
ويندوز جهت مديريت رخدادهاي مختلف از يك صف استفاده مي‌كند. رخدادهايي مانند كليك ماوس، تغيير اندازه‌ي يك فرم و مواردي شبيه به آن ابتدا در يك صف قرار مي‌گيرند و سپس پردازش مي‌شوند. زمانيكه كنترلي مشغول پاسخ دهي به يك رخ‌داد مي‌گردد، ساير رخ‌دادها هنوز در صف هستند و پردازش نخواهند شد. بنابراين اگر برنامه‌ي شما در يك روال رخ‌دادگردان كليك، عملياتي طولاني را در حال انجام باشد، بدليل عدم پردازش ساير رخ‌دادها اينطور به نظر خواهد رسيد كه هنگ كرده است.
روش صحيح پردازش يك عمليات طولاني استفاده از يك ترد ديگر مي‌باشد تا ترد اصلي برنامه كه كار مديريت رابط كاربر برنامه را به عهده دارد، درگير اين عمليات طولاني نشده و پاسخگوي رخ‌دادهاي رسيده باشد.
راه ميان‌بر و ساده‌اي كه اينجا وجود دارد استفاده از DoEvents مي‌باشد (بدون ايجاد يك ترد جديد). براي مثال اگر در روال رخ دادگردان كليك يك برنامه، حلقه‌اي طولاني در حال پردازش است، هر از چندگاهي اين متد فراخواني شود، رخ‌دادهاي در صف قرار گرفته فرصت ارسال به ترد اصلي برنامه را يافته و برنامه در حالت هنگ به نظر نخواهد رسيد.
براي نمونه مثال زير را در دو حالت با Application.DoEvents و بدون آن اجرا كنيد:

private void btnProcessWithDoEvents_Click(object sender, EventArgs e)
{
for (int i = 0; i < 100000; i++)
{
TextBox1.Text = "Processing " + i.ToString();
Application.DoEvents();
}
}

در حالت بدون استفاده از Application.DoEvents ، تنها آخرين عبارت پردازش شده را در TextBox1 مشاهده خواهيد كرد و همچنين در اين حين، برنامه در حالت هنگ به نظر مي‌رسد و برعكس.

مشكلات احتمالي حاصل از استفاده از Application.DoEvents :

الف) حس غلط پايان يافتن عمليات پيش از موعد
در مثال فوق در حين استفاده از Application.DoEvents ، دكمه‌ي btnProcessWithDoEvents مجددا فعال شده و قابل كليك كردن مي‌شود ولي آيا اين بدين معنا است كه پردازش قبلي به پايان رسيده است؟ به يك سري از كاربرها هم click-happy user گفته مي‌شود! يعني از كليك كردن مجدد لذت مي‌برند! در اين حالت حتما بايد دكمه‌ي btnProcessWithDoEvents را در ابتداي پردازش غيرفعال كرد و سپس در انتهاي آن بايد مجددا فعال شود.
مورد مشكل كليك مجدد حتي مي‌تواند منجر به تخريب اطلاعات در حال پردازش شود. فرض كنيد برنامه در حال ذخيره‌ي اطلاعات در يك فايل است و كاربر مرتبا بر روي دكمه‌ي پردازش مربوطه كليك كنيد. فايل نهايي از يك سري اطلاعات ناهماهنگ و بي‌ربط پر خواهد شد.

ب) مشكل stack overflow
اگر علاقمند باشيد، اين مورد را مي‌توان به صورت زير شبيه سازي كرد:

يك تايمر را به برنامه اضافه كنيد و يك دكمه. در روال رخ‌دادگردان كليك مربوط به دكمه، دستورات زير را اضافه كنيد:

private void btnStartTimer_Click(object sender, EventArgs e)
{
this.timer1.Enabled = true;
this.timer1.Start();
this.timer1.Interval = 20;

}
و در روال tick مربوط به تايمر، دستورات زير را اضافه كنيد:

private void timer1_Tick(object sender, EventArgs e)
{
Thread.Sleep(50);
Application.DoEvents();
}
برنامه را اجرا كرده و يكي دو دقيقه صبر كنيد، حتما با پيغام خطاي stack overflow مواجه خواهيد شد. چرا؟
فواصل زماني اجراي تايمر به 20 ميلي ثانيه تنظيم شده است اما در روال رخ‌داد گردان tick آن، نياز به 50 ميلي ثانيه (بيش از 20 ميلي ثانيه) يا بيشتر براي اجرا دارد. با رسيدن به Application.DoEvents ، رخ‌داد در صف قرار گرفته‌ي ديگر tick بلافاصله اجرا مي‌شود و همينطور الي آخر، تا بالاخره stack overflow حاصل خواهد شد.


پس چه بايد كرد؟

الف) هنگام استفاده از Application.DoEvents به موارد فوق حتما دقت داشته باشيد.
ب) بجاي استفاده از اين روش كه در بيشتر موارد يك ضعف برنامه نويسي محسوب مي‌شود، شروع به استفاده از روش‌هاي غيرهمزمان نمائيد. براي مثال استفاده از :
BackgroundWorker
Asynchronous delegates
Threads

تنها موردي را كه هنگام كار با تردها بايد در نظر داشت اين است كه امكان دسترسي به كنترل‌هاي يك فرم را از ترد ديگري كه آن كنترل را ايجاد نكرده است، نداريد و براي اين مورد راه‌ حل‌هاي زيادي موجود است.
همچنين بخاطر داشته باشيد در يك ترد استفاده از Application.DoEvents هيچ معنايي ندارد. ترد اصلي برنامه وظيفه‌ي به روز رساني رابط كاربر برنامه و پاسخگويي به رخ‌دادهاي رسيده را به عهده دارد. زمانيكه پردازش در تردي ديگر صورت مي‌گيرد، ترد اصلي برنامه تا پايان پردازش متد شما قفل نخواهد شد كه نيازي به استفاده از اين متد باشد. در اين حالت استفاده از Application.DoEvents ، سبب بالا رفتن مصرف حافظه‌ي برنامه و همچنين بالا رفتن ميزان مصرف CPU خواهد شد.

جهت مطالعه بيشتر
Keeping your UI Responsive and the Dangers of Application.DoEvents