জাভা থ্রেডপুলঅ্যাক্সিকিউটর: মূল পুলের আকার আপডেট করা অন্তর্বর্তী আগত কাজগুলিকে গতিশীলভাবে প্রত্যাখ্যান করে


13

আমি একটি ইস্যুতে চলেছি যেখানে ThreadPoolExecutorপুলটি তৈরির পরে যদি আমি একটি পুলের পুলের আকারকে আলাদা একটি সংখ্যার সাথে পুনরায় আকার দেওয়ার চেষ্টা করি , তবে মাঝে মাঝে মাঝে কিছু কাজকে RejectedExecutionExceptionআমি প্রত্যাখ্যান করেও প্রত্যাখ্যান করা হয় যদিও আমি কখনও queueSize + maxPoolSizeসংখ্যার চেয়ে বেশি কাজ জমা দিই না ।

আমি যে সমস্যার সমাধান করতে চাইছি তা হ'ল ThreadPoolExecutorথ্রেড পুলের কাতারে বসে থাকা মুলতুবি মৃত্যুদণ্ডের ভিত্তিতে এর মূল থ্রেডগুলির আকার পরিবর্তন করে extend আমার এটি দরকার কারণ ডিফল্টরূপে 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();
    }
}

যদি আমি কিউ-ক্যাপাসিটি + ম্যাক্সথ্রেডগুলি আরও কখনও জমা না করি তবে পুলটিকে কখনই প্রত্যাখাত এক্সেক্সিউশনএক্সেপশন নিক্ষেপ করা উচিত। থ্রেডপুলএক্সিকিউটারের সংজ্ঞা অনুসারে আমি কখনই সর্বোচ্চ থ্রেডগুলি পরিবর্তন করছি না, এটি হয় কোনও থ্রেডে বা কাতারে সামঞ্জস্য করা উচিত।

অবশ্যই, যদি আমি পুলটিকে কখনই আকার না দিয়ে থাকি তবে থ্রেড পুল কখনই কোনও জমা দেয় না। এটি ডিবাগ করাও শক্ত যেহেতু সাবমিশনে কোনও ধরণের বিলম্ব যুক্ত করা সমস্যাটিকে দূরে সরিয়ে দেয়।

প্রত্যাশিত এক্সেক্সিউশন এক্সপসেপশন কীভাবে ঠিক করবেন তার কোনও পয়েন্টার?


আপনার নিজস্ব বাস্তবায়ন কেন ExecutorServiceকোনও বিদ্যমানকে মুড়ে ফেলে যা পুনরায় আকার দেওয়ার কারণে জমা দেওয়া কাজগুলিকে পুনরায় জমা দেয়?
দানিউ

@ ডানিউ যেটা একান্তই কার্যকর নয়। প্রশ্নের বিন্দুটি হ'ল আমি যদি ক্যু ক্যাপাসিটি + ম্যাক্সথ্রেডগুলি আরও কখনও জমা না করি তবে পুলটিকে কেন একটি প্রত্যাখাত এক্সেক্সিউশনএক্সেপশন নিক্ষেপ করা উচিত। থ্রেডপুলএক্সিকিউটারের সংজ্ঞা অনুসারে আমি কখনই সর্বোচ্চ থ্রেডগুলি পরিবর্তন করছি না, এটি হয় কোনও থ্রেডে বা কাতারে সামঞ্জস্য করা উচিত।
স্বরাঙ্গা সরমা

ঠিক আছে আমি আপনার প্রশ্নটি ভুল বুঝেছি বলে মনে হচ্ছে। এটা কি? আপনি কী জানতে চান যে আচরণটি ঘটে কেন বা আপনি কীভাবে এটির আশেপাশে আসেন আপনার সমস্যার কারণ হয়ে দাঁড়ায়?
দানিউ

হ্যাঁ, আমার বাস্তবায়নকারীকে একটি নির্বাহক পরিষেবাতে পরিবর্তন করা সম্ভব হয় না কারণ প্রচুর কোড থ্রেডপুলএক্সেকিউটারকে বোঝায়। সুতরাং আমি যদি এখনও একটি পরিবর্তনযোগ্য থ্রেডপুলএক্সেকিউটার রাখতে চাইতাম তবে এটি কীভাবে ঠিক করতে পারি তা আমার জানা দরকার। এর মতো কিছু করার সঠিক উপায় হ'ল থ্রেডপুলএক্সেকিউটারকে প্রসারিত করা এবং এর কয়েকটি সুরক্ষিত ভেরিয়েবলের অ্যাক্সেস পাওয়া এবং সুপার শ্রেণীর দ্বারা ভাগ করা লকটিতে একটি সিঙ্ক্রোনাইজড ব্লকের মধ্যে পুলের আকার আপডেট করা।
স্বরঙ্গ সরমা

প্রসারিত 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);
}

এই কমান্ডের জন্য workQueue.offer (কমান্ড) এর চেয়ে অ্যাড ওয়ার্কার (নাল, মিথ্যা) কার্যকর করা হয়। কর্মী থ্রেড প্রথমে এই কমান্ডটি থ্রেড রান পদ্ধতিতে সারিটির বাইরে নিয়ে যায়, সুতরাং এই সারিটিতে এখনও একটি কমান্ড রয়েছে,

দ্বিতীয় কমান্ডটি এবার workQueue.offer (কমান্ড) সম্পাদন করা হয়ে যায়। এখন সারি পূর্ণ

এখন শিডিউলেডএক্সেকিউটারসেবা রিসাইজথ্রেডপুল পদ্ধতি কার্যকর করে যা ম্যাক্সথ্রেড সহ সেটকোরপুলসাইজকে কল করে। কোরপুলসাইজ পদ্ধতিটি এখানে রয়েছে:

 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 কর্মী সারি চলছে না, সর্বাধিক এবং সারি পূর্ণ।

তৃতীয় কমান্ড জমা দেওয়া হয় এবং ব্যর্থ হয় কারণ workQueue.offer (কমান্ড) এবং অ্যাড ওয়ার্কার (কমান্ড, মিথ্যা) ব্যর্থ হয়, ব্যতিক্রমের দিকে নিয়ে যায়:

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)

আমি মনে করি এই সমস্যাটি সমাধান করার জন্য আপনার সর্বাধিক কমান্ডগুলি কার্যকর করতে চান তার সারিটির সক্ষমতা নির্ধারণ করা উচিত।


সঠিক। আমি কোডটি আমার নিজের ক্লাসে অনুলিপি করে এবং লগারে যুক্ত করে তিরস্কার করতে সক্ষম হয়েছি। মূলত, যখন সারিটি পূর্ণ থাকে এবং আমি একটি নতুন কার্য জমা দিই, এটি একটি নতুন কর্মী তৈরি করার চেষ্টা করবে। ততক্ষণে যদি সেই মুহুর্তে, আমার পুনরায় প্রয়োগকারী সেটকোরপুলসাইজকে 2 এ কল করে, এটি একটি নতুন কর্মীও তৈরি করে। এই মুহুর্তে, দু'জন কর্মী যুক্ত হওয়ার প্রতিযোগিতা করছে তবে তারা উভয়ই হতে পারে না কারণ এটি সর্বাধিক-পুল-আকারের সীমাবদ্ধতা লঙ্ঘন করবে যাতে নতুন কার্য জমা দেওয়া প্রত্যাখ্যান হয়ে যায়। আমি মনে করি এটি একটি প্রতিযোগিতার শর্ত এবং আমি ওপেনজেডিকে একটি বাগ রিপোর্ট দায়ের করেছি। দেখা যাক. তবে আপনি আমার প্রশ্নের উত্তর দিয়েছেন যাতে আপনি অনুগ্রহ পান। ধন্যবাদ.
স্বরঙ্গ সরমা

2

এটি বাগ হিসাবে যোগ্যতা অর্জন করে কিনা তা নিশ্চিত নন। সারি পূর্ণ হওয়ার পরে অতিরিক্ত শ্রমিকের থ্রেড তৈরি হওয়ার পরে এটিই আচরণ করা হয় তবে জাভা ডক্সে এই জাতীয়ভাবে উল্লেখ করা হয়েছে যে কলারকে কাজগুলি প্রত্যাখ্যান হওয়ার সাথে মোকাবিলা করতে হবে।

জাভা ডক্স

নতুন থ্রেড জন্য কারখানা। সমস্ত থ্রেড এই কারখানাটি ব্যবহার করে তৈরি করা হয়েছে (পদ্ধতি অ্যাড ওয়ার্কারের মাধ্যমে)। অ্যাড ওয়ার্কারকে ব্যর্থ হওয়ার জন্য সমস্ত কলার অবশ্যই প্রস্তুত থাকতে হবে, যা থ্রেডের সংখ্যা সীমিত করে এমন কোনও সিস্টেম বা ব্যবহারকারীর নীতি প্রতিফলিত করতে পারে। যদিও এটি একটি ত্রুটি হিসাবে বিবেচিত না হয়, থ্রেড তৈরি করতে ব্যর্থ হওয়ার ফলে নতুন কার্যগুলি প্রত্যাখ্যান করা বা বিদ্যমান সারিগুলিতে আটকে থাকতে পারে।

যখন আপনি মূল পুলের আকারটি পুনরায় আকার দিন, বাড়তি বলতে দিন, অতিরিক্ত কর্মী তৈরি করা হয় ( 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পরিবর্তে ব্যবহার করবে ।


আমি মনে করি জাভাডক অনুসারে শাটডাউন ইতিমধ্যে জমা দেওয়া কার্যের জন্য অপেক্ষা করে: শাটডাউন () পূর্বে জমা দেওয়া কাজগুলি কার্যকর করা হয়েছে এমন একটি অর্ডলি শটডাউন শুরু করে, তবে কোনও নতুন কার্য গ্রহণ করা হবে না।
টমাস ক্রিগার

@ থমাসক্রিজার - এটি ইতিমধ্যে জমা দেওয়া কাজগুলি সম্পাদন করবে তবে ডকস থেকে ডকস.ওরাকল.com / javase / 7 / docs/ api / java / util / concurrent/… - এই পদ্ধতিটি পূর্বে জমা দেওয়ার জন্য অপেক্ষা করে না কার্য সম্পাদন সম্পূর্ণ। এটি করার জন্য প্রতীক্ষা করুন ব্যবহার করুন।
সাগর বীরম
আমাদের সাইট ব্যবহার করে, আপনি স্বীকার করেছেন যে আপনি আমাদের কুকি নীতি এবং গোপনীয়তা নীতিটি পড়েছেন এবং বুঝতে পেরেছেন ।
Licensed under cc by-sa 3.0 with attribution required.