تعلم برمجة Go ذات مؤشرات الترابط المتعددة بالصور


على الأرجح ، لقد سمعت بالفعل عن لغة برمجة Go ، وشعبيتها تتزايد باستمرار ، وهو أمر معقول للغاية. هذه اللغة بسيطة وسريعة وتعتمد على مجتمع عظيم. يعد نموذج البرمجة متعدد الخيوط من أكثر الجوانب الغريبة للغة. تسمح لك الأساسيات التي تقوم عليها بإنشاء برامج متعددة الخيوط بسهولة وبساطة. هذه المقالة مخصصة لأولئك الذين يرغبون في تعلم هذه البدائية: goroutines والقنوات. ومن خلال الرسوم التوضيحية ، سأوضح كيفية العمل معهم. آمل أن تكون هذه مساعدة جيدة لك في دراستك الإضافية.

برامج مفردة ومتعددة الخيوط


على الأرجح أنك كتبت بالفعل برامج أحادية الخيوط. عادة ما يبدو هذا: هناك مجموعة من الوظائف لأداء مهام مختلفة ، يتم استدعاء كل وظيفة فقط عندما تكون الوظيفة السابقة معدة لها. وبالتالي ، يعمل البرنامج بالتسلسل.

سيكون هذا مثالنا الأول - برنامج تعدين الركاز. وظائفنا سوف تسعى ، تعدين ومعالجة خام. يتم تمثيل الخام في المنجم في مثالنا بقوائم من السلاسل ، وتأخذها الدالات كمعلمات وتعيد قائمة السلاسل "المعالجة". بالنسبة لبرنامج أحادي الخيوط ، سيتم تصميم تطبيقنا على النحو التالي:



في هذا المثال ، يتم تنفيذ جميع الأعمال عن طريق خيط واحد (غوفر غوفر). هناك ثلاث وظائف رئيسية: البحث والإنتاج والمعالجة يتم تنفيذها بالتتابع الواحد تلو الآخر.

func main() { theMine := [5]string{"rock", "ore", "ore", "rock", "ore"} foundOre := finder(theMine) minedOre := miner(foundOre) smelter(minedOre) } 

إذا قمنا بطباعة نتيجة كل وظيفة ، نحصل على ما يلي:

 From Finder: [ore ore ore] From Miner: [minedOre minedOre minedOre] From Smelter: [smeltedOre smeltedOre smeltedOre] 

التصميم والتنفيذ البسيط هو إضافة لنهج واحد الخيوط. ولكن ماذا لو كنت تريد تشغيل وتنفيذ الوظائف بشكل مستقل عن بعضها البعض؟ هنا تأتي البرمجة متعددة الخيوط لمساعدتك.


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

جوروتينز


يمكن التفكير في Goroutines على أنها "خيوط خفيفة الوزن" ، لإنشاء goroutines تحتاج فقط إلى وضع الكلمة الرئيسية go قبل رمز استدعاء الوظيفة. لتوضيح مدى بساطتها ، دعنا ننشئ وظيفتين للبحث ، ونطلق عليهما الكلمة الأساسية go ونطبع رسالة في كل مرة يعثر فيها على "خام" في المنجم.


 func main() { theMine := [5]string{"rock", "ore", "ore", "rock", "ore"} go finder1(theMine) go finder2(theMine) <-time.After(time.Second * 5) //       } 

سيكون ناتج برنامجنا على النحو التالي:

 Finder 1 found ore! Finder 2 found ore! Finder 1 found ore! Finder 1 found ore! Finder 2 found ore! Finder 2 found ore! 

كما ترون ، لا يوجد ترتيب تقوم فيه الوظيفة أولاً "بإيجاد خام" ؛ تعمل وظائف البحث في وقت واحد. إذا قمت بتشغيل المثال عدة مرات ، فسيكون الترتيب مختلفًا. الآن يمكننا تشغيل برامج متعددة الخيوط (متعددة المجالات) ، وهذا تقدم خطير. ولكن ماذا نفعل عندما نحتاج إلى إقامة صلة بين الغوروتينات المستقلة؟ حان الوقت لسحر القنوات.

القنوات



تسمح القنوات goroutines لتبادل البيانات. هذا نوع من الأنابيب التي يمكن لل جوروتينات من خلالها إرسال واستقبال المعلومات من الغوروتينات الأخرى.

تتم القراءة والكتابة للقناة باستخدام عامل السهم (<-) ، مما يشير إلى اتجاه حركة البيانات.

 myFirstChannel := make(chan string) myFirstChannel <- "hello" //    myVariable := <- myFirstChannel //    

الآن لا يحتاج كشاف غوفر لدينا إلى تجميع الخام ، يمكنه نقله على الفور باستخدام القنوات.

لقد قمت بتحديث المثال ، الآن رمز مكتشف الخام وعمال المناجم عبارة عن وظائف مجهولة. لا تزعج نفسك كثيرًا إذا لم تصادفهم من قبل ، فقط ضع في اعتبارك أن كل واحد منهم يتم استدعاؤه باستخدام الكلمة الأساسية go ، لذلك ، سيتم تنفيذه في غوروتين خاص به. أهم شيء هنا هو أن goroutines تنقل البيانات فيما بينها باستخدام قناة oreChan . وسوف نتعامل مع الوظائف المجهولة أقرب إلى النهاية.

 func main() { theMine := [5]string{“ore1”, “ore2”, “ore3”} oreChan := make(chan string) //   go func(mine [5]string) { for _, item := range mine { oreChan <- item // } }(theMine) //   go func() { for i := 0; i < 3; i++ { foundOre := <-oreChan // fmt.Println(“Miner: Received “ + foundOre + “ from finder”) } }() <-time.After(time.Second * 5) //     } 

يوضح الاستنتاج أدناه بوضوح أن عامل المنجم لدينا يتلقى ثلاث مرات من القناة الخام جزءًا واحدًا في كل مرة.

 Miner: Received ore1 from finder Miner: Received ore2 from finder Miner: Received ore3 from finder 

لذا ، يمكننا الآن نقل البيانات بين goroutines المختلفة (gophers) ، ولكن قبل أن نبدأ في كتابة برنامج معقد ، دعونا نلقي نظرة على بعض الخصائص المهمة للقنوات.

أقفال


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

اكتب قفل




عندما يرسل goroutine (gopher) البيانات إلى قناة ، يتم حظره حتى يقرأ goroutine آخر البيانات من القناة.

قراءة القفل




على غرار القفل عند الكتابة إلى قناة ، يمكن قفل goroutin عند القراءة من قناة حتى يتم كتابة أي شيء إليها.
إذا كانت الأقفال تبدو للوهلة الأولى معقدة بالنسبة لك ، يمكنك تخيلها على أنها "تحويل أموال" بين اثنين من الغوروتينات. عندما يريد أحد الحاضرين تحويل الأموال أو استلامها ، عليه أن ينتظر المشارك الثاني في المعاملة.

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

قنوات غير منتفخة




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

القنوات المخزنة




لا يمكن دائمًا مزامنة التدفقات في البرنامج بشكل مثالي. لنفترض ، في مثالنا ، أن كشاف غوفر وجد ثلاثة أجزاء من الخام ، واستطاع عامل منجم غوفر استخراج جزء واحد فقط من الاحتياطيات التي تم العثور عليها في نفس الوقت. هنا ، لكي لا يقضي استطلاع غوفر معظم وقته ، في انتظار انتهاء عامل المنجم من عمله ، سنستخدم القنوات العازلة. لنبدأ بإنشاء قناة بسعة 3.

 bufferedChan := make(chan string, 3) 

يمكننا إرسال العديد من البيانات إلى القناة المخزنة ، دون الحاجة إلى قراءتها مع غوروتين آخر. هذا هو الفرق الرئيسي من القنوات غير المحجوبة.


 bufferedChan := make(chan string, 3) go func() { bufferedChan <- "first" fmt.Println("Sent 1st") bufferedChan <- "second" fmt.Println("Sent 2nd") bufferedChan <- "third" fmt.Println("Sent 3rd") }() <-time.After(time.Second * 1) go func() { firstRead := <- bufferedChan fmt.Println("Receiving..") fmt.Println(firstRead) secondRead := <- bufferedChan fmt.Println(secondRead) thirdRead := <- bufferedChan fmt.Println(thirdRead) }() 

سيكون ترتيب الإخراج في هذا البرنامج على النحو التالي:

 Sent 1st Sent 2nd Sent 3rd Receiving.. first second third 

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

ضع كل ذلك معًا


لذا ، مسلحًا بال goroutines والقنوات ، يمكننا كتابة برنامج باستخدام جميع مزايا البرمجة متعددة الخيوط في Go.


 theMine := [5]string{"rock", "ore", "ore", "rock", "ore"} oreChannel := make(chan string) minedOreChan := make(chan string) //  go func(mine [5]string) { for _, item := range mine { if item == "ore" { oreChannel <- item //   oreChannel } } }(theMine) //  go func() { for i := 0; i < 3; i++ { foundOre := <-oreChannel //   oreChannel fmt.Println("From Finder: ", foundOre) minedOreChan <- "minedOre" //   minedOreChan } }() //  go func() { for i := 0; i < 3; i++ { minedOre := <-minedOreChan //   minedOreChan fmt.Println("From Miner: ", minedOre) fmt.Println("From Smelter: Ore is smelted") } }() <-time.After(time.Second * 5) //     

سينتج مثل هذا البرنامج ما يلي:

 From Finder: ore From Finder: ore From Miner: minedOre From Smelter: Ore is smelted From Miner: minedOre From Smelter: Ore is smelted From Finder: ore From Miner: minedOre From Smelter: Ore is smelted 

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

غوروتينز مجهول




مثلما نقوم بتشغيل دالة عادية في goroutine ، يمكننا الإعلان عن وظيفة مجهولة مباشرة بعد الكلمة الأساسية go واستدعاؤها باستخدام بناء الجملة التالي:

 //   go func() { fmt.Println("I'm running in my own go routine") }() 

وبالتالي ، إذا كنا بحاجة إلى استدعاء دالة في مكان واحد فقط ، فيمكننا تشغيلها في غوروتين منفصل دون القلق بشأن إعلانها مسبقًا.

الوظيفة الرئيسية هي الغوروتين.




نعم ، الوظيفة الرئيسية تعمل في الغوروتين الخاص بها. والأهم من ذلك ، بعد اكتماله ، تنتهي جميع الغوروتينات الأخرى أيضًا. ولهذا السبب وضعنا مكالمة توقيت في نهاية وظيفتنا الرئيسية . تعمل هذه المكالمة على إنشاء قناة وإرسال البيانات إليها بعد 5 ثوانٍ.

 <-time.After(time.Second * 5) //       

تذكر أنه سيتم حظر الغوروتين عند القراءة من القناة حتى يتم إرسال شيء إليها؟ هذا هو بالضبط ما يحدث عند إضافة الرمز المحدد. سيتم حظر الغوروتين الرئيسي ، مما يمنح الغوروتيات الأخرى 5 ثوان من وقت العمل. تعمل هذه الطريقة بشكل جيد ، ولكن عادة ما يتم استخدام نهج مختلف للتحقق من اكتمال جميع الغوروتينات. لإرسال إشارة حول الانتهاء من العمل ، يتم إنشاء قناة خاصة ، يتم حظر الغوروتين الرئيسي من القراءة منه ، وبمجرد أن تكمل الغوروتين الابنة عملها ، تكتب إلى هذه القناة ؛ يتم فتح goroutine الرئيسي وينتهي البرنامج.



 func main() { doneChan := make(chan string) go func() { //  -  doneChan <- “I'm all done!” }() <-doneChan //        } 

اقرأ من ماسورة في حلقة النطاق


في مثالنا ، في وظيفة goffer-getter ، استخدمنا حلقة for لتحديد ثلاثة عناصر من القناة. ولكن ماذا تفعل إذا لم يكن مقدماً كمية البيانات التي يمكن أن تكون في القناة؟ في مثل هذه الحالات ، يمكنك استخدام القناة كوسيطة للحلقة للنطاق ، تمامًا كما هو الحال مع المجموعات. قد تبدو الوظيفة المحدثة كما يلي:

  //  go func() { for foundOre := range oreChan { fmt.Println(“Miner: Received “ + foundOre + “ from finder”) } }() 

وهكذا ، فإن عامل المنجم الخام سيقرأ كل ما يرسله الكشاف ؛ واستخدام القناة في الدورة سيضمن ذلك. يرجى ملاحظة أنه بعد معالجة جميع البيانات من القناة ، سيتم قفل الدورة عند القراءة ؛ لتجنب الحظر ، تحتاج إلى إغلاق القناة عن طريق الاتصال بإغلاق (قناة) .

قراءة القناة غير المحجوبة


باستخدام تركيبة الحالة المحددة ، يمكن تجنب حجب القراءات من الأنبوب. فيما يلي مثال على استخدام هذا البناء: سيقرأ goroutine البيانات من القناة ، إذا كانت موجودة فقط ، وإلا سيتم تنفيذ الكتلة الافتراضية :

 myChan := make(chan string) go func(){ myChan <- “Message!” }() select { case msg := <- myChan: fmt.Println(msg) default: fmt.Println(“No Msg”) } <-time.After(time.Second * 1) select { case msg := <- myChan: fmt.Println(msg) default: fmt.Println(“No Msg”) } 

بمجرد إطلاقه ، سيخرج هذا الرمز ما يلي:

 No Msg Message! 

تسجيل القناة غير المحجوبة


يمكن تجنب الأقفال عند الكتابة إلى قناة باستخدام نفس بنية حالة التحديد . لنقم بتحرير صغير للمثال السابق:

 select { case myChan <- “message”: fmt.Println(“sent the message”) default: fmt.Println(“no message sent”) } 

ما لمزيد من الدراسة




هناك عدد كبير من المقالات والتقارير التي تغطي العمل مع القنوات والكوروتين بمزيد من التفصيل. والآن ، باستخدام الشفرة ، لديك فكرة واضحة عن سبب وكيفية استخدام هذه الأدوات ، يمكنك الحصول على أقصى استفادة من المواد التالية:



شكرا لأخذ الوقت للقراءة. آمل أن أكون قد ساعدتك على فهم القنوات ، والخطوات ، والفوائد التي تقدمها لك البرامج متعددة المقالات.

Source: https://habr.com/ru/post/ar412715/


All Articles