मैं लॉक स्टेटमेंट के मुख्य भाग के भीतर 'प्रतीक्षा' संचालक का उपयोग क्यों नहीं कर सकता?


348

C # (.NET Async CTP) में प्रतीक्षित कीवर्ड को लॉक स्टेटमेंट के भीतर से अनुमति नहीं है।

से MSDN :

एक प्रतीक्षा अभिव्यक्ति को एक तुल्यकालिक फ़ंक्शन में, क्वेरी अभिव्यक्ति में, एक अपवाद हैंडलिंग स्टेटमेंट के कैच या अंत में ब्लॉक में, लॉक स्टेटमेंट के ब्लॉक में या असुरक्षित संदर्भ में उपयोग नहीं किया जा सकता है।

मुझे लगता है कि यह कंपाइलर टीम के लिए किसी कारण से लागू करना कठिन या असंभव है।

मैंने उपयोग कथन के साथ एक काम का प्रयास किया:

class Async
{
    public static async Task<IDisposable> Lock(object obj)
    {
        while (!Monitor.TryEnter(obj))
            await TaskEx.Yield();

        return new ExitDisposable(obj);
    }

    private class ExitDisposable : IDisposable
    {
        private readonly object obj;
        public ExitDisposable(object obj) { this.obj = obj; }
        public void Dispose() { Monitor.Exit(this.obj); }
    }
}

// example usage
using (await Async.Lock(padlock))
{
    await SomethingAsync();
}

हालाँकि यह अपेक्षा के अनुरूप काम नहीं करता है। ExitDisposable.Dispose के अंदर Monitor.Exit को कॉल करने से लगता है कि अन्य थ्रेड्स लॉक का प्रयास करने के कारण डेडलॉक को अनिश्चित काल के लिए रोकते हैं (ज्यादातर समय)। मुझे अपने काम की अविश्वसनीयता पर संदेह है और प्रतीक्षा बयानों को कारण बताने की अनुमति नहीं है।

क्या किसी को पता है कि प्रतीक्षारत को लॉक स्टेटमेंट की अनुमति क्यों नहीं है?


27
मुझे लगता है कि आपको इसकी वजह मिल गई, इसकी अनुमति क्यों नहीं है।
asawyer

3
क्या मैं इस लिंक का सुझाव दे सकता हूं: hanselman.com/blog/… और यह एक: blogs.msdn.com/b/pfxteam/archive/2012/02/12/1026696988.aspx
hans

मैं बस पकड़ना शुरू कर रहा हूं और async प्रोग्रामिंग के बारे में थोड़ा और जानना चाहता हूं। मेरे wpf अनुप्रयोगों में कई गतिरोधों के बाद, मुझे यह लेख async प्रोग्रामिंग प्रथाओं में एक महान सुरक्षित रक्षक के रूप में मिला। msdn.microsoft.com/en-us/magazine/…
सी। टिवाल्ट

लॉक को async एक्सेस को रोकने के लिए डिज़ाइन किया गया है जब async एक्सेस आपके कोड को तोड़ देगा, यदि आप लॉक के अंदर async का उपयोग कर रहे हैं तो अपने लॉक को अमान्य कर दिया है .. इसलिए यदि आपको अपने लॉक के अंदर कुछ प्रतीक्षा करने की आवश्यकता है तो आप लॉक का सही उपयोग नहीं कर रहे हैं
माइकटी

जवाबों:


366

मुझे लगता है कि यह कंपाइलर टीम के लिए किसी कारण से लागू करना कठिन या असंभव है।

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

ExitDisposable.Dispose के अंदर Monitor.Exit को कॉल करें। लगता है कि ज्यादातर थ्रेड लॉक प्राप्त करने के प्रयास के कारण डेडलॉक को अनिश्चित काल के लिए रोक दिया जाए (ज्यादातर समय)। मुझे अपने काम की अविश्वसनीयता पर संदेह है और प्रतीक्षा बयानों को कारण बताने की अनुमति नहीं है।

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

मुझे यकीन है कि आप देख सकते हैं कि क्यों: मनमाने ढंग से कोड उस समय के बीच चलता है जब कॉल करने वाले के लिए प्रतीक्षित रिटर्न नियंत्रण और विधि फिर से शुरू होती है । वह मनमाना कोड ताले को बाहर निकालने वाला हो सकता है जो लॉक ऑर्डरिंग इनवर्स का उत्पादन करता है, और इसलिए गतिरोध।

इससे भी बदतर, कोड एक और थ्रेड पर फिर से शुरू हो सकता है (उन्नत परिदृश्यों में; आमतौर पर आप उस थ्रेड पर फिर से उठाते हैं जो इंतजार कर रहा था, लेकिन जरूरी नहीं) जिस मामले में अनलॉक थ्रेड की तुलना में एक अलग थ्रेड पर लॉक अनलॉक कर रहा होगा। ताला बंद करो। क्या यह एक बेहतर तरकीब है? नहीं।

मैंने ध्यान दिया कि यह भी एक "सबसे खराब अभ्यास" yield returnहै lock, एक ही कारण के लिए अंदर । ऐसा करना कानूनी है, लेकिन काश हमने इसे गैरकानूनी बना दिया होता। हम "प्रतीक्षा" के लिए एक ही गलती नहीं करने जा रहे हैं।


189
आप एक परिदृश्य को कैसे संभालते हैं जहां आपको कैश प्रविष्टि को वापस करने की आवश्यकता होती है, और यदि प्रविष्टि मौजूद नहीं है तो आपको सामग्री को अतुल्यकालिक रूप से गणना करने की आवश्यकता है तो प्रविष्टि जोड़ें + वापस करें, यह सुनिश्चित करें कि इस बीच कोई और आपको कॉल नहीं करता है?
सॉफ्टलियन

9
मुझे पता है कि मुझे यहां पार्टी में देरी हो रही है, हालांकि मुझे यह देखकर आश्चर्य हुआ कि आपने गतिरोध को प्राथमिक कारण के रूप में रखा है कि यह एक बुरा विचार क्यों है। मैं अपनी सोच में इस निष्कर्ष पर पहुंचा था कि लॉक / मॉनीटर का फिर से प्रवेश प्रकृति की समस्या का एक बड़ा हिस्सा होगा। यही है, आप थ्रेड पूल को दो कार्यों को पंक्तिबद्ध करते हैं जो लॉक (), कि एक तुल्यकालिक दुनिया में अलग-अलग थ्रेड्स पर निष्पादित होगा। लेकिन अब इंतजार के साथ (यदि मेरा मतलब है तो अनुमति है) आपके पास लॉक ब्लॉक के भीतर निष्पादित होने वाले दो कार्य हो सकते हैं क्योंकि धागे का पुन: उपयोग किया गया था। प्रफुल्लितता छा जाती है। या मैंने कुछ गलत समझा?
गैरेथ विल्सन

4
@GarethWilson: मैंने गतिरोधों के बारे में बात की क्योंकि पूछे गए प्रश्न गतिरोधों के बारे में थे । आप सही हैं कि विचित्र फिर से प्रवेश के मुद्दे संभव हैं और संभावना प्रतीत होती है।
एरिक लिपर्ट

11
@ एरिक लिपर्ट। यह देखते हुए कि SemaphoreSlim.WaitAsyncइस उत्तर को पोस्ट करने के बाद वर्ग .NET फ्रेमवर्क में अच्छी तरह से जोड़ा गया है, मुझे लगता है कि हम सुरक्षित रूप से मान सकते हैं कि यह अब संभव है। इसके बावजूद, इस तरह के निर्माण को लागू करने की कठिनाई के बारे में आपकी टिप्पणियां अभी भी पूरी तरह से वैध हैं।
कॉन्टैंगो

7
"मनमाना कोड उस समय के बीच चलता है जब कॉल करने वाले के लिए वेट रिटर्न कंट्रोल हो जाता है और विधि फिर से शुरू हो जाती है" - निश्चित रूप से यह किसी भी कोड का सच है, यहां तक ​​कि एस्किंट / वेट की अनुपस्थिति में, एक बहुआयामी संदर्भ में: अन्य थ्रेड्स किसी भी समय मनमाने कोड को निष्पादित कर सकते हैं समय, और मनमाने ढंग से कोड के रूप में आप कहते हैं कि "ताले को बाहर निकालने वाला हो सकता है जो लॉक ऑर्डरिंग इनवर्स का उत्पादन करता है, और इसलिए गतिरोध।" तो यह async / प्रतीक्षा के साथ विशेष महत्व का क्यों है? मैं समझता हूं कि दूसरा बिंदु फिर से "कोड एक और धागे पर फिर से शुरू हो सकता है" async / प्रतीक्षा के लिए विशेष महत्व का है।
बैकर

291

प्रयोग SemaphoreSlim.WaitAsyncविधि।

 await mySemaphoreSlim.WaitAsync();
 try {
     await Stuff();
 } finally {
     mySemaphoreSlim.Release();
 }

10
जैसा कि इस पद्धति को हाल ही में .NET फ्रेमवर्क में पेश किया गया था, मुझे लगता है कि हम यह मान सकते हैं कि async / प्रतीक्षा दुनिया में लॉक करने की अवधारणा अब अच्छी तरह से साबित हो गई है।
कंटैंगो

5
कुछ और जानकारी के लिए, इस लेख में "सेमफोरस्लीम" पाठ की खोज करें: Async /
Await

1
@JamesKo यदि उन सभी कार्यों के परिणाम की प्रतीक्षा कर Stuffरहा हूँ तो मुझे इसके आस-पास कोई रास्ता दिखाई नहीं देता ...
Ohad Schneider

7
mySemaphoreSlim = new SemaphoreSlim(1, 1)जैसे काम करने के लिए इसे आरंभीकृत नहीं किया जाना चाहिए lock(...)?
सर्गेई

3
इस उत्तर के विस्तारित संस्करण को जोड़ा गया: stackoverflow.com/a/50139704/1844247
सर्गेई

67

मूल रूप से यह गलत काम होगा।

दो तरीके यह कर रहे हैं हो सकता है लागू किया:

  • लॉक को पकड़कर रखें, केवल इसे ब्लॉक के अंत में जारी करें
    यह वास्तव में बुरा विचार है क्योंकि आप नहीं जानते कि अतुल्यकालिक ऑपरेशन कब तक होने वाला है। आपको केवल कम से कम समय के लिए ताले रखने चाहिए । यह संभावित रूप से असंभव भी है, क्योंकि एक धागा एक ताला का मालिक होता है, एक विधि का नहीं - और आप एक ही धागे (कार्य अनुसूचक के आधार पर) पर असिंक्रोनस विधि के बाकी हिस्सों को निष्पादित भी नहीं कर सकते हैं।

  • प्रतीक्षा में लॉक को रिलीज़ करें, और इसे फिर से प्राप्त करें जब प्रतीक्षा वापस आती है।
    यह कम से कम विस्मयकारी आईएमओ के सिद्धांत का उल्लंघन करता है, जहां अतुल्यकालिक विधि को समतुल्य तुल्यकालिक कोड की तरह निकटता से व्यवहार करना चाहिए - जब तक आप Monitor.Waitलॉक ब्लॉक में उपयोग नहीं करते हैं , आप उम्मीद करते हैं ब्लॉक की अवधि के लिए लॉक का मालिक है।

तो मूल रूप से यहां दो प्रतिस्पर्धी आवश्यकताएं हैं - आपको पहले यहां करने की कोशिश नहीं करनी चाहिए , और यदि आप दूसरा दृष्टिकोण लेना चाहते हैं, तो आप दो अलग-अलग लॉक ब्लॉक को प्रतीक्षित अभिव्यक्ति द्वारा अलग करके कोड को अधिक स्पष्ट कर सकते हैं:

// Now it's clear where the locks will be acquired and released
lock (foo)
{
}
var result = await something;
lock (foo)
{
}

इसलिए आपको लॉक ब्लॉक में प्रतीक्षा करने से रोकते हुए, भाषा आपको यह सोचने के लिए मजबूर कर रही है कि आप वास्तव में क्या करना चाहते हैं, और उस पसंद को उस कोड में स्पष्ट कर दें जो आप लिखते हैं।


5
यह देखते हुए कि SemaphoreSlim.WaitAsyncइस उत्तर को पोस्ट करने के बाद वर्ग को .NET फ्रेमवर्क में अच्छी तरह से जोड़ा गया है, मुझे लगता है कि हम सुरक्षित रूप से मान सकते हैं कि यह अब संभव है। इसके बावजूद, इस तरह के निर्माण को लागू करने की कठिनाई के बारे में आपकी टिप्पणियां अभी भी पूरी तरह से वैध हैं।
कंटैंगो

7
@Contango: वैसे यह काफी समान बात नहीं है। विशेष रूप से, सेमाफोर एक विशिष्ट धागे से बंधा नहीं है। यह लॉक करने के लिए समान लक्ष्यों को प्राप्त करता है, लेकिन महत्वपूर्ण अंतर हैं।
जॉन स्कीट

@JonSkeet मुझे पता है कि यह एक बहुत पुराना धागा है और सभी, लेकिन मुझे यकीन नहीं है कि दूसरे तरीके से उन तालों का उपयोग करके कुछ () कॉल को कैसे संरक्षित किया जाता है? जब एक धागा कुछ निष्पादित कर रहा है () किसी भी अन्य धागे को इसमें शामिल किया जा सकता है! क्या मुझसे कोई चूक हो रही है ?

@ जोसेफ: यह उस बिंदु पर संरक्षित नहीं है। यह दूसरा तरीका है, जो यह स्पष्ट करता है कि आप प्राप्त कर रहे हैं / जारी कर रहे हैं, फिर प्राप्त कर रहे हैं / जारी कर रहे हैं, संभवतः एक अलग धागे पर। क्योंकि एरिक के उत्तर के अनुसार पहला दृष्टिकोण एक बुरा विचार है।
जॉन स्कीट

41

यह इस उत्तर का विस्तार है ।

using System;
using System.Threading;
using System.Threading.Tasks;

public class SemaphoreLocker
{
    private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1, 1);

    public async Task LockAsync(Func<Task> worker)
    {
        await _semaphore.WaitAsync();
        try
        {
            await worker();
        }
        finally
        {
            _semaphore.Release();
        }
    }
}

उपयोग:

public class Test
{
    private static readonly SemaphoreLocker _locker = new SemaphoreLocker();

    public async Task DoTest()
    {
        await _locker.LockAsync(async () =>
        {
            // [asyn] calls can be used within this block 
            // to handle a resource by one thread. 
        });
    }
}

1
tryब्लॉक के बाहर सेमाफोर लॉक प्राप्त करना खतरनाक हो सकता है - अगर कोई अपवाद होता है WaitAsyncऔर tryसेमीफोर कभी जारी नहीं किया जाएगा (गतिरोध)। दूसरी तरफ, ब्लॉक WaitAsyncमें चलती कॉल tryएक और मुद्दा पेश करेगी, जब सेमीफोर को बिना लॉक किए अधिग्रहित किया जा सकता है। संबंधित समस्या देखें जहां इस समस्या को समझाया गया था: stackoverflow.com/a/61806749/7889645
एंड्रीचह

16

यह http://blogs.msdn.com/b/pfxteam/archive/2012/02/12/10266988.aspx , http://winrtstoragehelper.codeplex.com/ , विंडोज 8 ऐप स्टोर और .net 4.5 का उल्लेख करता है।

यहाँ इस पर मेरा कोण है:

Async / प्रतीक्षित भाषा फीचर कई चीजों को काफी आसान बना देता है, लेकिन यह एक ऐसे परिदृश्य का भी परिचय देता है, जो शायद ही कभी मुठभेड़ में था, इससे पहले कि यह async कॉल का उपयोग करना इतना आसान था: reentrance।

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

यहाँ एक वास्तविक परिदृश्य है जो मैंने विंडोज 8 ऐप स्टोर ऐप में देखा है: मेरे ऐप के दो फ्रेम हैं: फ्रेम में आने और छोड़ने से मैं फ़ाइल / स्टोरेज के लिए कुछ डेटा लोड / सुरक्षित करना चाहता हूं। OnNavigatedTo / से घटनाओं को बचाने और लोड करने के लिए उपयोग किया जाता है। बचत और लोडिंग कुछ async यूटिलिटी फंक्शन (जैसे http://winrtstoragehelper.codeplex.com/ ) द्वारा की जाती है। फ्रेम 1 से फ्रेम 2 या अन्य दिशा में नेविगेट करते समय, एसिंक्स लोड और सुरक्षित संचालन कहा जाता है और प्रतीक्षित होता है। घटना संचालकों async लौटने शून्य हो जाते हैं => वे इंतजार नहीं किया जा सकता है।

हालाँकि, उपयोगिता का पहला फ़ाइल ओपन ऑपरेशन (बताता है: एक सेव फंक्शन के अंदर) भी async है और इसलिए पहला प्रतीक्षित रिटर्न फ्रेमवर्क पर नियंत्रण रखता है, जिसे कुछ समय बाद दूसरे इवेंट हैंडलर के माध्यम से अन्य उपयोगिता (लोड) कहते हैं। लोड अब उसी फ़ाइल को खोलने का प्रयास करता है और यदि फ़ाइल सहेजने के लिए अब तक खुली है, तो ACCESSDENIED अपवाद के साथ विफल हो जाती है।

मेरे लिए एक न्यूनतम समाधान एक उपयोग और एक AsyncLock के माध्यम से फ़ाइल एक्सेस को सुरक्षित करना है।

private static readonly AsyncLock m_lock = new AsyncLock();
...

using (await m_lock.LockAsync())
{
    file = await folder.GetFileAsync(fileName);
    IRandomAccessStream readStream = await file.OpenAsync(FileAccessMode.Read);
    using (Stream inStream = Task.Run(() => readStream.AsStreamForRead()).Result)
    {
        return (T)serializer.Deserialize(inStream);
    }
}

कृपया ध्यान दें कि उसका लॉक मूल रूप से केवल एक लॉक के साथ उपयोगिता के लिए सभी फ़ाइल ऑपरेशन को लॉक करता है, जो अनावश्यक रूप से मजबूत है लेकिन मेरे परिदृश्य के लिए ठीक काम करता है।

यहाँ मेरा परीक्षण प्रोजेक्ट है: http://winrtstoragehelper.codeplex.com/ से मूल संस्करण के लिए कुछ परीक्षण कॉल के साथ एक विंडोज़ 8 ऐप स्टोर ऐप और मेरा संशोधित संस्करण जो स्टीफन टूब से AsyncLock का उपयोग करता है http: । com / b / pfxteam / संग्रह / 2012/02/12 / 10266988.aspx

क्या मैं भी इस लिंक का सुझाव दे सकता हूं: http://www.hanselman.com/blog/ComparingTwoTechniquesInNETAsynchronousCoordinationPrimatics.aspx


7

स्टीफन टाउब ने इस सवाल का हल लागू किया है, बिल्डिंग एस्सिंक कोऑर्डिनेशन प्रिमिटिव देखें, भाग 7: AsyncReaderWriterLock

स्टीफन तौब को उद्योग में बहुत माना जाता है, इसलिए वह जो कुछ भी लिखते हैं वह ठोस होने की संभावना है।

मैं उस कोड को पुन: पेश नहीं करूंगा जो उसने अपने ब्लॉग पर पोस्ट किया था, लेकिन मैं आपको दिखाऊंगा कि इसका उपयोग कैसे करना है:

/// <summary>
///     Demo class for reader/writer lock that supports async/await.
///     For source, see Stephen Taub's brilliant article, "Building Async Coordination
///     Primitives, Part 7: AsyncReaderWriterLock".
/// </summary>
public class AsyncReaderWriterLockDemo
{
    private readonly IAsyncReaderWriterLock _lock = new AsyncReaderWriterLock(); 

    public async void DemoCode()
    {           
        using(var releaser = await _lock.ReaderLockAsync()) 
        { 
            // Insert reads here.
            // Multiple readers can access the lock simultaneously.
        }

        using (var releaser = await _lock.WriterLockAsync())
        {
            // Insert writes here.
            // If a writer is in progress, then readers are blocked.
        }
    }
}

यदि आप एक ऐसी विधि चाहते हैं जो .NET फ्रेमवर्क में बेक किया गया है, तो SemaphoreSlim.WaitAsyncइसके बजाय उपयोग करें । आपको एक रीडर / राइटर लॉक नहीं मिलेगा, लेकिन आपको कोशिश और परीक्षण कार्यान्वयन मिल जाएगा।


मैं यह जानने के लिए उत्सुक हूं कि क्या इस कोड का उपयोग करने के लिए कोई चेतावनी है। अगर कोई भी इस कोड के साथ किसी भी मुद्दे को प्रदर्शित कर सकता है, तो मैं जानना चाहता हूं। हालाँकि, यह सच है कि async / प्रतीक्षा लॉकिंग की अवधारणा निश्चित रूप से अच्छी तरह से सिद्ध है, जैसा SemaphoreSlim.WaitAsyncकि .NET फ्रेमवर्क में है। यह सब कोड एक पाठक / लेखक ताला अवधारणा जोड़ रहा है।
कॉन्टैंगो

3

हम्म, बदसूरत लग रहा है, काम करने लगता है।

static class Async
{
    public static Task<IDisposable> Lock(object obj)
    {
        return TaskEx.Run(() =>
            {
                var resetEvent = ResetEventFor(obj);

                resetEvent.WaitOne();
                resetEvent.Reset();

                return new ExitDisposable(obj) as IDisposable;
            });
    }

    private static readonly IDictionary<object, WeakReference> ResetEventMap =
        new Dictionary<object, WeakReference>();

    private static ManualResetEvent ResetEventFor(object @lock)
    {
        if (!ResetEventMap.ContainsKey(@lock) ||
            !ResetEventMap[@lock].IsAlive)
        {
            ResetEventMap[@lock] =
                new WeakReference(new ManualResetEvent(true));
        }

        return ResetEventMap[@lock].Target as ManualResetEvent;
    }

    private static void CleanUp()
    {
        ResetEventMap.Where(kv => !kv.Value.IsAlive)
                     .ToList()
                     .ForEach(kv => ResetEventMap.Remove(kv));
    }

    private class ExitDisposable : IDisposable
    {
        private readonly object _lock;

        public ExitDisposable(object @lock)
        {
            _lock = @lock;
        }

        public void Dispose()
        {
            ResetEventFor(_lock).Set();
        }

        ~ExitDisposable()
        {
            CleanUp();
        }
    }
}

0

मैंने एक मॉनिटर (नीचे दिए गए कोड) का उपयोग करने की कोशिश की, जो काम करने के लिए प्रकट होता है, लेकिन इसमें एक गॉक्टा होता है ... जब आपके पास कई थ्रेड्स होंगे तो यह ... System.Threading.SynchronizationLockException ऑब्जेक्ट सिंक्रोनाइज़ेशन विधि कोड के एक अनियंत्रित ब्लॉक से बुलाया गया था।

using System;
using System.Threading;
using System.Threading.Tasks;

namespace MyNamespace
{
    public class ThreadsafeFooModifier : 
    {
        private readonly object _lockObject;

        public async Task<FooResponse> ModifyFooAsync()
        {
            FooResponse result;
            Monitor.Enter(_lockObject);
            try
            {
                result = await SomeFunctionToModifyFooAsync();
            }
            finally
            {
                Monitor.Exit(_lockObject);
            }
            return result;
        }
    }
}

इससे पहले मैं बस यही कर रहा था, लेकिन यह एक ASP.NET कंट्रोलर में था इसलिए यह एक गतिरोध के रूप में सामने आया।

public async Task<FooResponse> ModifyFooAsync() { lock(lockObject) { return SomeFunctionToModifyFooAsync.Result; } }

हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.