Java ThreadPoolExecutor: कोर पूल आकार को गतिशील रूप से अपडेट करना आने वाले कार्यों को रुक-रुक कर अस्वीकार करता है


13

मैं एक ऐसे मुद्दे पर चल रहा हूं, जहां अगर मैं ThreadPoolExecutorपूल बनने के बाद एक अलग संख्या में एक कोर पूल आकार का आकार बदलने का प्रयास करता हूं , तो रुक-रुक कर, कुछ कार्यों को अस्वीकार कर दिया जाता है RejectedExecutionException, हालांकि मैं कभी भी queueSize + maxPoolSizeकार्यों की संख्या से अधिक जमा नहीं करता ।

मैं जिस समस्या को हल करने की कोशिश कर रहा हूं, वह यह है कि ThreadPoolExecutorथ्रेड पूल की कतार में बैठे लंबित निष्पादन के आधार पर इसके मूल थ्रेड्स का विस्तार करना। मुझे इसकी आवश्यकता है क्योंकि डिफ़ॉल्ट रूप से ThreadPoolExecutorएक नया Threadतभी बनेगा जब कतार भरी होगी।

यहां एक छोटा स्व-निहित शुद्ध जावा 8 कार्यक्रम है जो समस्या को प्रदर्शित करता है।

import static java.lang.Math.max;
import static java.lang.Math.min;

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class ThreadPoolResizeTest {

    public static void main(String[] args) throws Exception {
        // increase the number of iterations if unable to reproduce
        // for me 100 iterations have been enough
        int numberOfExecutions = 100;

        for (int i = 1; i <= numberOfExecutions; i++) {
            executeOnce();
        }
    }

    private static void executeOnce() throws Exception {
        int minThreads = 1;
        int maxThreads = 5;
        int queueCapacity = 10;

        ThreadPoolExecutor pool = new ThreadPoolExecutor(
                minThreads, maxThreads,
                0, TimeUnit.SECONDS,
                new LinkedBlockingQueue<Runnable>(queueCapacity),
                new ThreadPoolExecutor.AbortPolicy()
        );

        ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
        scheduler.scheduleAtFixedRate(() -> resizeThreadPool(pool, minThreads, maxThreads),
                0, 10, TimeUnit.MILLISECONDS);
        CompletableFuture<Void> taskBlocker = new CompletableFuture<>();

        try {
            int totalTasksToSubmit = queueCapacity + maxThreads;

            for (int i = 1; i <= totalTasksToSubmit; i++) {
                // following line sometimes throws a RejectedExecutionException
                pool.submit(() -> {
                    // block the thread and prevent it from completing the task
                    taskBlocker.join();
                });
                // Thread.sleep(10); //enabling even a small sleep makes the problem go away
            }
        } finally {
            taskBlocker.complete(null);
            scheduler.shutdown();
            pool.shutdown();
        }
    }

    /**
     * Resize the thread pool if the number of pending tasks are non-zero.
     */
    private static void resizeThreadPool(ThreadPoolExecutor pool, int minThreads, int maxThreads) {
        int pendingExecutions = pool.getQueue().size();
        int approximateRunningExecutions = pool.getActiveCount();

        /*
         * New core thread count should be the sum of pending and currently executing tasks
         * with an upper bound of maxThreads and a lower bound of minThreads.
         */
        int newThreadCount = min(maxThreads, max(minThreads, pendingExecutions + approximateRunningExecutions));

        pool.setCorePoolSize(newThreadCount);
        pool.prestartAllCoreThreads();
    }
}

अगर मैं कभी भी अधिक क्यूपेसिटी + मैक्सट्रेड्स जमा नहीं करता हूं, तो पूल को कभी भी रिजेक्टेड एक्सेप्शन एक्ससेप्शन फेंकना चाहिए। मैं थ्रेडपूल एक्ज़ीक्यूटर की परिभाषा से अधिकतम थ्रेड्स को कभी नहीं बदल रहा हूं, इसे या तो थ्रेड में या क्यू में कार्य को समायोजित करना चाहिए।

बेशक, अगर मैं कभी पूल का आकार नहीं बदलता हूं, तो थ्रेड पूल कभी भी किसी भी प्रस्तुतियाँ को अस्वीकार नहीं करता है। सबमिशन में किसी भी तरह की देरी को शामिल करने के बाद से डिबग करना भी मुश्किल हो जाता है।

RejectExecutionException को ठीक करने के बारे में कोई संकेत?


ExecutorServiceकिसी मौजूदा को लपेटकर अपना स्वयं का कार्यान्वयन क्यों नहीं प्रदान करता है, जो उन कार्यों को फिर से शुरू करता है जो कि आकार बदलने के कारण प्रस्तुत करने में विफल रहे?
दानीऊ

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

ठीक है मुझे लगता है कि आपके सवाल को गलत समझा। यह क्या है? क्या आप जानना चाहते हैं कि व्यवहार क्यों होता है या आपके आस-पास कैसे होता है जो आपके लिए समस्याएँ पैदा करता है?
दानी

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

विस्तार ThreadPoolExecutorशायद बहुत बुरा विचार है, और क्या आपको इस मामले में मौजूदा कोड को बदलने की आवश्यकता नहीं है? यह सबसे अच्छा होगा कि आप कुछ उदाहरण प्रदान करें कि आपका वास्तविक कोड निष्पादक तक कैसे पहुँचता है। मुझे आश्चर्य होगा अगर यह कई विधियों का उपयोग करता है ThreadPoolExecutor(यानी नहीं में ExecutorService)।
दानीऊ

जवाबों:


5

यहाँ एक परिदृश्य है कि ऐसा क्यों हो रहा है:

अपने उदाहरण में मैं इसे छोटा करने के लिए मिन्टह्रेड = 0, मैक्सट्रेड्स = 2 और क्यूपैपेसिटी = 2 का उपयोग करता हूं। पहला आदेश सबमिट किया जाता है, यह विधि निष्पादित में किया जाता है:

public void execute(Runnable command) {
    if (command == null)
        throw new NullPointerException();
    /*
     * Proceed in 3 steps:
     *
     * 1. If fewer than corePoolSize threads are running, try to
     * start a new thread with the given command as its first
     * task.  The call to addWorker atomically checks runState and
     * workerCount, and so prevents false alarms that would add
     * threads when it shouldn't, by returning false.
     *
     * 2. If a task can be successfully queued, then we still need
     * to double-check whether we should have added a thread
     * (because existing ones died since last checking) or that
     * the pool shut down since entry into this method. So we
     * recheck state and if necessary roll back the enqueuing if
     * stopped, or start a new thread if there are none.
     *
     * 3. If we cannot queue task, then we try to add a new
     * thread.  If it fails, we know we are shut down or saturated
     * and so reject the task.
     */
    int c = ctl.get();
    if (workerCountOf(c) < corePoolSize) {
        if (addWorker(command, true))
            return;
        c = ctl.get();
    }
    if (isRunning(c) && workQueue.offer(command)) {
        int recheck = ctl.get();
        if (! isRunning(recheck) && remove(command))
            reject(command);
        else if (workerCountOf(recheck) == 0)
            addWorker(null, false);
    }
    else if (!addWorker(command, false))
        reject(command);
}

इस कमांड के लिए addWorker (null, false) की तुलना में workQueue.offer (कमांड) निष्पादित किया जाता है। मजदूर धागा पहले इस कमांड को थ्रेड रन विधि में कतार से बाहर ले जाता है, इसलिए इस समय कतार में एक कमांड होती है,

दूसरी कमांड इस बार सबमिट हो जाती है जब वर्कक्यू.ऑफर (कमांड) निष्पादित होता है। अब कतार भरी हुई है

अब ScheduledExecutorService resizeThreadPool पद्धति को कार्यान्वित करता है, जो setTorePoolSize को maxThreads के साथ कॉल करता है। यहाँ विधि setCorePoolSize है:

 public void setCorePoolSize(int corePoolSize) {
    if (corePoolSize < 0)
        throw new IllegalArgumentException();
    int delta = corePoolSize - this.corePoolSize;
    this.corePoolSize = corePoolSize;
    if (workerCountOf(ctl.get()) > corePoolSize)
        interruptIdleWorkers();
    else if (delta > 0) {
        // We don't really know how many new threads are "needed".
        // As a heuristic, prestart enough new workers (up to new
        // core size) to handle the current number of tasks in
        // queue, but stop if queue becomes empty while doing so.
        int k = Math.min(delta, workQueue.size());
        while (k-- > 0 && addWorker(null, true)) {
            if (workQueue.isEmpty())
                break;
        }
    }
}

यह विधि ऐड वर्कर (अशक्त, सत्य) का उपयोग करके एक कार्यकर्ता को जोड़ती है। नहीं, 2 श्रमिक कतारें चल रही हैं, अधिकतम और कतार भरी हुई है।

तीसरा कमांड जमा हो जाता है और विफल हो जाता है क्योंकि वर्कक्यूयू.ऑफर (कमांड) और एडवर्कर (कमांड, गलत) फेल हो जाता है, जिससे अपवाद होता है:

java.util.concurrent.RejectedExecutionException: Task java.util.concurrent.FutureTask@24c22fe rejected from java.util.concurrent.ThreadPoolExecutor@cd1e646[Running, pool size = 2, active threads = 2, queued tasks = 2, completed tasks = 0]
at java.util.concurrent.ThreadPoolExecutor$AbortPolicy.rejectedExecution(ThreadPoolExecutor.java:2047)
at java.util.concurrent.ThreadPoolExecutor.reject(ThreadPoolExecutor.java:823)
at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1369)
at java.util.concurrent.AbstractExecutorService.submit(AbstractExecutorService.java:112)
at ThreadPoolResizeTest.executeOnce(ThreadPoolResizeTest.java:60)
at ThreadPoolResizeTest.runTest(ThreadPoolResizeTest.java:28)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:44)
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:15)
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:41)
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:20)
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:263)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:69)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:48)
at org.junit.runners.ParentRunner$3.run(ParentRunner.java:231)
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:60)
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:229)
at org.junit.runners.ParentRunner.access$000(ParentRunner.java:50)
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:222)
at org.junit.runners.ParentRunner.run(ParentRunner.java:292)
at org.apache.maven.surefire.junit4.JUnit4Provider.execute(JUnit4Provider.java:365)

मुझे लगता है कि इस समस्या को हल करने के लिए आपको उस क्षमता को निर्धारित करना चाहिए जिसे आप निष्पादित करना चाहते हैं।


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

2

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

जावा डॉक्स

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

जब आप कोर पूल के आकार का आकार बदल देते हैं, तो कहते हैं कि वृद्धि, अतिरिक्त श्रमिकों को बनाया जाता है ( addWorkerविधि में setCorePoolSize) और अतिरिक्त कार्य बनाने के लिए कॉल ( addWorkerविधि execute) addWorkerरिटर्न को गलत ( add Workerअंतिम कोड स्निपेट) के रूप में खारिज कर दिया जाता है क्योंकि पर्याप्त अतिरिक्त कार्यकर्ता पहले से ही हैं द्वारा बनाया गया है, setCorePoolSize लेकिन कतार में अद्यतन को प्रतिबिंबित करने के लिए अभी तक नहीं चलाया गया है

प्रासंगिक भाग

तुलना

public void setCorePoolSize(int corePoolSize) {
    ....
    int k = Math.min(delta, workQueue.size());
    while (k-- > 0 && addWorker(null, true)) {
        if (workQueue.isEmpty())
             break;
    }
}

public void execute(Runnable command) {
    ...
    int c = ctl.get();
    if (workerCountOf(c) < corePoolSize) {
        if (addWorker(command, true))
            return;
        c = ctl.get();
    }
    if (isRunning(c) && workQueue.offer(command)) {
        int recheck = ctl.get();
        if (! isRunning(recheck) && remove(command))
            reject(command);
        else if (workerCountOf(recheck) == 0)
            addWorker(null, false);
    }
    else if (!addWorker(command, false))
        reject(command);
}

private boolean addWorker(Runnable firstTask, boolean core) {
....
   if (wc >= CAPACITY || wc >= (core ? corePoolSize : maximumPoolSize))
     return false;             
}

कस्टम रिट्री रिजेक्शन एक्ज़ीक्यूशन हैंडलर का उपयोग करें (यह आपके मामले के लिए काम करना चाहिए क्योंकि आपके पास अधिकतम पूल आकार के रूप में ऊपरी सीमा है)। कृपया आवश्यकतानुसार समायोजित करें।

public static class RetryRejectionPolicy implements RejectedExecutionHandler {
    public RetryRejectionPolicy () {}

    public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
        if (!e.isShutdown()) {
           while(true)
            if(e.getQueue().offer(r)) break;
        }
    }
}

ThreadPoolExecutor pool = new ThreadPoolExecutor(
      minThreads, maxThreads,
      0, TimeUnit.SECONDS,
      new LinkedBlockingQueue<Runnable>(queueCapacity),
      new ThreadPoolResizeTest.RetryRejectionPolicy()
 );

यह भी ध्यान दें कि शटडाउन का आपका उपयोग सही नहीं है क्योंकि यह निष्पादन को पूरा करने के लिए प्रस्तुत कार्य की प्रतीक्षा नहीं करेगा बल्कि awaitTerminationइसके साथ उपयोग करेगा।


मुझे लगता है कि शटडाउन पहले से ही सबमिट किए गए कार्यों के लिए इंतजार कर रहा है, JavaDoc के अनुसार: शटडाउन () एक अर्दली शटडाउन की शुरुआत करता है जिसमें पहले सबमिट किए गए कार्यों को निष्पादित किया जाता है, लेकिन कोई नया कार्य स्वीकार नहीं किया जाएगा।
थॉमस क्राइगर

@ThomasKrieger - यह पहले से सबमिट किए गए कार्यों को निष्पादित करेगा लेकिन उन्हें समाप्त करने के लिए इंतजार नहीं करेगा - डॉक्स से डॉक्स ।oracle.com / javase / 7/docs/ api / java / util / concurrent/ - कार्यों को पूरा करने के लिए। ऐसा करने के लिए waitTermination का उपयोग करें।
सागर वीरम
हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.