2025, Oct 18 08:31

Python asyncio में एक ही लोडर सुनिश्चित करने का तरीका: double‑checked locking

Python asyncio में डेटा रिफ्रेश करते समय डुप्लिकेट लोड कैसे रोकें? double‑checked locking से केवल एक coroutine को लोड करने दें और रेस कंडीशन, फालतू काम से बचें.

Asyncio: डुप्लिकेट काम से बचते हुए एक ही लोडर सुनिश्चित करना

जब कोई सेवा Redis-समर्थित टाइमस्टैम्प पर नज़र रखती है और बदलाव होते ही इन‑मेमोरी डेटा को रीफ़्रेश करती है, तो लक्ष्य सरल है: एक समय में केवल एक coroutine को लोड करना चाहिए। समस्या तब आती है जब कई coroutines उसी await बिंदु तक दौड़ लगाते हैं और फिर एक‑के‑बाद‑एक वही काम दोहराते हैं। समाधान चौंकाने वाला छोटा है, लेकिन अहम।

समस्या की रूपरेखा

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

import asyncio
class SyncAgent:
    def __init__(self):
        self.gate = asyncio.Lock()
        self.mark = None
    async def _pull_mark_from_store(self):
        # डेटाबेस पर जाएं
        ...
    async def apply_if_needed(self):
        latest = await self._pull_mark_from_store()
        if self.mark == latest:
            return
        if self.gate.locked():
            return  # लोड चल रहा है; फिलहाल मौजूदा डेटा का उपयोग करना ठीक है
        async with self.gate:
            # लोड करें
            self.mark = latest

गड़बड़ी कहाँ होती है और क्यों

लॉक पकड़ा गया है या नहीं — यह जांच क्रिटिकल सेक्शन में प्रवेश करने से पहले होती है। कई coroutines लॉक को खाली देखकर एक साथ async with gate वाली पंक्ति तक पहुंच सकते हैं। इसके बाद वे बारी-बारी से क्रिटिकल सेक्शन में प्रवेश करेंगे और लोड दोहराएँगे। मूल वजह है प्री‑चेक और लॉक हासिल करने के बीच रेस। await पर रुके रहने के दौरान जिसकी तुलना हो रही है वह वैल्यू बदल सकती है, इसलिए लॉक के बाहर एक ही प्री‑चेक पर्याप्त नहीं है।

समाधान: double-checked locking

यहाँ कारगर पैटर्न है double‑checked locking: जब कुछ नहीं बदला हो तो अनावश्यक लॉकिंग से बचने के लिए पहले एक तेज़ प्री‑चेक करें, और लॉक लेने के बाद वही जांच दोहराएँ। इससे सिर्फ वही पहला coroutine, जिसे बदलाव दिखा, वास्तविक लोड करता है; बाकी बाद में आते हैं, दोबारा जाँचते हैं और देखते हैं कि अब करने को कुछ नहीं बचा।

जांच को लॉक के भीतर ले जाना सही है।

import asyncio
class SyncAgent:
    def __init__(self):
        self.gate = asyncio.Lock()
        self.mark = None
    async def _pull_mark_from_store(self):
        ...
    async def apply_if_needed(self):
        latest = await self._pull_mark_from_store()
        if self.mark == latest:
            return
        async with self.gate:
            if self.mark == latest:
                return
            # लोड करें
            self.mark = latest

इससे इच्छित व्यवहार बना रहता है। बाहर की जांच तब अनावश्यक लॉक अधिग्रहण से बचाती है जब कुछ नहीं बदला हो। अंदर की जांच उस स्थिति में शुद्धता सुनिश्चित करती है जब कई coroutines एक ही await सीमा पर आ जुटते हैं।

वैकल्पिक: लोड के दौरान तेज़ी से वापस लौटना

कुछ स्थितियों में, लॉक लेने के लिए इंतज़ार करने के बजाय, बेहतर होता है कि यदि लोड पहले से चल रहा हो तो तुरंत लौट आएँ। यह तरीका लॉक पर कतार लगाने से बचाता है, लेकिन शुद्धता के लिए फिर भी अंदर की जांच पर निर्भर रहता है। यह asyncio के साथ उपयुक्त है क्योंकि समवर्ती कोड में await के स्पष्ट बिंदु होते हैं; बहु‑थ्रेडेड concurrency के लिए नहीं।

import asyncio
class SyncAgent:
    def __init__(self):
        self.gate = asyncio.Lock()
        self.mark = None
    async def _pull_mark_from_store(self):
        ...
    async def apply_if_needed(self):
        latest = await self._pull_mark_from_store()
        if self.mark == latest:
            return
        if self.gate.locked():
            return  # इंतज़ार न करें; कोई अन्य coroutine पहले से लोड कर रहा है
        async with self.gate:
            if self.mark == latest:
                return
            # लोड करें
            self.mark = latest

यह क्यों मायने रखता है

अंदर की जांच के बिना, कई coroutines एक ही महंगा लोड कर सकते हैं। double‑checked locking कोड को सरल रखता है और फालतू काम रोकता है। यह यहाँ आम पैटर्न है, क्योंकि await बिंदुओं के बीच स्टेट बदल सकती है, और दूसरी जांच ही रेस की खिड़की बंद करती है।

निष्कर्ष

asyncio में coroutines के बीच अपडेट समन्वित करते समय, काम और उसकी पुष्टि—दोनों की सुरक्षा के लिए लॉक पर भरोसा करें। अनावश्यक अधिग्रहण से बचने के लिए लॉक के बाहर जांच करें, फिर यह सुनिश्चित करने के लिए लॉक के भीतर दोबारा जांचें कि आगे केवल एक coroutine बढ़े। यदि आप लॉक के पीछे इंतज़ार नहीं करना चाहते, तो उसके पहले से पकड़े होने पर जल्दी लौट आना asyncio में एक वैध रणनीति है—लेकिन अंदर की जांच बनी रहनी चाहिए। इस तरह आप डुप्लिकेट लोड हटाते हैं और रिफ्रेश का रास्ता पूर्वानुमेय रखते हैं।

यह लेख StackOverflow पर एक प्रश्न (लेखक: Artem Ilin) और jei के उत्तर पर आधारित है।