مقدّمة إلى البرمجة غير المتزامنة في Xamarin


حسام برهان

سنتعرّف في هذا الدرس من ضمن سلسلة تعلّم برمجة تطبيقات أندرويد باستخدام Xamarin.Forms على تقنيّة متقدّمة بعض الشيء، وهي البرمجة غير المتزامنة Asynchronous Programming. وهي تقنيّة مهمّة تسمح للمبرمجين ببناء تطبيقات مرنة وذات استجابة عالية عند الاستخدام. سيتناول هذا الدرس الحاجة إلى البرمجة غير المتزامنة، ويقدّم تطبيق عملي بسيط يعمل على توضيح فكرة البرمجة غير المتزامنة من خلال تطبيق بسيط للغاية.

main.png

الحاجة إلى البرمجة غير المتزامنة

تسلك التطبيقات نفس السلوك في معالجة مُدخلات المستخدم التي قد تكون عبارة عن نقرة فأرة أو لمسة على الشاشة أو ضغط على مفتاح من لوحة المفاتيح. إذ يجب أن تُعالج هذه المدخلات بشكل متسلسل حسب ترتيب وقوعها. ولا يتم الانتقال من مُدخل إلى مُدخل آخر إلّا بعد الانتهاء من معالجة التعليمات البرمجيّة التي سبّبها المُدخل الحالي. فإذا فرضنا مثلًا وجود زرّين على الشاشة، فإذا نقرت (لمست) الزرّين بشكل متتال وبسرعة، فإنّ التعليمات الموجودة في معالج حدث النقر للزر الذي لُمسَ أولًا سيتم تنفيذها بالكامل، وبعد ذلك ينتقل البرنامج لتنفيذ مجموعة التعليمات البرمجيّة الموجودة في معالج حدث النقر للزر الثاني. بمعنى آخر، لن يتم تنفيذ التعليمات البرمجيّة للزرّين معًا، حتى وإن بدا للمستخدم أنّه قد نقرهما (لمسهما) معًا. وهذا أمر طبيعي تمامًا ويجري في جميع التطبيقات التي تعمل على أنظمة التشغيل المختلفة.

السبب في هذا الأمر، هو أنّه وبشكل افتراضي يعمل التطبيق ضمن ما يُسمّى بمسار تنفيذ Thread رئيسي. يضمن مسار التنفيذ هذا أن يتم التنفيذ بالصورة التي أوضحناها قبل قليل. وفي الحقيقة يُسمّى مسار التنفيذ الرئيسي أحيانًا بمسار التنفيذ الخاص بواجهة المستخدم UI Thread وذلك لأنّ جميع العناصر المرئيّة يتم التعامل معها حصرًا من خلاله.

تكمن المشكلة في أنّ بعض التطبيقات قد تحتاج إلى زمن طويل نسبيًا في تنفيذ التعليمات البرمجيّة الموجودة -ولنقل- في أحد معالجات الأحداث. فقد ينقر (يلمس) المستخدم زرًا يؤدّي مثلًا إلى جلب بعض البيانات من الإنترنت، وهي عمليّة قد تتطلّب في بعض الأحيان زمنًا طويلًا نسبيًّا وذلك لأسباب متنوّعة لسنا بمعرض الحديث عنها حاليًّا. سيعني هذا بكلّ تأكيد أنّ واجهة التطبيق ستبقى جامدة ولا تستجيب حتى تنجح عملية جلب البيانات أو تفشل لسبب ما. السيناريو السابق غير مرغوب بكل تأكيد، فلا بدّ من وجود طريقة ما تسمح بجلب البيانات دون التأثير على واجهة التطبيق الأساسيّة، أو بمعنى أدق، دون التأثير على مسار التنفيذ الرئيسي UI Thread.

تُوفّر العديد من لغات البرمجة تقنيّة مهمّة لحل هذه المشكلة تتمثّل في استخدام مسارات تنفيذ أخرى (نسميها مسارات التنفيذ العاملة Worker Threads) يتم من خلالها تنفيذ المهام المتنوّعة التي يتطلّبها التطبيق والتي قد تستغرق زمنًا طويلًا نسبيًّا وذلك دون التأثير على مسار التنفيذ الرئيسي UI Thread. تُسمّى هذه التقنيّة بالبرمجة ذات مسارات التنفيذ المتعدّدة Multi-Threading Programming وهي من التقنيّات البرمجيّة المتقدّمة، وليس من السهل العمل معها.

توفّر معظم لغات البرمجة بما فيها سي شارب حلول مبسّطة تُعفي المستخدم من مغبّة التورّط -إن صحّ التعبير- في موضوع متقدّم كهذا. الحل الذي تقدّمه سي شارب هو في البرمجة غير المتزامنة Asynchronous Programming حيث تعمل هذه التقنيّة على تنفيذ المهام البرمجيّة التي تتطلّب وقتًا طويلًا نسبيًّا ضمن ما يشبه مسارات تنفيذ منفصلة عن مسار التنفيذ الرئيسي، بحيث يمكن القول أنّ التعليمات البرمجيّة الموجودة ضمن معالجات أحداث مختلفة يصبح من الممكن تنفيذها بشكل متوازٍ بدلًا من الشكل المتسلسل الذي تحدثنا عنه. انظر إلى الشكل التالي الذي يوضّح الفرق العام بين التطبيقات التي تستخدم مسار التنفيذ الرئيسي فقط، وبين التطبيقات التي تستخدم مسارات تنفيذ متعدّدة.

fig01.png

Quote

في الحقيقة هناك فرق تقني بين برمجة مسارات التنفيذ المتعدّدة وبين البرمجة غير المتزامنة. إذ أنّ عمليّة إنشاء مسارات التنفيذ المنفصلة تتم بشكل مختلف بينهما.

تطبيق العدّاد المحسّن مع استدعاء غير متزامن

تذكر معي تطبيق العدّاد المحسّن الذي تناولناه في هذا الدرس. سنجري تعديلًا عليه بحيث سنعمل على إضافة زر جديد لإجراء استدعاء غير متزامن يعمل على محاكاة عمليّة برمجيّة تستغرق 5 ثانية، بالإضافة إلى لصيقة تقع أسفل الزر السابق لعرض رسالة بعد الانتهاء من عمليّة الاستدعاء غير المتزامن. أثناء تنفيذ هذه العمليّة البرمجيّة سنضغط على زرّي الزيادة والإنقاص، وسيتابع التطبيق العمل بشكل طبيعي دون حدوث أيّ جمود على واجهة المستخدم. بعد انقضاء العمليّة الناتجة عن الاستدعاء غير المتزامن بعد 5 ثانية، سيعرض التطبيق رسالة "Hello From Async." ضمن لصيقة تقع أسفل الزر للإشارة إلى انتهاء هذه العمليّة. انظر الشكل التالي لمعاينة الشكل الجديد لذلك التطبيق:

fig02.png

أنشئ مشروعًا جديدًا من النوع Blank App (Xamarin.Forms Portable) وسمّه AsyncCounterApp، ثم أبق فقط على المشروعين AsyncCounterApp (Portable) و AsyncCounterApp.Droid كما وسبق أن فعلنا في هذا الدرس. بعد ذلك أضف صفحة محتوى وسمّها AsyncEnhancedCounterPage. احرص على أن تكون محتويات هذه الصفحة على الشكل التالي:

1	using System;
2	using System.Threading.Tasks;
3	using Xamarin.Forms;
4	
5	namespace AsyncCounterApp
6	{
7	    public class AsyncEnhancedCounterPage : ContentPage
8	    {
9	        Label lblDisplayAsync;
10	
11	        public AsyncEnhancedCounterPage()
12	        {
13	            int counter = 0;
14	
15	            Label lblDisplay = new Label
16	            {
17	                Text = "0",
18	                TextColor = Color.Accent,
19	                HorizontalOptions = LayoutOptions.CenterAndExpand,
20	                FontSize = Device.GetNamedSize(NamedSize.Large, typeof(Label))
21	            };
22	
23	            Button btnIncrement = new Button
24	            {
25	                Text = "+",
26	                HorizontalOptions = LayoutOptions.CenterAndExpand,
27	                FontSize = Device.GetNamedSize(NamedSize.Large, typeof(Label))
28	            };
29	            btnIncrement.Clicked += (s, e) =>
30	            {
31	                counter++;
32	                lblDisplay.Text = counter.ToString();
33	            };
34	
35	            Button btnDecrement = new Button
36	            {
37	                Text = "-",
38	                HorizontalOptions = LayoutOptions.CenterAndExpand,
39	                FontSize = Device.GetNamedSize(NamedSize.Large, typeof(Label))
40	            };
41	            btnDecrement.Clicked += (s, e) =>
42	            {
43	                if (counter == 0)
44	                {
45	                    DisplayAlert("تحذير", "لا يمكن لقيمة العدّاد أن تكون أصغر من الصفر", "موافق");
46	                }
47	                else
48	                {
49	                    counter--;
50	                    lblDisplay.Text = counter.ToString();
51	                }
52	            };
53	
54	            Button btnAsyncCall = new Button
55	            {
56	                Text = "Async Call",
57	                HorizontalOptions = LayoutOptions.CenterAndExpand,
58	                FontSize = Device.GetNamedSize(NamedSize.Large, typeof(Label))
59	            };
60	            btnAsyncCall.Clicked += BtnAsyncCall_Clicked; ;
61	
62	            lblDisplayAsync = new Label
63	            {
64	                TextColor = Color.Green,
65	                HorizontalOptions = LayoutOptions.CenterAndExpand,
66	                FontSize = Device.GetNamedSize(NamedSize.Large, typeof(Label))
67	            };
68	
69	            Content = new StackLayout
70	            {
71	                Children = {
72	                    new StackLayout
73	                    {
74	                        Orientation = StackOrientation.Horizontal,
75	                        Padding = new Thickness(0,64,0,64),
76	                        Children =
77	                        {
78	                            btnIncrement,
79	                            btnDecrement
80	                        }
81	                    },
82	                    lblDisplay,
83	                    btnAsyncCall,
84	                    lblDisplayAsync
85	                }
86	            };
87	        }
88	
89	        private async void BtnAsyncCall_Clicked(object sender, EventArgs e)
90	        {
91	            //simulate long process execution for 5 seconds
92	            string msg = await Task<string>.Run(() =>
93	            {
94	                DateTime d = DateTime.Now;
95	
96	                while (DateTime.Now.Second - d.Second < 5) ;
97	
98	                return "Hello from Async.";
99	            });
100	
101	            lblDisplayAsync.Text = msg;
102	        }
103	    }
104	}

لقد تناولنا الشيفرة السابقة من قبل. لذلك سنتحدّث عن عمليّة الاستدعاء غير المتزامن فحسب. لقد أسندنا لحدث نقر الزر btnAsyncCall معالج حدث BtnAsyncCall_Clicked وهو موجود في الأسطر من 89 حتى 102. الجديد في معالج الحدث هذا هو وجود الكلمة المحجوزة async (السطر 89) قبل نوع القيمة المعادة مباشرةً. تُشير هذه الكلمة المحجوزة إلى أنّ عملية الاستدعاء غير المتزامن ستجري ضمن التابع  BtnAsyncCall_Clicked.

انظر الآن إلى الأسطر من 92 إلى 99:

string msg = await Task<string>.Run(() =>

{

    DateTime d = DateTime.Now;



    while (DateTime.Now.Second - d.Second < 5) ;



    return "Hello from Async.";

});

لاحظ التابع الساكن Run من الصنف العمومي Task<string>. يعمل هذا التابع على استدعاء أيّ تابع مُمرّر إليه ضمن مسار تنفيذ منفصل عن مسار التنفيذ الأساسي، وبالتالي لا يُشغل مسار التنفيذ الأساسي المرتبط بواجهة المستخدم.

يجب أن يُسبق الاستدعاء غير المتزامن بالكلمة المحجوزة await. لقد مرّرنا إلى التابع Run تعبير Lambda يحتوي التعليمات البرمجيّة التي نريد تنفيذها على مسار تنفيذ مستقل (للمزيد حول تعابير Lambda انظر هنا). من الواضح أنّنا قد كتبنا شيفرة برمجيّة لا تقوم بعمل مفيد بكل تأكيد، إنما الغاية الوحيدة منها هي توضيح فكرة الاستدعاء غير المتزامن، حيث أجريت تأخيرًا زمنيًّا باستخدام حلقة while يستغرق 5 ثواني.

سنكتفي بهذه المقدّمة حول البرمجة غير المتزامنة، مع التأكيد على أنّنا سنتوسّع في هذا الموضوع حينما نتناول تطبيقًا عمليًّا مفيدًا لاحقًا في هذه السلسلة.

الخلاصة

تحدثنا في هذا الدرس عن مفهوم البرمجة غير المتزامنة Asynchronous Programming والحاجة إليها في التطبيقات التي نصمّمها لضمان أنّ العمليات البرمجيّة التي قد تستغرق وقتًا طويلًا نسبيًّا لن تؤدّي إلى جمود في واجهة المستخدم. ثمّ أجرينا تعديلًا بسيطًا على تطبيق بنيناه في درس سابق بحيث نجري استدعاءً غير متزامنًا يستغرق وقتًا لا بأس به حتى ينتهي، ورغم ذلك استطاع المستخدم متابعة العمل على التطبيق دون أيّ تأثير على واجهة التطبيق. لن نكتفي بكلّ تأكيد بما وصلنا إليه هنا، سنتوسّع بالحديث عن هذا الموضوع المهم لاحقًا حينما نبدأ بتنفيذ تطبيقات عمليّة باستخدام Xamarin.





تفاعل الأعضاء


لا توجد أيّة تعليقات بعد



يجب أن تكون عضوًا لدينا لتتمكّن من التعليق

انشاء حساب جديد

يستغرق التسجيل بضع ثوان فقط


سجّل حسابًا جديدًا

تسجيل الدخول

تملك حسابا مسجّلا بالفعل؟


سجّل دخولك الآن