गो पारंपरिक थ्रेडिंग मॉडल की तुलना में समवर्तीता को बेहतर ढंग से कैसे संभालता है

गो पारंपरिक थ्रेडिंग मॉडल की तुलना में समवर्तीता को बेहतर ढंग से कैसे संभालता है

आधुनिक सॉफ्टवेयर इंजीनियरिंग में, ऐसे एप्लिकेशन बनाना जो एक साथ कई कार्य कर सकें, अब कोई विलासिता नहीं रह गई है - यह एक मुख्य आवश्यकता है। उच्च-थ्रूपुट वेब सर्वर से लेकर वास्तविक समय स्ट्रीमिंग सेवाओं तक, समवर्ती प्रदर्शन के केंद्र में है।

दशकों तक, C++, Java और Python जैसी पारंपरिक प्रोग्रामिंग भाषाएं समवर्ती कार्यों को संभालने के लिए ऑपरेटिंग सिस्टम के मूल थ्रेडिंग मॉडल पर निर्भर थीं। हालाँकि, जब Google ने 2000 के दशक के अंत में गो (गोलंग) को डिज़ाइन किया, तो उन्होंने एक बिल्कुल अलग रास्ता अपनाया। कच्चे ओएस थ्रेड्स को उजागर करने के बजाय, गो ने गोरूटीन्स और एक विशेष एम:एन शेड्यूलर पेश किया।

इस लेख में, हम पारंपरिक थ्रेडिंग मॉडल की वास्तुशिल्प सीमाओं की जांच करेंगे और पता लगाएंगे कि गो का समवर्ती डिज़ाइन काफी अधिक कुशल, स्केलेबल और डेवलपर-अनुकूल क्यों है।


1. पारंपरिक थ्रेडिंग की अड़चनें (1:1 मॉडल)

अधिकांश पारंपरिक रनटाइम सिस्टम 1:1 थ्रेडिंग मॉडल का उपयोग करते हैं। इस मॉडल में, उपयोगकर्ता-स्पेस कोड में बनाया गया प्रत्येक थ्रेड सीधे ऑपरेटिंग सिस्टम (ओएस) द्वारा प्रबंधित एक कर्नेल-स्पेस थ्रेड पर मैप होता है।

सरल होते हुए भी, यह 1:1 मानचित्रण तीन महत्वपूर्ण बाधाओं का परिचय देता है:

ए. मेमोरी ओवरहेड (बड़े स्टैक आकार)

OS थ्रेड एक भारी संसाधन है. डिफ़ॉल्ट रूप से, ऑपरेटिंग सिस्टम प्रत्येक थ्रेड के लिए एक निश्चित, सन्निहित स्टैक आकार आवंटित करते हैं (आमतौर पर 1MB से 8MB)।

  • यदि आप 10,000 समवर्ती कनेक्शनों को संभालना चाहते हैं, तो 10,000 ओएस थ्रेड आवंटित करने के लिए केवल थ्रेड स्टैक मेमोरी के लिए 10 जीबी और 80 जीबी रैम की आवश्यकता होगी।
  • इससे उच्च कनेक्शन गणना के तहत उच्च संगामिति को संभालना मानक सर्वर हार्डवेयर पर बेहद महंगा और लगभग असंभव हो जाता है।

बी. उच्च संदर्भ-स्विचिंग लागत

जब OS निष्पादन को एक थ्रेड से दूसरे थ्रेड में स्विच करता है, तो यह संदर्भ स्विच निष्पादित करता है। चूँकि OS थ्रेड्स को कर्नेल द्वारा प्रबंधित किया जाता है, इसलिए संदर्भ स्विच के लिए उपयोगकर्ता-कर्नेल सीमा को पार करना आवश्यक होता है। सीपीयू को चाहिए:

  1. वर्तमान रजिस्टरों की स्थिति सहेजें।
  2. सीपीयू कैश लाइनों को फ्लश करें और पेज टेबल्स (ट्रांसलेशन लुकसाइड बफ़र्स) को अपडेट करें।
  3. कर्नेल मोड में जाएं, अगला थ्रेड चुनें, और उसकी सहेजी गई स्थिति लोड करें।
  4. उपयोगकर्ता मोड पर वापस जाएँ।

इस संपूर्ण राउंड-ट्रिप में लगभग 1 से 2 माइक्रोसेकंड का समय लगता है, जो वास्तविक व्यावसायिक तर्क को निष्पादित करने के बजाय पूरी तरह से प्रशासनिक ओवरहेड पर खर्च किए गए सैकड़ों या हजारों सीपीयू चक्रों का प्रतिनिधित्व करता है।

सी. ओएस शेड्यूलिंग सीमाएं

OS अनुसूचक सामान्य-उद्देश्य है। यह डेटाबेस थ्रेड्स, यूआई थ्रेड्स और हल्के नेटवर्क कनेक्शन को समान शेड्यूलिंग अनुमान के साथ व्यवहार करता है। क्योंकि यह एप्लिकेशन-स्तरीय संदर्भ को नहीं समझता है, यह इस आधार पर शेड्यूलिंग को अनुकूलित नहीं कर सकता है कि नेटवर्क सॉकेट पर थ्रेड अवरुद्ध है या नहीं, आंतरिक लॉक की प्रतीक्षा कर रहा है या सक्रिय गणना कर रहा है।


2. गो का समाधान: एम:एन शेड्यूलर (गोरोइन्स)

गो गोरआउटिंस को शुरू करके और अपने स्वयं के रनटाइम शेड्यूलर को लागू करके ओएस थ्रेडिंग की सीमाओं को दरकिनार कर देता है। थ्रेड्स 1:1 को मैप करने के बजाय, गो एक एम:एन शेड्यूलिंग मॉडल का उपयोग करता है जहां एम हल्के गोरोइन को एन भौतिक ओएस थ्रेड्स पर मल्टीप्लेक्स किया जाता है।

पारंपरिक 1:1 थ्रेडिंग बनाम गो एम:एन शेड्यूलर मॉडल आरेख

यह वास्तुकला तीन प्राथमिक संरचनात्मक तत्वों द्वारा शासित होती है, जिन्हें अक्सर जीएमपी मॉडल के रूप में जाना जाता है:

  • जी (गोरोइन): एकल गोरोइन का प्रतिनिधित्व करता है। इसमें गोरोइन का निष्पादन स्टैक, प्रोग्राम काउंटर और राज्य शामिल है। G कोई OS थ्रेड नहीं है; यह एक सरल गो संरचना है जिसे आरंभ करने में केवल 2KB मेमोरी खर्च होती है।
  • एम (मशीन): एक भौतिक ओएस/कर्नेल थ्रेड का प्रतिनिधित्व करता है। इसे OS शेड्यूलर द्वारा प्रबंधित किया जाता है और यह गोरआउट्स के मशीन कोड निर्देशों को निष्पादित करने के लिए जिम्मेदार है।
  • पी (प्रोसेसर): एक तार्किक प्रोसेसर या निष्पादन संदर्भ का प्रतिनिधित्व करता है। P इंस्टेंसेस की संख्या होस्ट मशीन पर तार्किक CPU कोर की संख्या पर डिफ़ॉल्ट होती है (GOMAXPROCS द्वारा नियंत्रित)। गो कोड चलाने के लिए एक मशीन M को एक लॉजिकल प्रोसेसर P प्राप्त करना होगा।

जीएमपी मॉडल सुपीरियर क्यों है

क्योंकि गोरोइन को गो रनटाइम द्वारा पूरी तरह से उपयोगकर्ता-स्थान में प्रबंधित किया जाता है:

  1. डायनामिक स्टैक: एक गोरोइन 2KB के छोटे स्टैक से शुरू होता है। जैसे-जैसे निष्पादन की मांग होती है, स्टैक गतिशील रूप से बढ़ता है (ढेर में बड़े सन्निहित मेमोरी सेगमेंट आवंटित करता है) और सिकुड़ता है। यह गो को एक ही लैपटॉप पर एक साथ सैकड़ों-हजारों गोरोइन चलाने की अनुमति देता है।
  2. तेज संदर्भ स्विच: गोरोइन के बीच स्विचिंग कर्नेल संदर्भ स्विच को लागू किए बिना पूरी तरह से उपयोगकर्ता-स्थान में होती है। केवल प्रोग्राम काउंटर और कुछ सीपीयू रजिस्टर सहेजे गए हैं। यह उपयोगकर्ता-स्थान संदर्भ स्विच केवल 10 से 100 नैनोसेकंड लेता है - ओएस थ्रेड स्विच की तुलना में लगभग 10x से 100x तेज।

3. कार्य चोरी और नॉन-ब्लॉकिंग I/O

गो रनटाइम यह सुनिश्चित करने के लिए दो उन्नत शेड्यूलिंग तंत्रों का उपयोग करता है कि भौतिक सीपीयू कोर का कभी भी कम उपयोग न हो: कार्य चोरी और सिस्कल हैंड-ऑफ

ए. कार्य-चोरी एल्गोरिथ्म

प्रत्येक लॉजिकल प्रोसेसर P गोरआउट्स की अपनी स्थानीय रन कतार बनाए रखता है। इसके अतिरिक्त, अतिप्रवाह के लिए एक वैश्विक रन कतार है। यदि कोई प्रोसेसर P अपनी स्थानीय रन कतार में सभी गोरआउट्स को समाप्त कर देता है, तो वह सो नहीं जाता है। इसके बजाय, यह कार्य चोरी करता है: यह अन्य तार्किक प्रोसेसर की जांच करता है और सभी सीपीयू कोर पर कार्यभार को संतुलित करने के लिए उनके कतारबद्ध गोरोइन का आधा हिस्सा चुरा लेता है।

Local Queue P1: [ G1, G2, G3 ]  ---> Running on Thread M1
Local Queue P2: [ ]            ---> Running on Thread M2 (Idle)
                                     P2 steals G3 and G2 from P1!

बी. नेटवर्क पोलर और नॉन-ब्लॉकिंग I/O

जब कोई गोरोइन नेटवर्क I/O निष्पादित करता है (उदाहरण के लिए, डेटाबेस कनेक्शन से पढ़ना या HTTP कॉल करना), तो गो अंतर्निहित OS थ्रेड M को ब्लॉक नहीं करता है।

इसके बजाय, गो रनटाइम एक समर्पित नेटवर्क पोलर के साथ ब्लॉक को पंजीकृत करता है (जो लिनक्स पर epoll, macOS पर kqueue, या विंडोज़ पर IOCP जैसे कुशल ओएस-विशिष्ट मल्टीप्लेक्सिंग एपीआई का उपयोग करता है)। अवरुद्ध गोरोइन G को थ्रेड M से अलग किया जाता है और नेटवर्क पोलर में पार्क किया जाता है।

थ्रेड M तुरंत कतार से एक और चलने योग्य गोरआउटिन उठाता है। एक बार जब I/O ईवेंट पूरा हो जाता है, तो नेटवर्क पोलर निष्पादन को फिर से शुरू करने के लिए G को सक्रिय रन कतार में वापस ले जाता है।


4. चैनल बनाम साझा मेमोरी (सीएसपी मॉडल)

पारंपरिक थ्रेडिंग मॉडल मेमोरी साझा करके समवर्ती कार्यों का समन्वय करते हैं (उदाहरण के लिए, थ्रेड्स के बीच पॉइंटर्स पास करना)। डेटा रेस को रोकने के लिए, डेवलपर्स को लॉक, म्यूटेक्स और कंडीशन वेरिएबल्स को मैन्युअल रूप से प्रबंधित करना होगा:

// Traditional Java Shared Memory Approach
synchronized(lock) {
    sharedResource.updateState();
}

यह मॉडल बेहद त्रुटि-प्रवण है, जिसके परिणामस्वरूप अक्सर गतिरोध, दौड़ की स्थिति और कैश सुसंगतता बाधाएं उत्पन्न होती हैं।

गो संचार अनुक्रमिक प्रक्रियाओं (सीएसपी) औपचारिक मॉडल को लागू करता है, जिसे प्रसिद्ध गोलांग कहावत द्वारा संक्षेपित किया गया है:

“स्मृति साझा करके संवाद न करें; इसके बजाय, संचार करके स्मृति साझा करें।”

गो प्रथम श्रेणी के प्राइमेटिव के रूप में चैनल प्रदान करता है। चैनल प्रकार-सुरक्षित कतारों के रूप में कार्य करते हैं जो गोरआउट्स को निष्पादन को सिंक्रनाइज़ करने और डेटा के स्वामित्व को स्थानांतरित करने के लिए संदेश भेजने और प्राप्त करने की अनुमति देते हैं।


5. व्यावहारिक कार्यान्वयन: गोरआउट्स और कार्रवाई में चैनल

यहां एक व्यावहारिक गो प्रोग्राम है जो दर्शाता है कि कैसे कई कार्यकर्ता गोरोइन कार्यों को समवर्ती रूप से संसाधित कर सकते हैं और एक चैनल के माध्यम से परिणाम लौटा सकते हैं, डेटा प्रवाह को बिना ताले के सुरक्षित रूप से प्रबंधित कर सकते हैं।

package main

import (
	"fmt"
	"time"
)

// Task represents a unit of work
type Task struct {
	ID       int
	Duration time.Duration
}

// Result represents the outcome of a processed task
type Result struct {
	TaskID int
	Value  string
}

// worker processes incoming tasks from the jobs channel and sends results
func worker(id int, jobs <-chan Task, results chan<- Result) {
	for job := range jobs {
		fmt.Printf("Worker %d: Started processing job %d\n", id, job.ID)
		time.Sleep(job.Duration) // Simulate CPU/IO delay
		
		results <- Result{
			TaskID: job.ID,
			Value:  fmt.Sprintf("Processed by worker %d in %v", id, job.Duration),
		}
	}
}

func main() {
	// Create tasks
	tasks := []Task{
		{ID: 101, Duration: 200 * time.Millisecond},
		{ID: 102, Duration: 400 * time.Millisecond},
		{ID: 103, Duration: 100 * time.Millisecond},
		{ID: 104, Duration: 300 * time.Millisecond},
	}

	// Buffered channels prevent sender blocking
	jobs := make(chan Task, len(tasks))
	results := make(chan Result, len(tasks))

	// Spawn 3 concurrent worker goroutines
	for w := 1; w <= 3; w++ {
		go worker(w, jobs, results)
	}

	// Feed tasks into the job channel
	for _, task := range tasks {
		jobs <- task
	}
	close(jobs) // Closing tells workers no more jobs are coming

	// Collect and aggregate results
	for i := 0; i < len(tasks); i++ {
		res := <-results
		fmt.Printf("[RESULT] Job %d: %s\n", res.TaskID, res.Value)
	}
	close(results)
}

6. आर्किटेक्चर तुलना: ओएस थ्रेड्स बनाम गोरोइन्स

मीट्रिक / फ़ीचर पारंपरिक ओएस थ्रेड्स (1:1 मॉडल) गो गोरोइन्स (एम:एन मॉडल)
स्टार्टअप मेमोरी निश्चित (आम तौर पर 1एमबी - 8एमबी) गतिशील (~2KB से शुरू होता है)
संदर्भ स्विच समय धीमा (1,000 - 2,000 नैनोसेकंड) तेज़ (10 - 100 नैनोसेकंड)
स्विचिंग स्पेस कर्नेल-स्पेस (भारी संदर्भ स्विच) यूजर-स्पेस (रनटाइम शेड्यूलर)
निर्माण लागत महँगा (OS सिस्टम कॉल शामिल है) बेहद सस्ता (सरल आवंटन)
संचार साझा की गई मेमोरी (म्यूटेक्स, सेमाफोर) चैनल (सीएसपी संदेश पासिंग)
गतिरोध जोखिम उच्च (मैन्युअल रूप से पता लगाना कठिन) चैनल डिज़ाइन और संकलन रनटाइम डिटेक्शन द्वारा कम किया गया

निष्कर्ष

ऑपरेटिंग सिस्टम के कच्चे थ्रेडिंग मॉडल से समवर्तीता को अलग करके, गो ने आधुनिक बैकएंड आर्किटेक्चर की मूलभूत स्केलिंग समस्याओं को हल किया।

गोरोइन्स न्यूनतम मेमोरी ओवरहेड के साथ बड़े पैमाने पर समवर्ती को सक्षम बनाता है, एम: एन शेड्यूलर कर्नेल संदर्भ-स्विचिंग लागत के बिना काम चोरी के माध्यम से सीपीयू उपयोग को अनुकूलित करता है, और चैनल एक सुरक्षित, अभिव्यंजक समवर्ती प्रतिमान प्रदान करते हैं।

चाहे आप माइक्रोसर्विसेज या बड़े वितरित सिस्टम का निर्माण कर रहे हों, गो की अंतर्निहित समवर्ती वास्तुकला सुनिश्चित करती है कि आपका एप्लिकेशन उत्तरदायी, संसाधन-कुशल और बनाए रखने में आसान बना रहे।


ग़ज़निक्स ब्लॉग पर अधिक सॉफ़्टवेयर विकास और बैकएंड इंजीनियरिंग अंतर्दृष्टि का अन्वेषण करें →