Vivasoft-logo

[৪] কনকারেন্সি (Concurrency)

এই পর্যায়ে আমরা পরিচিত হবো Go কনকারেন্সির সাথে। Go প্রোগ্রামিং এ খুবই গুরুত্বপূর্ণ একটি অংশ হল কনকারেন্সি। আমরা অনেকেই কনকারেন্সি এবং প্যারালেলিজম শব্দ দুটি নিয়ে দ্বিধান্বিত থাকি। আশা রাখছি, বুটক্যাম্পের এই অংশে কনকারেন্সি এবং প্যারালেলিজম এর ধারণা পরিষ্কার হয়ে যাবে এবং Go তে এর প্রয়োগ সম্পর্কেও ধারণা পেয়ে যাবো।

 

কনকারেন্সি হল এমন একটি প্রক্রিয়া যেখানে অনেকগুলো কাজ বা অপারেশন একইসাথে সম্পাদন হতে পারে, কিন্তু একই সময়ে হতে পারে না। যেমন, একজন শ্রমিক রঙ করার কাজ এবং দেয়াল বানানোর কাজ করতে পারে। কিন্তু একই সময়ে দেয়াল বানানো এবং রং করার কাজ করতে পারে না।

Go Concurrency 1 Goroutine


             প্যারালেলিজম হল এমন একটি প্রক্রিয়া যেখানে অনেকগুলো কাজ একই সাথে এবং একই সময়ে হতে পারে। যেমন, একজন রাঁধুনি কিন্তু চাইলেই একাধিক চুলা ব্যবহার করে একসাথে কয়েকটি খাবার রান্না করতে পারেন, এখানে একটি খাবার রান্না হওয়ার জন্য আরেকটি খাবার রান্না শেষ হবার প্রয়োজন নেই।

parallelism Goroutine

Go এর নিজস্ব গো-রুটিন এবং চ্যানেলের মাধ্যমে কনকারেন্সি অর্জন করে থাকে। এই পর্যায়ে আমরা গো-রুটিন, চ্যানেল, ওয়েটগ্রুপ,  সিঙ্কগ্রুপ নিয়ে আলোচনা করবো।

[৪.১] গো-রুটিন (Goroutine)

  আমরা আগেই বলেছি গো-রুটিন ব্যবহারের মাধ্যমে, Go কনকারেন্সি অর্জন করে থাকে। তাহলে দেখা যাক কী ভাবে আমরা গো-রুটিন ব্যবহারের মাধ্যমে কনকারেন্সি অর্জন করতে পারি। কিন্তু তার আগে আমাদের জানতে হবে গো-রুটিন কী, এর ব্যবহার এবং কাজ।

[৪.১.১] গো-রুটিন কী ?

গো-রুটিন হলো ফাংশন বা মেথড যা অন্যান্য ফাংশন বা মেথডের সাথে একইসময়ে বা একইসাথে রান করে। আমরা চাইলে গো-রুটিন কে thread এর সাথে তুলনা করতে পারি। একটি thread এর তুলনায় একটি গো-রুটিন তৈরিতে, রিসোর্সের প্রয়োজনীয়তা অনেক কম এবং একটি সিঙ্গেল অ্যাপ্লিকেশনে অনেকগুলো গো-রুটিন একইসময়ে ও একইসাথে চলতে পারে।

[৪.১.২]  কীভাবে গো-রুটিন  শুরু করবো ?

ভয় পাবার কোন কিছু নেই কারণ গো-রুটিন শুরু করা খুবই সহজ। ফাংশন বা মেথডের আগে go কিওয়ার্ড লিখে দিলেই নতুন গো-রুটিন  তৈরি হয়ে যাবে। নিচে উদাহরেনের সাহায্যে দেখা যাক – 

				
					 package main

   import ( 
       "fmt"
   )

   func hello() { 
       fmt.Println("Hello world Goroutine")
   }
   func main() { 
       go hello()
       fmt.Println("main function")
   }

   //Output :
   //main function
   //Program exited.

				
			

  উপরের উদাহরণে, go hello() দ্বারা নতুন গো-রুটিন  তৈরি হয়েছে। এখন hello() ফাংশনটি main() ফাংশনের সাথে একইসাথে রান করবেmain() ফাংশন নিজেও একটি গো-রুটিন হিসেবে রান হয় যাকে বলা হয় মেইন গো-রুটিন।  

 উপরের আউটপুটের দিকে যদি লক্ষ করি তাহলে দেখতে পাবো শুধু “main function” লেখাটি প্রিন্ট হয়েছে। তাহলে কি আমাদের গো-রুটিন কাজ করেনি ? বিষয়টা আসলে এমন না, এটা বুঝতে হলে আগে আমাদের গো-রুটিন এর দুইটা বৈশিষ্ট্য বুঝতে হবে। দেখা যাক বৈশিষ্ট্য দুটি কী – 

 

১। যখন একটি নতুন গো-রুটিন শুরু হয়, তখন অন্য ফাংশনগুলো গো-রুটিন রিটার্ন আসা পর্যন্ত অপেক্ষা করে না এবং কোডের পরবর্তী লাইনগুলো এক্সিকিউট করতে থাকে।

 

২। আগেই বলেছিলাম main() ফাংশন তার নিজস্ব গো-রুটিন এ চলে। এখন অন্য কোন গো-রুটিন চালানোর জন্য মেইন গো-রুটিনটি  অবশ্যই চলমান হতে হবে। যদি মেইন গো-রুটিন বন্ধ হয়ে যায় তাহলে প্রোগ্রামটি বন্ধ হয়ে যাবে এবং অন্য কোন গো-রুটিন আর চলবে না। 

এই একই কাজটাই আমাদের প্রোগ্রামের ক্ষেত্রে হয়েছে, যার কারণে আমাদের গো-রুটিন সম্পূর্ণভাবে এক্সিকিউট হয়নি। 

এবার এই সমস্যার সমাধান করা যাক। নিচের উদাহরণটা খেয়াল করি – 

				
					  package main

   import ( 
       "fmt"
       "time"
   )

   func hello() { 
       fmt.Println("Hello world Goroutine")
   }
   func main() { 
       go hello()
       time.Sleep(1 * time.Second)
       fmt.Println("main function")
   }

   //Output :
   //Hello world Goroutine
   //main function

				
			

এখানে আমরা hello() ফাংশনকে main() ফাংশনের ভেতর থেকে গো-রুটিনে পাঠিয়েছি। এখন hello() এর জন্য main() ফাংশন অপেক্ষা না করে পরের লাইনে চলে যাবে। কিন্তু পরের লাইনে time.Sleep() কে কল করেছি এবং তাকে এক সেকেন্ড অপেক্ষা করতে বলেছি। এই অপেক্ষাকালীন সময়েই গো-রুটিন এর ফাংশনটি সম্পূর্ণভাবে এক্সিকিউট হয়ে যাবে এবং কনসোলে “Hello word Goroutine” প্রিন্ট করবে। এই এক সেকেন্ড অতিক্রান্ত হলে পরের কোড গুলো এক্সিকিউট হওয়া শুরু করবে, অর্থাৎ “main function” মেসেজটি প্রিন্ট হয়ে প্রোগ্রাম শেষ হবে।

[৪.১.৩]  মাল্টিপল গো-রুটিন (Multiple Goroutine)  

আগেই বলা হয়েছে একটা সিঙ্গেল অ্যাপ্লিকেশনে একাধিক গো-রুটিন থাকতে পারে। নিচের উদাহরণের সাহায্যে মাল্টিপল গো-রুটিন বুঝার চেষ্টা করি –

				
					 package main

   import ( 
       "fmt"
       "time"
   )

   func numbers() { 
       for i := 1; i <= 5; i++ {
           time.Sleep(250 * time.Millisecond)
           fmt.Printf("%d ", i)
       }
   }
   
   func alphabets() { 
       for i := 'a'; i <= 'e'; i++ {
           time.Sleep(400 * time.Millisecond)
           fmt.Printf("%c ", i)
       }
   }
   
   func main() { 
       go numbers()
       go alphabets()
       time.Sleep(3000 * time.Millisecond)
       fmt.Println("main terminated")
   }

   //Output : 1 a 2 3 b 4 c 5 d e main terminated

				
			

 উপরের উদাহরণে, দুইটি গো-রুটিন numbers() এবং alphabets() কনকারেন্টলি চলতেছে। numbers গো-রুটিন শুরুতে 250 মিলিসেকেন্ড স্লীপের পর 1 প্রিন্ট করবে, এরপর আরও 250 মিলিসেকেন্ড পরে 2 প্রিন্ট করবে, এভাবে 5 পর্যন্ত প্রিন্ট করতে থাকবে। সেইম alphabets() 400 মিলিসেকেন্ড পরপর a থেকে e  পর্যন্ত প্রিন্ট করতে থাকবে। তারপর 3000 মিলিসেকেন্ড পর “main terminated” প্রিন্ট হবে এবং main() ফাংশন বন্ধ হয়ে যাবে।

 

আসলে প্রোগ্রামটি কীভাবে কাজ করছে নিচের ডায়াগ্রামের সাহায্যে বুঝার চেষ্টা করি :

4.1.3 image

ছবিটির প্রথম অংশের নীল কালার দ্বারা  numbers গো-রুটিন , দ্বিতীয় অংশের লাল কালার দ্বারা alphabets গো-রুটিন  , তৃতীয় অংশের সবুজ কালার দ্বারা main গো-রুটিন এবং সর্বশেষ কালো কালার দ্বারা অউটপুট প্রকাশ করা হয়েছে। আশা করা যায়  গো-রুটিন সম্পর্কে আমরা মোটামুটি বুঝতে পেরেছি।

[৪.১.৪] ক্লোজিং গো-রুটিন(Closing Goroutine)

গো-রুটিনের ব্যবহার শেষে তা সঠিকভাবে বন্ধ করাটা জরুরি। এই পর্যায়ে আমরা সঠিক পদ্ধতিতে গো-রুটিন বন্ধ করার ধাপগুলোর সাথে পরিচিত হবো – 

  • গো-রুটিন বন্ধ করার সবথেকে সহজ উপায় হলো সিগনাল ব্যবহার করে। এই পদ্ধতিতে চ্যানেলের মাধ্যমে একটি সিগন্যাল উক্ত গো-রুটিনে পাঠানো হয় এবং গো-রুটিন ওই চ্যানেলে কিছু রিসিভের অপেক্ষায় থাকে, যা পেয়ে গেলে নিজে থেকে বন্ধ হয়ে যায়। 
  • প্রথমে একটি চ্যানেল তৈরি করে নেবো যার দ্বারা আমরা সিগনালটি পাঠাবো – 
				
					   cancel := make(chan struct{})
				
			
  •  চ্যানেলটি একটি গো-রুটিনের প্যারামিটার হিসেবে পাস করি – 
				
					   go worker(cancel)
				
			
  • ফাংশনটি – 

				
					 func worker(cancel <-chan struct{}) {
       fmt.Println("Worker started")
       defer fmt.Println("Worker stopped")
       for {
           select {
           case <-cancel:
               fmt.Println("Worker cancelled")
               return
           default:
               // Do some work here
               time.Sleep(time.Second)
               fmt.Println("Working...")
           }
       }
   }

				
			
  • এবারে cancel চ্যানেলে ক্লোজিং সিগনাল পাঠিয়ে গো-রুটিনটি বন্ধ হবার জন্য অপেক্ষা করব – 
				
					time.Sleep(5 * time.Second)
   fmt.Println("Canceling worker")
   close(cancel)


   time.Sleep(2 * time.Second)
   fmt.Println("Main goroutine stopped")
				
			
  •  গো-রুটিনটি বন্ধ হয়ে গেলে এরপর main() ফাংশনের কাজ শেষে প্রোগ্রামটির কাজ শেষ হবে। তাহলে পুরো কোডটি দাঁড়ালো – 
				
					 package main

   import (
       "fmt"
       "time"
   )

   func worker(cancel <-chan struct{}) {
       fmt.Println("Worker started")
       defer fmt.Println("Worker stopped")
       for {
           select {
           case <-cancel:
               fmt.Println("Worker canceled")
               return
           default:
               // Do some work here
               time.Sleep(time.Second)
               fmt.Println("Working...")
           }
       }
   }


   func main() {
       cancel := make(chan struct{})
       go worker(cancel)
       time.Sleep(5 * time.Second)
       fmt.Println("Canceling worker")
       close(cancel)
       time.Sleep(2 * time.Second)
       fmt.Println("Main goroutine stopped")
   }

				
			
  • time.Sleep() ব্যবহারের উদ্দেশ্য হলো গো-রুটিনের কাজ ঠিকমতো করতে দেয়া।

আউটপুট – 

				
					Worker started
   Working...
   Working...
   Working...
   Working...
   Canceling worker
   Working...
   Worker canceled
   Worker stopped
   Main goroutine stopped