วิธีการบล็อกวิธีการ submit () ของ ThreadPoolExecutor ถ้ามันอิ่มตัว?


102

ฉันต้องการสร้างสิ่งThreadPoolExecutorนั้นเมื่อถึงขนาดสูงสุดและคิวเต็มsubmit()เมธอดจะบล็อกเมื่อพยายามเพิ่มงานใหม่ ฉันจำเป็นต้องใช้งานแบบกำหนดเองRejectedExecutionHandlerสำหรับสิ่งนั้นหรือมีวิธีที่มีอยู่ในการดำเนินการนี้โดยใช้ไลบรารี Java มาตรฐานหรือไม่


2
สิ่งที่คุณต้องการคืออะไรเช่นวิธีการเสนอ () ของคิวการบล็อกอาร์เรย์หรือไม่?
ภายนอก


2
@bacar ฉันไม่เห็นด้วย คำถาม & คำตอบนี้ดูมีค่ามากขึ้น (นอกเหนือจากอายุมากกว่า)
JasonMArcher

คำตอบ:


47

หนึ่งในวิธีแก้ปัญหาที่เป็นไปได้ที่ฉันเพิ่งพบ:

public class BoundedExecutor {
    private final Executor exec;
    private final Semaphore semaphore;

    public BoundedExecutor(Executor exec, int bound) {
        this.exec = exec;
        this.semaphore = new Semaphore(bound);
    }

    public void submitTask(final Runnable command)
            throws InterruptedException, RejectedExecutionException {
        semaphore.acquire();
        try {
            exec.execute(new Runnable() {
                public void run() {
                    try {
                        command.run();
                    } finally {
                        semaphore.release();
                    }
                }
            });
        } catch (RejectedExecutionException e) {
            semaphore.release();
            throw e;
        }
    }
}

มีวิธีแก้ไขอื่น ๆ หรือไม่? ฉันต้องการบางสิ่งบางอย่างตามRejectedExecutionHandlerเนื่องจากดูเหมือนเป็นวิธีมาตรฐานในการจัดการกับสถานการณ์ดังกล่าว


2
มีเงื่อนไขการแข่งขันที่นี่ระหว่างจุดที่เซมาฟอร์ถูกปล่อยออกมาในประโยคสุดท้ายและได้รับสัญญาณหรือไม่?
volni

2
ดังที่กล่าวไว้ข้างต้นการใช้งานนี้มีข้อบกพร่องเนื่องจากสัญญาณถูกปล่อยออกมาก่อนที่งานจะเสร็จสมบูรณ์ จะดีกว่าถ้าใช้ method java.util.concurrent.ThreadPoolExecutor # afterExecute (Runnable, Throwable)
FelixM

2
@FelixM: การใช้ java.util.concurrent.ThreadPoolExecutor # afterExecute (Runnable, Throwable) จะไม่แก้ปัญหาเพราะ afterExecute ถูกเรียกทันทีหลังจาก task.run () ใน java.util.concurrent.ThreadPoolExecutor # runWorker (Worker w) ก่อน นำองค์ประกอบถัดไปจากคิว (ดูที่ซอร์สโค้ดของ openjdk 1.7.0.6)
Jaan

1
คำตอบนี้มาจากหนังสือ Java Concurrency in Practice โดย Brian Goetz
orangepips

11
คำตอบนี้ไม่ถูกต้องทั้งหมดนอกจากนี้ยังเป็นความคิดเห็น โค้ดส่วนนี้มาจาก Java Concurrency in Practice และจะถูกต้องหากคุณคำนึงถึงบริบทของมัน หนังสือระบุอย่างชัดเจนตามตัวอักษร: "ในแนวทางดังกล่าวให้ใช้คิวที่ไม่ถูกผูกมัด (... ) และกำหนดขอบเขตบนเซมาฟอร์ให้เท่ากับขนาดพูลบวกจำนวนงานในคิวที่คุณต้องการอนุญาต" ด้วยคิวที่ไม่ถูกผูกมัดงานจะไม่ถูกปฏิเสธดังนั้นการยกเลิกข้อยกเว้นใหม่จึงไร้ประโยชน์อย่างสิ้นเชิง! ซึ่งก็คือผมเชื่อว่ายังมีเหตุผลที่throw e;เป็นไม่ได้ในหนังสือเล่มนี้ JCIP ถูกต้อง!
Timmos

30

คุณสามารถใช้ ThreadPoolExecutor และ blockQueue:

public class ImageManager {
    BlockingQueue<Runnable> blockingQueue = new ArrayBlockingQueue<Runnable>(blockQueueSize);
    RejectedExecutionHandler rejectedExecutionHandler = new ThreadPoolExecutor.CallerRunsPolicy();
    private ExecutorService executorService =  new ThreadPoolExecutor(numOfThread, numOfThread, 
        0L, TimeUnit.MILLISECONDS, blockingQueue, rejectedExecutionHandler);

    private int downloadThumbnail(String fileListPath){
        executorService.submit(new yourRunnable());
    }
}

ฉันแค่อยากจะบอกว่านี่เป็นวิธีแก้ปัญหาที่ง่ายและรวดเร็วอย่างเหลือเชื่อในการนำไปใช้ซึ่งได้ผลดีมาก!
Ivan

58
สิ่งนี้เรียกใช้งานที่ถูกปฏิเสธบนเธรดการส่ง ฟังก์ชันใดไม่เป็นไปตามข้อกำหนดของ OP
การรับรู้

4
การดำเนินการนี้จะเรียกใช้งาน "ในเธรดการเรียก" แทนที่จะบล็อกเพื่อวางไว้บนคิวซึ่งอาจมีผลเสียบางอย่างเช่นหากมีหลายเธรดเรียกแบบนี้งานมากกว่า "ขนาดคิว" จะทำงานอยู่และถ้า งานที่เกิดขึ้นใช้เวลานานกว่าที่คาดไว้เธรด "การผลิต" ของคุณอาจทำให้ตัวดำเนินการไม่ว่าง แต่ทำงานได้ดีที่นี่!
rogerdpack

4
ลดลง: สิ่งนี้ไม่ได้ปิดกั้นเมื่อ TPE อิ่มตัว นี่เป็นเพียงทางเลือกไม่ใช่ทางแก้ปัญหา
Timmos

1
คะแนนโหวต: สิ่งนี้เหมาะกับเธรดไคลเอ็นต์ 'การออกแบบ TPE' และ 'บล็อกตามธรรมชาติ' มากโดยให้พวกเขาได้ลิ้มรสมากเกินไป สิ่งนี้ควรครอบคลุมกรณีการใช้งานส่วนใหญ่ แต่ไม่ใช่ทั้งหมดแน่นอนและคุณควรเข้าใจว่าเกิดอะไรขึ้นภายใต้ประทุน
Mike

12

คุณควรใช้CallerRunsPolicyซึ่งเรียกใช้งานที่ถูกปฏิเสธในเธรดการโทร ด้วยวิธีนี้จะไม่สามารถส่งงานใหม่ใด ๆ ไปยังผู้ดำเนินการได้จนกว่างานนั้นจะเสร็จสิ้น ณ จุดนั้นจะมีเธรดพูลว่างหรือกระบวนการจะทำซ้ำ

http://java.sun.com/j2se/1.5.0/docs/api/java/util/concurrent/ThreadPoolExecutor.CallerRunsPolicy.html

จากเอกสาร:

งานที่ถูกปฏิเสธ

งานใหม่ที่ส่งใน method execute (java.lang.Runnable) จะถูกปฏิเสธเมื่อ Executor ปิดตัวลงและเมื่อ Executor ใช้ขอบเขต จำกัด สำหรับทั้งเธรดสูงสุดและความจุคิวงานและอิ่มตัว ไม่ว่าในกรณีใดเมธอด execute จะเรียกใช้เมธอด RejectedExecutionHandler.rejectedExecution (java.lang.Runnable, java.util.concurrent.ThreadPoolExecutor) ของ RejectedExecutionHandler มีนโยบายตัวจัดการที่กำหนดไว้ล่วงหน้าสี่นโยบาย:

  1. ใน ThreadPoolExecutor.AbortPolicy ดีฟอลต์ตัวจัดการจะพ่นรันไทม์ RejectedExecutionException เมื่อปฏิเสธ
  2. ใน ThreadPoolExecutor.CallerRunsPolicy เธรดที่เรียกใช้ execute เองจะรันงาน สิ่งนี้มีกลไกการควบคุมการตอบกลับอย่างง่ายซึ่งจะทำให้อัตราการส่งงานใหม่ช้าลง
  3. ใน ThreadPoolExecutor.DiscardPolicy งานที่ไม่สามารถดำเนินการได้จะหลุดออกไป
  4. ใน ThreadPoolExecutor.DiscardOldestPolicy ถ้าตัวดำเนินการไม่ถูกปิดงานที่ส่วนหัวของคิวงานจะหลุดจากนั้นการดำเนินการจะถูกลองใหม่ (ซึ่งอาจล้มเหลวอีกครั้งทำให้ต้องทำซ้ำ)

นอกจากนี้ตรวจสอบให้แน่ใจว่าได้ใช้คิวที่มีขอบเขตเช่น ArrayBlockingQueue เมื่อเรียกตัวThreadPoolExecutorสร้าง มิฉะนั้นจะไม่มีอะไรถูกปฏิเสธ

แก้ไข: ในการตอบกลับความคิดเห็นของคุณตั้งค่าขนาดของ ArrayBlockingQueue ให้เท่ากับขนาดสูงสุดของเธรดพูลและใช้ AbortPolicy

แก้ไข 2: ตกลงฉันรู้ว่าคุณกำลังทำอะไรอยู่ สิ่งนี้: แทนที่beforeExecute()วิธีการตรวจสอบที่getActiveCount()ไม่เกินgetMaximumPoolSize()และถ้าเป็นเช่นนั้นให้นอนหลับแล้วลองอีกครั้ง?


3
ฉันต้องการให้มีจำนวนงานที่ดำเนินการพร้อมกันเพื่อ จำกัด ขอบเขตอย่างเคร่งครัด (ตามจำนวนเธรดใน Executor) นี่คือสาเหตุที่ฉันไม่สามารถอนุญาตให้เธรดผู้โทรดำเนินการงานเหล่านี้ได้ด้วยตนเอง
Fixpoint

1
AbortPolicy จะทำให้เธรดผู้โทรได้รับ RejectedExecutionException ในขณะที่ฉันต้องการเพียงแค่บล็อก
Fixpoint

2
ฉันขอบล็อคไม่นอน & โพล;)
Fixpoint

@danben: คุณไม่ได้หมายถึงCallerRunsPolicy ?
user359996

7
ปัญหาเกี่ยวกับ CallerRunPolicy คือถ้าคุณมีผู้ผลิตเธรดเดียวคุณมักจะไม่มีการใช้เธรดหากงานที่รันเป็นเวลานานเกิดขึ้นจนถูกปฏิเสธ (เนื่องจากงานอื่น ๆ ในเธรดพูลจะเสร็จสิ้นในขณะที่งานที่รันเป็นเวลานานยังคงอยู่ วิ่ง).
Adam Gent

6

Hibernate BlockPolicyนั้นเรียบง่ายและสามารถทำในสิ่งที่คุณต้องการ:

ดู: Executors.java

/**
 * A handler for rejected tasks that will have the caller block until
 * space is available.
 */
public static class BlockPolicy implements RejectedExecutionHandler {

    /**
     * Creates a <tt>BlockPolicy</tt>.
     */
    public BlockPolicy() { }

    /**
     * Puts the Runnable to the blocking queue, effectively blocking
     * the delegating thread until space is available.
     * @param r the runnable task requested to be executed
     * @param e the executor attempting to execute this task
     */
    public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
        try {
            e.getQueue().put( r );
        }
        catch (InterruptedException e1) {
            log.error( "Work discarded, thread was interrupted while waiting for space to schedule: {}", r );
        }
    }
}

4
ในความคิดที่สองนี่เป็นความคิดที่ไม่ดีเลยทีเดียว ฉันไม่แนะนำให้คุณใช้มัน ดูเหตุผลดีๆได้ที่นี่: stackoverflow.com/questions/3446011/…
Nate Murray

นอกจากนี้ยังไม่ได้ใช้ "ไลบรารี Java มาตรฐาน" ตามคำขอของ OP ลบ?
user359996

1
ว้าวน่าเกลียดมาก โดยทั่วไปโซลูชันนี้จะรบกวนการทำงานภายในของ TPE javadoc สำหรับThreadPoolExecutorแม้แต่พูดตามตัวอักษร: "Method getQueue () อนุญาตให้เข้าถึงคิวงานเพื่อวัตถุประสงค์ในการตรวจสอบและการดีบักไม่แนะนำให้ใช้วิธีนี้เพื่อจุดประสงค์อื่น" สิ่งนี้มีอยู่ในห้องสมุดที่เป็นที่รู้จักอย่างกว้างขวางเป็นเรื่องน่าเศร้าอย่างยิ่งที่ได้เห็น
Timmos

1
com.amazonaws.services.simpleworkflow.flow.worker.BlockCallerPolicy จะคล้ายกัน
Adrian Baker

6

BoundedExecutorคำตอบที่ยกมาข้างต้นจากJava Concurrency ในการปฏิบัติงานได้เฉพาะอย่างถูกต้องถ้าคุณใช้คิวมากมายสำหรับผู้ปฏิบัติการหรือสัญญาณผูกพันที่มีขนาดใหญ่กว่าขนาดไม่มีคิว เซมาฟอร์เป็นสถานะที่ใช้ร่วมกันระหว่างเธรดการส่งและเธรดในพูลทำให้สามารถทำให้ตัวดำเนินการอิ่มตัวแม้ว่าขนาดคิว <ขอบเขต <= (ขนาดคิว + ขนาดพูล)

การใช้CallerRunsPolicyจะใช้ได้ก็ต่อเมื่องานของคุณไม่ทำงานตลอดไปซึ่งในกรณีนี้เธรดที่คุณส่งจะยังคงอยู่rejectedExecutionตลอดไปและเป็นความคิดที่ไม่ดีหากงานของคุณใช้เวลานานในการรันเนื่องจากเธรดการส่งไม่สามารถส่งงานใหม่ ๆ หรือ ทำอย่างอื่นถ้ามันทำงานเอง

หากไม่สามารถยอมรับได้ฉันขอแนะนำให้ตรวจสอบขนาดของคิวที่ถูกล้อมรอบของผู้ดำเนินการก่อนส่งงาน หากคิวเต็มให้รอสักครู่ก่อนลองส่งอีกครั้ง ปริมาณงานจะได้รับผลกระทบ แต่ฉันขอแนะนำว่าเป็นวิธีแก้ปัญหาที่ง่ายกว่าโซลูชันอื่น ๆ ที่เสนอและรับประกันว่าจะไม่มีงานใดถูกปฏิเสธ


ฉันไม่แน่ใจว่าการตรวจสอบความยาวคิวก่อนส่งจะรับประกันได้อย่างไรว่าไม่มีงานที่ถูกปฏิเสธในสภาพแวดล้อมแบบมัลติเธรดที่มีผู้ผลิตงานหลายคน นั่นไม่ได้ฟังดูปลอดภัย
ทิม

5

ฉันรู้ว่ามันเป็นการแฮ็ก แต่ในความคิดของฉันแฮ็คที่สะอาดที่สุดระหว่างที่เสนอที่นี่ ;-)

เนื่องจาก ThreadPoolExecutor ใช้การบล็อกคิว "offer" แทน "put" ทำให้สามารถลบล้างพฤติกรรมของ "offer" ของคิวการบล็อกได้:

class BlockingQueueHack<T> extends ArrayBlockingQueue<T> {

    BlockingQueueHack(int size) {
        super(size);
    }

    public boolean offer(T task) {
        try {
            this.put(task);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        return true;
    }
}

ThreadPoolExecutor tp = new ThreadPoolExecutor(1, 2, 1, TimeUnit.MINUTES, new BlockingQueueHack(5));

ฉันทดสอบแล้วและดูเหมือนว่าจะได้ผล การใช้นโยบายการหมดเวลาบางส่วนถือเป็นการออกกำลังกายของผู้อ่าน


ดูstackoverflow.com/a/4522411/2601671สำหรับเวอร์ชันที่ล้างข้อมูลนี้ ฉันเห็นด้วยเป็นวิธีที่สะอาดที่สุดที่จะทำ
เทรนตัน

3

คลาสต่อไปนี้ล้อมรอบ ThreadPoolExecutor และใช้ Semaphore เพื่อบล็อกจากนั้นคิวงานจะเต็ม:

public final class BlockingExecutor { 

    private final Executor executor;
    private final Semaphore semaphore;

    public BlockingExecutor(int queueSize, int corePoolSize, int maxPoolSize, int keepAliveTime, TimeUnit unit, ThreadFactory factory) {
        BlockingQueue<Runnable> queue = new LinkedBlockingQueue<Runnable>();
        this.executor = new ThreadPoolExecutor(corePoolSize, maxPoolSize, keepAliveTime, unit, queue, factory);
        this.semaphore = new Semaphore(queueSize + maxPoolSize);
    }

    private void execImpl (final Runnable command) throws InterruptedException {
        semaphore.acquire();
        try {
            executor.execute(new Runnable() {
                @Override
                public void run() {
                    try {
                        command.run();
                    } finally {
                        semaphore.release();
                    }
                }
            });
        } catch (RejectedExecutionException e) {
            // will never be thrown with an unbounded buffer (LinkedBlockingQueue)
            semaphore.release();
            throw e;
        }
    }

    public void execute (Runnable command) throws InterruptedException {
        execImpl(command);
    }
}

คลาส Wrapper นี้ขึ้นอยู่กับโซลูชันที่ให้ไว้ในหนังสือ Java Concurrency in Practice โดย Brian Goetz คำตอบในหนังสือเล่มนี้ใช้พารามิเตอร์ตัวสร้างสองตัวเท่านั้น: Executorและขอบเขตที่ใช้สำหรับเซมาฟอร์ สิ่งนี้แสดงในคำตอบที่กำหนดโดย Fixpoint มีปัญหากับแนวทางนั้น: อาจอยู่ในสถานะที่เธรดพูลไม่ว่างคิวเต็ม แต่เซมาฟอร์เพิ่งออกใบอนุญาต ( semaphore.release()ในบล็อกสุดท้าย) ในสถานะนี้งานใหม่สามารถคว้าใบอนุญาตที่เพิ่งออก แต่ถูกปฏิเสธเนื่องจากคิวงานเต็ม แน่นอนว่านี่ไม่ใช่สิ่งที่คุณต้องการ คุณต้องการบล็อกในกรณีนี้

ในการแก้ปัญหานี้เราต้องใช้คิวที่ไม่ถูกผูกไว้ตามที่ JCiP กล่าวไว้อย่างชัดเจน เซมาฟอร์ทำหน้าที่เป็นตัวป้องกันให้ผลของขนาดคิวเสมือน สิ่งนี้มีผลข้างเคียงที่เป็นไปได้ว่าหน่วยสามารถบรรจุmaxPoolSize + virtualQueueSize + maxPoolSizeงานได้ ทำไมถึงเป็นเช่นนั้น? เนื่องจาก semaphore.release()ในบล็อกสุดท้าย หากเธรดพูลทั้งหมดเรียกใช้คำสั่งนี้พร้อมกันการmaxPoolSizeอนุญาตจะถูกปล่อยออกมาโดยอนุญาตให้มีงานจำนวนเท่ากันเข้าสู่หน่วย หากเราใช้คิวที่มีขอบเขตคิวจะยังคงเต็มส่งผลให้งานถูกปฏิเสธ ตอนนี้เนื่องจากเรารู้ว่าสิ่งนี้จะเกิดขึ้นเฉพาะเมื่อเธรดพูลเกือบเสร็จแล้วนี่จึงไม่ใช่ปัญหา เรารู้ว่าเธรดพูลจะไม่บล็อกดังนั้นในไม่ช้างานจะถูกนำออกจากคิว

คุณสามารถใช้คิวที่มีขอบเขตได้ virtualQueueSize + maxPoolSizeเพียงให้แน่ใจว่าขนาดของมันเท่ากับ ขนาดที่ใหญ่กว่านั้นไม่มีประโยชน์เซมาฟอร์จะป้องกันไม่ให้มีรายการมากขึ้นขนาดที่เล็กลงจะส่งผลให้งานถูกปฏิเสธ โอกาสที่งานจะถูกปฏิเสธจะเพิ่มขึ้นเมื่อขนาดลดลง ตัวอย่างเช่นสมมติว่าคุณต้องการตัวดำเนินการที่มีขอบเขตที่มี maxPoolSize = 2 และ virtualQueueSize = 5 จากนั้นใช้สัญญาณที่มีใบอนุญาต 5 + 2 = 7 และขนาดคิวจริง 5 + 2 = 7 จำนวนงานจริงที่สามารถอยู่ในหน่วยคือ 2 + 5 + 2 = 9 เมื่อตัวดำเนินการเต็ม (5 งานในคิว 2 ในเธรดพูลดังนั้นจึงมี 0 อนุญาตให้ใช้ได้) และเธรดพูลทั้งหมดปล่อยการอนุญาตจากนั้นจึงสามารถรับสิทธิ์ได้ 2 สิทธิ์โดยงานที่เข้ามา

ตอนนี้วิธีแก้ปัญหาจาก JCiP ค่อนข้างยุ่งยากที่จะใช้เนื่องจากไม่ได้บังคับใช้ข้อ จำกัด เหล่านี้ทั้งหมด (คิวที่ไม่ถูกผูกมัดหรือถูก จำกัด ด้วยข้อ จำกัด ทางคณิตศาสตร์เหล่านั้นเป็นต้น) ฉันคิดว่านี่เป็นเพียงตัวอย่างที่ดีในการแสดงให้เห็นว่าคุณสามารถสร้างคลาสที่ปลอดภัยของเธรดใหม่ได้อย่างไรโดยอิงจากชิ้นส่วนที่มีอยู่แล้ว แต่ไม่ใช่คลาสที่เติบโตเต็มที่และใช้ซ้ำได้ ฉันไม่คิดว่าอย่างหลังเป็นความตั้งใจของผู้เขียน


2

คุณสามารถใช้ RejectedExecutionHandler แบบกำหนดเองได้เช่นนี้

ThreadPoolExecutor tp= new ThreadPoolExecutor(core_size, // core size
                max_handlers, // max size 
                timeout_in_seconds, // idle timeout 
                TimeUnit.SECONDS, queue, new RejectedExecutionHandler() {
                    public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
                        // This will block if the queue is full
                        try {
                            executor.getQueue().put(r);
                        } catch (InterruptedException e) {
                            System.err.println(e.getMessage());
                        }

                    }
                });

1
เอกสารสำหรับ getQueue () กล่าวถึงอย่างชัดเจนว่าการเข้าถึงคิวงานมีไว้สำหรับการดีบักและการตรวจสอบเป็นหลัก
Chadi

0

สร้างคิวการบล็อกของคุณเองเพื่อให้ Executor ใช้พร้อมกับพฤติกรรมการบล็อกที่คุณกำลังมองหาในขณะที่คืนความจุที่เหลืออยู่เสมอ (ตรวจสอบให้แน่ใจว่าตัวดำเนินการจะไม่พยายามสร้างเธรดมากกว่าพูลหลักหรือทริกเกอร์ตัวจัดการการปฏิเสธ)

ฉันเชื่อว่าสิ่งนี้จะทำให้คุณได้รับพฤติกรรมการบล็อกที่คุณกำลังมองหา ตัวจัดการการปฏิเสธจะไม่พอดีกับใบเรียกเก็บเงินเนื่องจากนั่นบ่งชี้ว่าผู้ปฏิบัติการไม่สามารถดำเนินการได้ สิ่งที่ฉันสามารถจินตนาการได้ก็คือคุณจะได้รับ 'การรอที่วุ่นวาย' ในตัวจัดการ นั่นไม่ใช่สิ่งที่คุณต้องการคุณต้องการคิวสำหรับตัวดำเนินการที่บล็อกผู้โทร ...


2
ThreadPoolExecutorใช้offerวิธีการเพิ่มงานลงในคิว ถ้าฉันสร้างแบบกำหนดเองBlockingQueueที่บล็อกไว้offerนั่นจะเป็นการผิดBlockingQueueสัญญา
Fixpoint

@Shooshpanchick นั่นจะทำลายสัญญา BlockingQueues แล้วยังไงต่อ? หากคุณกระตือรือร้นมากคุณสามารถเปิดใช้งานพฤติกรรมอย่างชัดเจนในระหว่างส่ง () เท่านั้น (จะใช้ ThreadLocal)
ที่สุด

ดูคำตอบสำหรับคำถามอื่นที่อธิบายทางเลือกนี้
Robert Tupelo-Schneck

มีเหตุผลว่าทำไมจึงThreadPoolExecutorถูกนำไปใช้offerและไม่ใช้put(เวอร์ชันการบล็อก) นอกจากนี้หากมีวิธีที่รหัสไคลเอ็นต์จะบอกได้ว่าควรใช้รหัสใดเมื่อใดผู้คนจำนวนมากที่พยายามใช้โซลูชันแบบกำหนดเองด้วยมือจะรู้สึกโล่งใจ
asgs

0

เพื่อหลีกเลี่ยงปัญหาเกี่ยวกับโซลูชัน @FixPoint หนึ่งสามารถใช้ ListeningExecutorService และปล่อยเซมาฟอร์ onSuccess และ onFailure ภายใน FutureCallback


ที่มีปัญหาโดยธรรมชาติเช่นเดียวกับเพียงแค่ตัดเป็นวิธีการเหล่านั้นยังคงเรียกว่าก่อนการทำความสะอาดของผู้ปฏิบัติงานในปกติRunnable ThreadPoolExecutorนั่นคือคุณยังคงต้องจัดการกับข้อยกเว้นการปฏิเสธ
Adam Gent

0

เมื่อเร็ว ๆ นี้ฉันพบว่าคำถามนี้มีปัญหาเดียวกัน OP ไม่ได้พูดอย่างชัดเจน แต่เราไม่ต้องการใช้RejectedExecutionHandlerซึ่งดำเนินการงานบนเธรดของผู้ส่งเนื่องจากจะใช้เธรดของผู้ปฏิบัติงานน้อยเกินไปหากงานนี้ทำงานเป็นเวลานาน

การอ่านคำตอบและความคิดเห็นทั้งหมดโดยเฉพาะอย่างยิ่งการแก้ปัญหาที่มีข้อบกพร่องด้วยเซมาฟอร์หรือการใช้afterExecuteฉันได้ตรวจสอบโค้ดของThreadPoolExecutor อย่างละเอียดเพื่อดูว่ามีทางออกบ้างหรือไม่ ฉันประหลาดใจที่จะเห็นว่ามีมากกว่า 2000 บรรทัด (ความเห็น) รหัสบางอย่างที่ทำให้ฉันรู้สึกวิงเวียน ด้วยข้อกำหนดที่ค่อนข้างง่ายที่ฉันมี - ผู้ผลิตรายหนึ่งผู้บริโภคหลายรายปล่อยให้ผู้ผลิตบล็อกเมื่อไม่มีผู้บริโภคสามารถทำงานได้ - ฉันตัดสินใจที่จะม้วนโซลูชันของตัวเอง ไม่ใช่ExecutorServiceแต่เป็นเพียงExecutorไฟล์. และไม่ได้ปรับจำนวนเธรดให้เข้ากับภาระงาน แต่มีจำนวนเธรดคงที่เท่านั้นซึ่งตรงกับความต้องการของฉันด้วย นี่คือรหัส อย่าลังเลที่จะคุยโวเกี่ยวกับเรื่องนี้ :-)

package x;

import java.util.concurrent.BlockingQueue;
import java.util.concurrent.Executor;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.SynchronousQueue;

/**
 * distributes {@code Runnable}s to a fixed number of threads. To keep the
 * code lean, this is not an {@code ExecutorService}. In particular there is
 * only very simple support to shut this executor down.
 */
public class ParallelExecutor implements Executor {
  // other bounded queues work as well and are useful to buffer peak loads
  private final BlockingQueue<Runnable> workQueue =
      new SynchronousQueue<Runnable>();
  private final Thread[] threads;

  /*+**********************************************************************/
  /**
   * creates the requested number of threads and starts them to wait for
   * incoming work
   */
  public ParallelExecutor(int numThreads) {
    this.threads = new Thread[numThreads];
    for(int i=0; i<numThreads; i++) {
      // could reuse the same Runner all over, but keep it simple
      Thread t = new Thread(new Runner());
      this.threads[i] = t;
      t.start();
    }
  }
  /*+**********************************************************************/
  /**
   * returns immediately without waiting for the task to be finished, but may
   * block if all worker threads are busy.
   * 
   * @throws RejectedExecutionException if we got interrupted while waiting
   *         for a free worker
   */
  @Override
  public void execute(Runnable task)  {
    try {
      workQueue.put(task);
    } catch (InterruptedException e) {
      Thread.currentThread().interrupt();
      throw new RejectedExecutionException("interrupt while waiting for a free "
          + "worker.", e);
    }
  }
  /*+**********************************************************************/
  /**
   * Interrupts all workers and joins them. Tasks susceptible to an interrupt
   * will preempt their work. Blocks until the last thread surrendered.
   */
  public void interruptAndJoinAll() throws InterruptedException {
    for(Thread t : threads) {
      t.interrupt();
    }
    for(Thread t : threads) {
      t.join();
    }
  }
  /*+**********************************************************************/
  private final class Runner implements Runnable {
    @Override
    public void run() {
      while (!Thread.currentThread().isInterrupted()) {
        Runnable task;
        try {
          task = workQueue.take();
        } catch (InterruptedException e) {
          // canonical handling despite exiting right away
          Thread.currentThread().interrupt(); 
          return;
        }
        try {
          task.run();
        } catch (RuntimeException e) {
          // production code to use a logging framework
          e.printStackTrace();
        }
      }
    }
  }
}

0

ผมเชื่อว่ามีวิธีที่สง่างามมากทีเดียวที่จะแก้ปัญหานี้โดยใช้และพฤติกรรมของการมอบหมายjava.util.concurrent.Semaphore Executor.newFixedThreadPoolบริการตัวดำเนินการใหม่จะดำเนินการงานใหม่ก็ต่อเมื่อมีเธรดให้ทำเช่นนั้น การบล็อกถูกจัดการโดย Semaphore โดยมีจำนวนอนุญาตเท่ากับจำนวนเธรด เมื่องานเสร็จสิ้นจะส่งคืนใบอนุญาต

public class FixedThreadBlockingExecutorService extends AbstractExecutorService {

private final ExecutorService executor;
private final Semaphore blockExecution;

public FixedThreadBlockingExecutorService(int nTreads) {
    this.executor = Executors.newFixedThreadPool(nTreads);
    blockExecution = new Semaphore(nTreads);
}

@Override
public void shutdown() {
    executor.shutdown();
}

@Override
public List<Runnable> shutdownNow() {
    return executor.shutdownNow();
}

@Override
public boolean isShutdown() {
    return executor.isShutdown();
}

@Override
public boolean isTerminated() {
    return executor.isTerminated();
}

@Override
public boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException {
    return executor.awaitTermination(timeout, unit);
}

@Override
public void execute(Runnable command) {
    blockExecution.acquireUninterruptibly();
    executor.execute(() -> {
        try {
            command.run();
        } finally {
            blockExecution.release();
        }
    });
}

ฉันใช้งาน BoundedExecutor ที่อธิบายไว้ใน Java Concurrency in Practice และพบว่า Semaphore ต้องเริ่มต้นด้วยค่าสถานะความเป็นธรรมที่ตั้งค่าเป็นจริงเพื่อให้แน่ใจว่ามีการเสนอใบอนุญาตเซมาฟอร์ในคำขอสั่งซื้อ โปรดดูdocs.oracle.com/javase/7/docs/api/java/util/concurrent/... สำหรับรายละเอียด
Prahalad Deshpande

0

ฉันมีความต้องการเหมือนกันในอดีต: คิวการบล็อกชนิดหนึ่งที่มีขนาดคงที่สำหรับไคลเอ็นต์แต่ละตัวที่ได้รับการสนับสนุนโดยเธรดพูลที่ใช้ร่วมกัน ฉันลงเอยด้วยการเขียน ThreadPoolExecutor ของตัวเอง:

UserThreadPoolExecutor (บล็อกคิว (ต่อไคลเอนต์) + เธรดพูล (แบ่งใช้ระหว่างไคลเอนต์ทั้งหมด))

ดู: https://github.com/d4rxh4wx/UserThreadPoolExecutor

UserThreadPoolExecutor แต่ละตัวจะได้รับจำนวนเธรดสูงสุดจาก ThreadPoolExecutor ที่แบ่งใช้

UserThreadPoolExecutor แต่ละตัวสามารถ:

  • ส่งงานไปยังตัวดำเนินการพูลเธรดที่ใช้ร่วมกันหากไม่ถึงโควต้า หากถึงโควต้างานจะอยู่ในคิว (การบล็อกที่ไม่สิ้นเปลืองกำลังรอ CPU) เมื่องานที่ส่งเสร็จสมบูรณ์โควต้าจะถูกลดลงทำให้งานอื่นที่รอส่งไปยัง ThreadPoolExecutor
  • รอให้งานที่เหลือเสร็จสมบูรณ์

0

ฉันพบนโยบายการปฏิเสธนี้ในไคลเอนต์การค้นหาแบบยืดหยุ่น บล็อกเธรดผู้โทรในคิวการบล็อก รหัสด้านล่าง -

 static class ForceQueuePolicy implements XRejectedExecutionHandler 
 {
        public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) 
        {
            try 
            {
                executor.getQueue().put(r);
            } 
            catch (InterruptedException e) 
            {
                //should never happen since we never wait
                throw new EsRejectedExecutionException(e);
            }
        }

        @Override
        public long rejected() 
        {
            return 0;
        }
}

0

เมื่อเร็ว ๆ นี้ฉันมีความต้องการที่จะบรรลุสิ่งที่คล้ายกัน แต่ในไฟล์ScheduledExecutorService.

ฉันต้องตรวจสอบให้แน่ใจด้วยว่าฉันจัดการกับความล่าช้าในการส่งต่อวิธีการและตรวจสอบให้แน่ใจว่ามีการส่งงานไปดำเนินการตามเวลาที่ผู้โทรคาดหวังหรือล้มเหลวดังนั้นจึงโยนไฟล์RejectedExecutionException.

วิธีการอื่น ๆScheduledThreadPoolExecutorในการดำเนินการหรือส่งงานการโทรภายใน#scheduleซึ่งจะยังคงเรียกใช้เมธอดที่ถูกแทนที่

import java.util.concurrent.*;

public class BlockingScheduler extends ScheduledThreadPoolExecutor {
    private final Semaphore maxQueueSize;

    public BlockingScheduler(int corePoolSize,
                             ThreadFactory threadFactory,
                             int maxQueueSize) {
        super(corePoolSize, threadFactory, new AbortPolicy());
        this.maxQueueSize = new Semaphore(maxQueueSize);
    }

    @Override
    public ScheduledFuture<?> schedule(Runnable command,
                                       long delay,
                                       TimeUnit unit) {
        final long newDelayInMs = beforeSchedule(command, unit.toMillis(delay));
        return super.schedule(command, newDelayInMs, TimeUnit.MILLISECONDS);
    }

    @Override
    public <V> ScheduledFuture<V> schedule(Callable<V> callable,
                                           long delay,
                                           TimeUnit unit) {
        final long newDelayInMs = beforeSchedule(callable, unit.toMillis(delay));
        return super.schedule(callable, newDelayInMs, TimeUnit.MILLISECONDS);
    }

    @Override
    public ScheduledFuture<?> scheduleAtFixedRate(Runnable command,
                                                  long initialDelay,
                                                  long period,
                                                  TimeUnit unit) {
        final long newDelayInMs = beforeSchedule(command, unit.toMillis(initialDelay));
        return super.scheduleAtFixedRate(command, newDelayInMs, unit.toMillis(period), TimeUnit.MILLISECONDS);
    }

    @Override
    public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command,
                                                     long initialDelay,
                                                     long period,
                                                     TimeUnit unit) {
        final long newDelayInMs = beforeSchedule(command, unit.toMillis(initialDelay));
        return super.scheduleWithFixedDelay(command, newDelayInMs, unit.toMillis(period), TimeUnit.MILLISECONDS);
    }

    @Override
    protected void afterExecute(Runnable runnable, Throwable t) {
        super.afterExecute(runnable, t);
        try {
            if (t == null && runnable instanceof Future<?>) {
                try {
                    ((Future<?>) runnable).get();
                } catch (CancellationException | ExecutionException e) {
                    t = e;
                } catch (InterruptedException ie) {
                    Thread.currentThread().interrupt(); // ignore/reset
                }
            }
            if (t != null) {
                System.err.println(t);
            }
        } finally {
            releaseQueueUsage();
        }
    }

    private long beforeSchedule(Runnable runnable, long delay) {
        try {
            return getQueuePermitAndModifiedDelay(delay);
        } catch (InterruptedException e) {
            getRejectedExecutionHandler().rejectedExecution(runnable, this);
            return 0;
        }
    }

    private long beforeSchedule(Callable callable, long delay) {
        try {
            return getQueuePermitAndModifiedDelay(delay);
        } catch (InterruptedException e) {
            getRejectedExecutionHandler().rejectedExecution(new FutureTask(callable), this);
            return 0;
        }
    }

    private long getQueuePermitAndModifiedDelay(long delay) throws InterruptedException {
        final long beforeAcquireTimeStamp = System.currentTimeMillis();
        maxQueueSize.tryAcquire(delay, TimeUnit.MILLISECONDS);
        final long afterAcquireTimeStamp = System.currentTimeMillis();
        return afterAcquireTimeStamp - beforeAcquireTimeStamp;
    }

    private void releaseQueueUsage() {
        maxQueueSize.release();
    }
}

ฉันมีรหัสที่นี่จะขอบคุณข้อเสนอแนะใด ๆ https://github.com/AmitabhAwasthi/BlockingScheduler


คำตอบนี้อาศัยเนื้อหาของลิงก์ภายนอกอย่างสมบูรณ์ หากไม่ถูกต้องคำตอบของคุณก็จะไร้ประโยชน์ ดังนั้นโปรดแก้ไขคำตอบของคุณและเพิ่มอย่างน้อยสรุปสิ่งที่สามารถพบได้ในนั้น ขอบคุณ!
Fabio กล่าว Reinstate Monica

@fabio: ขอบคุณที่ชี้ให้เห็น ฉันได้เพิ่มรหัสเข้าไปในนั้นเพื่อให้ผู้อ่านเข้าใจได้ง่ายขึ้น ชื่นชมความคิดเห็นของคุณ :)
Dev Amitabh

0

นี่คือวิธีแก้ปัญหาที่ดูเหมือนจะใช้งานได้ดีจริงๆ มันเรียกว่าNotifyingBlockingThreadPoolExecutor

โปรแกรมสาธิต

แก้ไข: มีปัญหากับรหัสนี้ await () วิธีการเป็นบั๊กกี้ การโทรปิดเครื่อง () + awaitTermination () ดูเหมือนจะทำงานได้ดี


0

ฉันไม่ชอบ CallerRunsPolicy เสมอไปโดยเฉพาะอย่างยิ่งเนื่องจากอนุญาตให้งานที่ถูกปฏิเสธ 'ข้ามคิว' และดำเนินการก่อนงานที่ส่งไปก่อนหน้านี้ ยิ่งไปกว่านั้นการรันงานบนเธรดการเรียกอาจใช้เวลานานกว่าการรอให้สล็อตแรกพร้อมใช้งาน

ฉันแก้ไขปัญหานี้โดยใช้ RejectedExecutionHandler ที่กำหนดเองซึ่งเพียงแค่บล็อกเธรดการโทรสักครู่แล้วพยายามส่งงานอีกครั้ง:

public class BlockWhenQueueFull implements RejectedExecutionHandler {

    public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {

        // The pool is full. Wait, then try again.
        try {
            long waitMs = 250;
            Thread.sleep(waitMs);
        } catch (InterruptedException interruptedException) {}

        executor.execute(r);
    }
}

คลาสนี้สามารถใช้ในเธรดพูลตัวดำเนินการเป็น RejectedExecutinHandler เช่นเดียวกับอื่น ๆ ตัวอย่างเช่น:

executorPool = new ThreadPoolExecutor(1, 1, 10,
                                      TimeUnit.SECONDS, new SynchronousQueue<Runnable>(),
                                      new BlockWhenQueueFull());

ข้อเสียเพียงอย่างเดียวที่ฉันเห็นคือเธรดการโทรอาจถูกล็อคนานกว่าที่จำเป็นเล็กน้อยเล็กน้อย (สูงสุด 250ms) ยิ่งไปกว่านั้นเนื่องจากตัวดำเนินการนี้ถูกเรียกซ้ำอย่างมีประสิทธิภาพการรอให้เธรดพร้อมใช้งานนานมาก (ชั่วโมง) อาจส่งผลให้สแต็กล้น

อย่างไรก็ตามฉันชอบวิธีนี้เป็นการส่วนตัว มีขนาดกะทัดรัดเข้าใจง่ายและใช้งานได้ดี


1
อย่างที่คุณพูดเอง: สิ่งนี้สามารถสร้าง stackoverflow ไม่ใช่สิ่งที่ฉันต้องการในรหัสการผลิต
Harald

ทุกคนควรตัดสินใจด้วยตนเอง สำหรับภาระงานของฉันนี่ไม่ใช่ปัญหา งานจะทำงานเป็นวินาทีแทนที่จะเป็นชั่วโมงที่ต้องใช้ในการระเบิดสแต็ก นอกจากนี้ยังสามารถกล่าวได้เช่นเดียวกันสำหรับอัลกอริธึมการเรียกซ้ำแทบทุกชนิด นั่นเป็นเหตุผลที่จะไม่ใช้อัลกอริทึมแบบเรียกซ้ำในการผลิตหรือไม่?
TinkerTank
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.