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


123

निम्नलिखित कोड स्निपेट दो थ्रेड को निष्पादित करता है, एक हर सेकंड लॉगिंग करने वाला एक साधारण टाइमर है, दूसरा एक अनंत लूप है जो एक शेष ऑपरेशन को निष्पादित करता है:

public class TestBlockingThread {
    private static final Logger LOGGER = LoggerFactory.getLogger(TestBlockingThread.class);

    public static final void main(String[] args) throws InterruptedException {
        Runnable task = () -> {
            int i = 0;
            while (true) {
                i++;
                if (i != 0) {
                    boolean b = 1 % i == 0;
                }
            }
        };

        new Thread(new LogTimer()).start();
        Thread.sleep(2000);
        new Thread(task).start();
    }

    public static class LogTimer implements Runnable {
        @Override
        public void run() {
            while (true) {
                long start = System.currentTimeMillis();
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    // do nothing
                }
                LOGGER.info("timeElapsed={}", System.currentTimeMillis() - start);
            }
        }
    }
}

यह निम्न परिणाम देता है:

[Thread-0] INFO  c.m.c.concurrent.TestBlockingThread - timeElapsed=1004
[Thread-0] INFO  c.m.c.concurrent.TestBlockingThread - timeElapsed=1003
[Thread-0] INFO  c.m.c.concurrent.TestBlockingThread - timeElapsed=13331
[Thread-0] INFO  c.m.c.concurrent.TestBlockingThread - timeElapsed=1006
[Thread-0] INFO  c.m.c.concurrent.TestBlockingThread - timeElapsed=1003
[Thread-0] INFO  c.m.c.concurrent.TestBlockingThread - timeElapsed=1004
[Thread-0] INFO  c.m.c.concurrent.TestBlockingThread - timeElapsed=1004

मुझे समझ नहीं आता कि अनंत कार्य 13.3 सेकंड के लिए अन्य सभी थ्रेड्स को ब्लॉक क्यों करता है। मैंने थ्रेड प्राथमिकताएं और अन्य सेटिंग्स बदलने की कोशिश की, कुछ भी काम नहीं किया।

यदि आपके पास इसे ठीक करने के लिए कोई सुझाव हैं (जिसमें ओएस संदर्भ स्विचिंग सेटिंग्स शामिल है) तो कृपया मुझे बताएं।


8
@ मैर्थिन जीसी नहीं। यह JIT है। साथ चल रहा है -XX:+PrintCompilationमैं समय पर निम्नलिखित मिल विस्तारित देरी समाप्त होता है: TestBlockingThread :: लैम्ब्डा $ 0 @ 2 (24 बाइट्स) संकलन छोड़ा गया: तुच्छ अनंत लूप (विभिन्न स्तर पर फिर से प्रयास करें)
एंड्रियास

4
यह मेरे सिस्टम पर एकमात्र परिवर्तन के साथ पुन: उत्पन्न करता है जिसे मैंने System.out.println के साथ लॉग कॉल को बदल दिया है। एक शेड्यूलर समस्या की तरह लगता है क्योंकि यदि आप रननेबल के अंदर 1ms की नींद का परिचय देते हैं, तो (सही) लूप दूसरे धागे में रुक जाता है।
JJF

3
ऐसा नहीं है कि मैं इसे सुझाता हूं, लेकिन यदि आप JIT का उपयोग करके अक्षम करते हैं -Djava.compiler=NONE, तो ऐसा नहीं होगा।
एंड्रियास

3
आप एकल विधि के लिए JIT को निष्क्रिय कर सकते हैं। देखें एक विशिष्ट विधि / वर्ग के लिए अक्षम जावा JIT?
एंड्रियास

3
इस कोड में कोई पूर्णांक विभाजन नहीं है। कृपया अपना शीर्षक और प्रश्न ठीक करें।
Lorne

जवाबों:


94

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

लेकिन मैंने गहराई से जाने का फैसला किया और पाया कि सेफपॉइंट शायद ही कभी पहुँचा हो। मुझे यह थोड़ा उलझा हुआ लगा कि whileइस मामले में लूप का बैक जंप "सुरक्षित" क्यों नहीं है।

इसलिए मैं -XX:+PrintAssemblyइसकी सभी महिमा में मदद करने के लिए बुलाता हूं

-XX:+UnlockDiagnosticVMOptions \
-XX:+TraceClassLoading \
-XX:+DebugNonSafepoints \
-XX:+PrintCompilation \
-XX:+PrintGCDetails \
-XX:+PrintStubCode \
-XX:+PrintAssembly \
-XX:PrintAssemblyOptions=-Mintel

कुछ जांच के बाद मैंने पाया कि लैम्ब्डा C2कंपाइलर के तीसरे पुनर्संयोजन के बाद लूप के अंदर सेफपॉइंट पोल को पूरी तरह से फेंक दिया।

अपडेट करें

प्रोफाइलिंग स्टेज के दौरान वैरिएबल iको कभी भी 0. के बराबर नहीं देखा गया, इसीलिए C2इस ब्रांच को सट्टेबाजी से दूर से ऑप्टिमाइज़ किया गया, ताकि लूप कुछ इस तरह बदल जाए

for (int i = OSR_value; i != 0; i++) {
    if (1 % i == 0) {
        uncommon_trap();
    }
}
uncommon_trap();

ध्यान दें कि मूल रूप से अनंत लूप को काउंटर के साथ नियमित परिमित लूप में बदल दिया गया था! परिमित गिनती वाले छोरों में सेफपॉइंट पोल को खत्म करने के लिए जेआईटी ऑप्टिमाइज़ेशन के कारण, इस लूप में कोई भी सेफ पोल नहीं था।

कुछ समय बाद, iवापस लपेटा गया 0और असामान्य जाल लिया गया। विधि की व्याख्या की गई और दुभाषिया में निष्पादन जारी रखा गया। एक नए ज्ञान के साथ पुनर्संयोजन के दौरान C2अनंत लूप को पहचान लिया और संकलन छोड़ दिया। बाकी विधि उचित व्याख्या के साथ दुभाषिया में आगे बढ़ी।

निट्सन वकार्ट द्वारा सेफफ़ी पॉइंट्स और इस विशेष अंक को कवर करते हुए एक महान ब्लॉग पोस्ट "Safepoints: Meaning, साइड इफेक्ट्स और ओवरहेड्स" पढ़ा जाना चाहिए ।

बहुत लंबी गिनती वाले छोरों में सफेदी उन्मूलन एक समस्या के रूप में जाना जाता है। बग JDK-5014723( व्लादिमीर इवानोव के लिए धन्यवाद ) इस समस्या को संबोधित करता है।

बग अंतत: ठीक होने तक वर्कअराउंड उपलब्ध है।

  1. आप उपयोग करने की कोशिश कर सकते हैं -XX:+UseCountedLoopSafepoints(यह समग्र प्रदर्शन दंड का कारण होगा और जेवीएम दुर्घटना का कारण बन सकता हैJDK-8161147 )। इसे इस्तेमाल करने के बाद C2कंपाइलर बैक जंप पर सेफ पॉइंट्स जारी रखता है और ओरिजनल पॉज पूरी तरह से गायब हो जाता है।
  2. आप स्पष्ट रूप से समस्याग्रस्त विधि के संकलन को उपयोग करके स्पष्ट रूप से अक्षम कर सकते हैं
    -XX:CompileCommand='exclude,binary/class/Name,methodName'

  3. या आप मैन्युअल रूप से सेफपॉइंट जोड़कर अपने कोड को फिर से लिख सकते हैं। उदाहरण के लिए Thread.yield(), चक्र के अंत में कॉल करें या यहां तक ​​कि बदलने के int iलिए long i(धन्यवाद, निट्सन वकार्ट ) भी ठहराव को ठीक करेगा।


7
यह कैसे तय करने के सवाल का सही जवाब है ।
एंड्रियास

चेतावनी: -XX:+UseCountedLoopSafepointsउत्पादन में उपयोग न करें , क्योंकि इससे जेवीएम दुर्घटनाग्रस्त हो सकता है । अब तक का सबसे अच्छा समाधान यह है कि लंबे लूप को छोटे लोगों में विभाजित किया जाए।
अपंगिन

@ पपंगिन आह। समझ गया! धन्यवाद :) इसीलिए c2सफीदों को हटा दिया! लेकिन एक और बात जो मुझे नहीं मिली वह वह है जो आगे चल रही है। जहां तक ​​मैं देख सकता हूं कि लूप अन्रॉलिंग (?) के बाद कोई सेफपॉइंट नहीं बचा है और ऐसा लग रहा है कि स्टोव करने का कोई तरीका नहीं है। इसलिए किसी तरह का टाइमआउट होता है और डी-ऑप्टिमाइज़ेशन होता है?
22

2
मेरी पिछली टिप्पणी सटीक नहीं थी। अब यह पूरी तरह से स्पष्ट है कि क्या होता है। प्रोफाइलिंग स्टेज iपर कभी 0 नहीं होता है, इसलिए लूप को सट्टा रूप से कुछ इस तरह बदल दिया जाता है जैसे कि for (int i = osr_value; i != 0; i++) { if (1 % i == 0) uncommon_trap(); } uncommon_trap();एक नियमित परिमित गिना हुआ लूप। एक बार i0 से वापस लपेटने के बाद , असामान्य जाल लिया जाता है, विधि को अपनाया जाता है और दुभाषिया में आगे बढ़ा जाता है। नए ज्ञान के साथ पुनर्संयोजन के दौरान जेआईटी अनंत लूप को पहचानता है और संकलन छोड़ देता है। बाकी की विधि उचित सफ़ाई के साथ दुभाषिया में निष्पादित की जाती है।
अपंगिन

1
आप सिर्फ एक इंट के बजाय ia को लंबा कर सकते हैं, जो लूप को "बेशुमार" बना देगा और समस्या को हल कर देगा।
निट्टन वाकार्ट

64

संक्षेप में, आपके पास कोई लूप नहीं होता i == 0है जब तक कि वह पहुँच न जाए। जब इस विधि को संकलित किया जाता है और कोड को प्रतिस्थापित किया जाता है तो इसे सभी थ्रेड्स को एक सुरक्षित बिंदु पर लाने की आवश्यकता होती है, लेकिन इसमें बहुत लंबा समय लगता है, न केवल कोड को चलाने वाले थ्रेड को लॉक करना बल्कि सभी थ्रेड्स को JVM में बदलना।

मैंने निम्न कमांड लाइन विकल्प जोड़े।

-XX:+PrintGCApplicationStoppedTime -XX:+PrintGCApplicationConcurrentTime -XX:+PrintCompilation

मैंने फ़्लोटिंग पॉइंट का उपयोग करने के लिए कोड को भी संशोधित किया जो कि अधिक समय लगता है।

boolean b = 1.0 / i == 0;

और जो मैं आउटपुट में देख रहा हूं वह है

timeElapsed=100
Application time: 0.9560686 seconds
  41423  280 %     4       TestBlockingThread::lambda$main$0 @ -2 (27 bytes)   made not entrant
Total time for which application threads were stopped: 40.3971116 seconds, Stopping threads took: 40.3967755 seconds
Application time: 0.0000219 seconds
Total time for which application threads were stopped: 0.0005840 seconds, Stopping threads took: 0.0000383 seconds
  41424  281 %     3       TestBlockingThread::lambda$main$0 @ 2 (27 bytes)
timeElapsed=40473
  41425  282 %     4       TestBlockingThread::lambda$main$0 @ 2 (27 bytes)
  41426  281 %     3       TestBlockingThread::lambda$main$0 @ -2 (27 bytes)   made not entrant
timeElapsed=100

नोट: कोड को प्रतिस्थापित करने के लिए, थ्रेड्स को एक सुरक्षित बिंदु पर रोकना होगा। हालाँकि यह यहाँ प्रतीत होता है कि इस तरह के एक सुरक्षित बिंदु पर शायद ही कभी पहुँचा जाता है (संभवतः केवल जब i == 0कार्य को बदलना

Runnable task = () -> {
    for (int i = 1; i != 0 ; i++) {
        boolean b = 1.0 / i == 0;
    }
};

मैं इसी तरह की देरी देखता हूं।

timeElapsed=100
Application time: 0.9587419 seconds
  39044  280 %     4       TestBlockingThread::lambda$main$0 @ -2 (28 bytes)   made not entrant
Total time for which application threads were stopped: 38.0227039 seconds, Stopping threads took: 38.0225761 seconds
Application time: 0.0000087 seconds
Total time for which application threads were stopped: 0.0003102 seconds, Stopping threads took: 0.0000105 seconds
timeElapsed=38100
timeElapsed=100

लूप में कोड जोड़ने से ध्यान से आपको एक लंबा विलंब मिलता है।

for (int i = 1; i != 0 ; i++) {
    boolean b = 1.0 / i / i == 0;
}

हो जाता है

 Total time for which application threads were stopped: 59.6034546 seconds, Stopping threads took: 59.6030773 seconds

हालाँकि, मूल विधि का उपयोग करने के लिए कोड को बदल दें जिसमें हमेशा एक सुरक्षित बिंदु होता है (यदि यह आंतरिक नहीं है)

for (int i = 1; i != 0 ; i++) {
    boolean b = Math.cos(1.0 / i) == 0;
}

प्रिंट

Total time for which application threads were stopped: 0.0001444 seconds, Stopping threads took: 0.0000615 seconds

नोट: if (Thread.currentThread().isInterrupted()) { ... }लूप में जोड़ना एक सुरक्षित बिंदु जोड़ता है।

नोट: यह 16 कोर मशीन पर हुआ है इसलिए सीपीयू संसाधनों की कोई कमी नहीं है।


1
तो यह एक JVM बग है, है ना? जहां "बग" का मतलब कार्यान्वयन के मुद्दे की गंभीर गुणवत्ता है और कल्पना का उल्लंघन नहीं है।
usr

1
@vinkinkov कई मिनटों के लिए दुनिया को रोकने में सक्षम होने के कारण यह सुरक्षित लगता है जैसे कि इसे बग के रूप में माना जाना चाहिए। रनटाइम लंबे वेट से बचने के लिए सेफ पॉइंट पेश करने के लिए जिम्मेदार है।
वू

1
@Voo लेकिन दूसरी तरफ हर बैक जंप में सेफपॉइंट्स रखने से बहुत सारे cpu साइकल खर्च हो सकते हैं और पूरे एप्लिकेशन के ध्यान देने योग्य प्रदर्शन में गिरावट आ सकती है। लेकिन मैं तुमसे सहमत हूँ। उस विशेष मामले में यह सुरक्षित रखने के लिए कानूनी लग रहा है
vsminkov

9
@ अच्छी तरह से ... मैं हमेशा इस तस्वीर को याद करता हूं जब यह प्रदर्शन अनुकूलन की बात आती है: D
vsminkov

1
.NET यहां सेफ पॉइंट्स डालता है (लेकिन .NET में स्लो जनरेट कोड है)। एक संभावित समाधान लूप को चोक करना है। दो छोरों में विभाजित करें, 1024 तत्वों के बैचों के लिए आंतरिक जांच न करें और बाहरी लूप ड्राइव बैचों और सेफ पॉइंट्स को ड्राइव करते हैं। 1024x द्वारा ओवरहेड को व्यावहारिक रूप से कम कर देता है।
usr

26

का जवाब क्यों मिला । उन्हें सेफपॉइंट कहा जाता है, और सबसे अच्छा स्टॉप-द-वर्ल्ड के रूप में जाना जाता है जो जीसी के कारण होता है।

इस लेख को देखें: JVM में स्टॉप-द-वर्ल्ड रुका हुआ है

विभिन्न घटनाओं के कारण JVM सभी एप्लिकेशन थ्रेड को रोक सकता है। ऐसे ठहराव को स्टॉप-द-वर्ल्ड (एसटीडब्ल्यू) ठहराव कहा जाता है। ट्रिगर किए जाने वाले एसटीडब्ल्यू ठहराव का सबसे आम कारण कचरा संग्रह (जीथब्यू में उदाहरण) है, लेकिन विभिन्न जेआईटी कार्रवाइयां (उदाहरण), पक्षपाती ताला निरसन (उदाहरण), कुछ जेवीएमटीआई संचालन, और कई और भी आवेदन को रोकने की आवश्यकता है।

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

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

-XX:+PrintGCApplicationStoppedTime -XX:+PrintGCApplicationConcurrentTime

यदि आप जीसी के बारे में स्पष्ट रूप से नामकरण के बारे में सोच रहे हैं, तो घबराएं नहीं - इन विकल्पों को चालू करने से सभी सुरक्षित बिंदुओं पर लॉग होता है, न कि केवल कचरा संग्रह पोज में। यदि आप ऊपर दिए गए झंडे के साथ निम्नलिखित उदाहरण (गीथूब में स्रोत) चलाते हैं।

शर्तों के हॉटस्पॉट शब्दावली को पढ़ना , यह इसे परिभाषित करता है:

safepoint

कार्यक्रम निष्पादन के दौरान एक बिंदु जिस पर सभी जीसी जड़ें ज्ञात हैं और सभी ढेर वस्तु सामग्री सुसंगत हैं। वैश्विक दृष्टिकोण से, जीसी चलने से पहले सभी थ्रेड्स को एक सुरक्षित स्थान पर ब्लॉक करना चाहिए। (एक विशेष मामले के रूप में, जेएनआई कोड से चलने वाले थ्रेड्स जारी रह सकते हैं, क्योंकि वे केवल हैंडल का उपयोग करते हैं। एक सेफपॉइंट के दौरान उन्हें हैंडल की सामग्री लोड करने के बजाय ब्लॉक करना होगा।) एक स्थानीय दृष्टिकोण से, एक सेफ पॉइंट एक विशिष्ट बिंदु है। कोड के एक ब्लॉक में जहां निष्पादन धागा जीसी के लिए ब्लॉक हो सकता है। अधिकांश कॉल साइटें सेफ पॉइंट के रूप में योग्य हैं।ऐसे मजबूत आक्रमणकारी होते हैं जो हर सुरक्षित स्थान पर सही रहते हैं, जो गैर-सुरक्षित बिंदुओं पर अवहेलना हो सकता है। दोनों संकलित जावा कोड और C / C ++ कोड को सेफ पॉइंट्स के बीच ऑप्टिमाइज़ किया जाता है, लेकिन कम से कम सेफ पॉइंट्स के बीच। JIT कंपाइलर प्रत्येक सेफपॉइंट पर GC मैप का उत्सर्जन करता है। VM में C / C ++ कोड स्टाइलिस्ट मैक्रो-आधारित सम्मेलनों (जैसे, TRAPS) का उपयोग संभावित सेफ पॉइंट को चिह्नित करने के लिए करता है।

उपर्युक्त झंडे के साथ चल रहा है, मुझे यह आउटपुट मिलता है:

Application time: 0.9668750 seconds
Total time for which application threads were stopped: 0.0000747 seconds, Stopping threads took: 0.0000291 seconds
timeElapsed=1015
Application time: 1.0148568 seconds
Total time for which application threads were stopped: 0.0000556 seconds, Stopping threads took: 0.0000168 seconds
timeElapsed=1015
timeElapsed=1014
Application time: 2.0453971 seconds
Total time for which application threads were stopped: 10.7951187 seconds, Stopping threads took: 10.7950774 seconds
timeElapsed=11732
Application time: 1.0149263 seconds
Total time for which application threads were stopped: 0.0000644 seconds, Stopping threads took: 0.0000368 seconds
timeElapsed=1015

तीसरे STW घटना पर ध्यान दें:
कुल समय रुक गया: 10.7951187 सेकंड
स्टॉपिंग थ्रेड्स लिया गया: 10.7950774 सेकंड

JIT ने स्वयं लगभग कोई समय नहीं लिया, लेकिन एक बार जब JVM ने JIT संकलन करने का निर्णय लिया, तो उसने STW मोड में प्रवेश किया, हालाँकि जब से कोड को संकलित किया जाता है (अनंत लूप) में कॉल साइट नहीं होती है, कोई भी सेफपॉइंट कभी नहीं पहुँचा था।

एसटीडब्ल्यू तब समाप्त होता है जब जेआईटी अंततः प्रतीक्षा छोड़ देता है और निष्कर्ष निकालता है कि कोड एक अनंत लूप में है।


"सेफपॉइंट - प्रोग्राम निष्पादन के दौरान एक बिंदु जिस पर सभी जीसी जड़ें जानी जाती हैं और सभी ढेर ऑब्जेक्ट सामग्री सुसंगत हैं" - यह एक लूप में सच क्यों नहीं होगा जो केवल स्थानीय मूल्य-प्रकार के चर सेट / पढ़ता है?
ब्लूराजा - डैनी पफ्लुघोफ्ट

@ BlueRaja-DannyPflughoeft मैंने इस सवाल का जवाब अपने जवाब
vsminkov

5

कमेंट थ्रेड्स और अपने आप पर कुछ परीक्षण के बाद, मेरा मानना ​​है कि ठहराव JIT कंपाइलर के कारण होता है। जेआईटी कंपाइलर को इतना लंबा समय क्यों लग रहा है, यह डिबग करने की मेरी क्षमता से परे है।

हालाँकि, चूंकि आपने केवल इसे रोकने का तरीका पूछा था, इसलिए मेरे पास एक समाधान है:

अपने अनंत लूप को एक विधि में खींचें, जहां इसे JIT कंपाइलर से बाहर रखा जा सकता है

public class TestBlockingThread {
    private static final Logger LOGGER = Logger.getLogger(TestBlockingThread.class.getName());

    public static final void main(String[] args) throws InterruptedException     {
        Runnable task = () -> {
            infLoop();
        };
        new Thread(new LogTimer()).start();
        Thread.sleep(2000);
        new Thread(task).start();
    }

    private static void infLoop()
    {
        int i = 0;
        while (true) {
            i++;
            if (i != 0) {
                boolean b = 1 % i == 0;
            }
        }
    }

इस VM तर्क के साथ अपना कार्यक्रम चलाएं:

-XX: CompileCommand = बाहर करें, PACKAGE.TestBlockingThread :: infLoop (अपनी पैकेज जानकारी के साथ पैकेज बदलें)

आपको यह इंगित करने के लिए इस तरह का संदेश प्राप्त करना चाहिए कि विधि
JIT- संकलित की गई होगी: ### संकलन को छोड़कर: स्थैतिक अवरोधन ।estBlockingThread :: infLoop
आप देख सकते हैं कि मैंने कक्षा को अवरुद्ध करने वाले पैकेज में रखा है।


1
संकलक इतना लंबा नहीं ले जा रहा है, समस्या यह है कि कोड एक सुरक्षित बिंदु तक नहीं पहुंच रहा है क्योंकि लूप के अंदर कोई भी नहीं है सिवाय इसकेi == 0
पीटर लॉरे

@PeterLawrey लेकिन whileलूप में चक्र का अंत सुरक्षित क्यों नहीं है?
19-21 बजे vsminkov

@vsminkov ऐसा प्रतीत होता है कि एक सेफपॉइंट है, if (i != 0) { ... } else { safepoint(); }लेकिन यह बहुत दुर्लभ है। अर्थात। यदि आप लूप से बाहर निकलते / टूटते हैं तो आपको समान समय मिलता है।
पीटर लॉरी

@PeterLawrey थोड़ी जाँच पड़ताल के बाद मैंने पाया कि लूप के बैक जंप पर एक सुरक्षित जगह बनाना आम बात है। मैं बस उत्सुक हूं कि इस विशेष मामले में क्या अंतर है। शायद मैं अनुभवहीन हूं, लेकिन मुझे कोई कारण नहीं है कि बैक जंप "सुरक्षित" नहीं है
vsminkov

@vsminkov मुझे संदेह है कि जेआईटी देखती है कि एक सेफपॉइंट लूप में है इसलिए अंत में एक नहीं जोड़ता है।
पीटर लॉरी
हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.