ExecutorService ও থ্রেড পুল: নিজে থ্রেড তৈরি করা বন্ধ করুন

এই সিরিজের প্রথম পর্বে দেখানো হয়েছে যে প্রতিটি OS থ্রেড প্রকৃত মেমোরি সংরক্ষণ করে এবং কনটেক্সট-সুইচিংয়ে বাস্তব ওভারহেড যোগ করে, আর হাজার হাজার থ্রেড তৈরি করলে অপারেটিং সিস্টেমের সম্পদ শেষ হয়ে যেতে পারে। ExecutorService-এর কাজই হলো সেই সীমা নির্ধারণ করা: একটি নির্দিষ্ট, পুনরায় ব্যবহারযোগ্য ওয়ার্কার থ্রেডের পুল, যেখানে প্রতিটি কাজের জন্য নতুন Thread না বানিয়ে কাজ জমা দেওয়া হয়।

নতুন থ্রেড চালু না করে কাজ জমা দেওয়া

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

public class ExecutorBasics {
    public static void main(String[] args) throws InterruptedException {
        ExecutorService executor = Executors.newFixedThreadPool(4);

        for (int i = 1; i <= 10; i++) {
            int taskId = i;
            executor.execute(() -> 
                System.out.println("Task " + taskId + " on " + Thread.currentThread().getName()));
        }

        executor.shutdown();
        executor.awaitTermination(5, TimeUnit.SECONDS);
    }
}

দশটি কাজ মাত্র চারটি পুনরায় ব্যবহারযোগ্য থ্রেডের ওপর চলে, আপনি যত কাজই জমা দিন না কেন। execute() সঙ্গে সঙ্গে রিটার্ন করে; এক্সিকিউটর কাজটিকে কিউতে রাখে এবং পুলের যে থ্রেডটি আগে ফ্রি হয় তাকে দিয়ে দেয়। থ্রেডগুলো একবারই তৈরি হয়, শুরুতেই, এবং পরবর্তী প্রতিটি কাজের জন্য পুনর্ব্যবহৃত হয় — এটাই সেই নির্দিষ্ট ও সীমাবদ্ধ খরচ যা new Thread()-এর অসীম খরচের বদলে এই API দেয়।

Callable ও Future দিয়ে ফলাফল ফেরত আনা

execute() কোনো মান ফেরত দিতে পারে না, চেক্ড এক্সেপশনও ছড়াতে পারে না। submit() একটি Callable-এর সাথে Future-এর মাধ্যমে তা পারে:

import java.util.concurrent.*;

public class FutureExample {
    public static void main(String[] args) throws Exception {
        ExecutorService executor = Executors.newFixedThreadPool(2);

        Future<Integer> future = executor.submit(() -> {
            Thread.sleep(200);
            return 21 * 2;
        });

        System.out.println("জমা দেওয়া হয়েছে, অন্য কাজ চলছে...");
        Integer result = future.get(); // কাজ শেষ না হওয়া পর্যন্ত ব্লক করে
        System.out.println("ফলাফল: " + result);

        executor.shutdown();
    }
}

future.get() কলিং থ্রেডকে ততক্ষণ ব্লক করে রাখে যতক্ষণ না কাজ শেষ হয়, তারপর ফলাফল রিটার্ন করে অথবা ব্যতিক্রম থাকলে সেটিকে ExecutionException-এ মুড়ে পুনরায় নিক্ষেপ করে। প্রোডাকশন কোডে future.get(timeout, unit) ব্যবহার করাই ভালো, যাতে কোনো আটকে যাওয়া কাজ কলারকে চিরতরে ব্লক না রাখে।

সঠিকভাবে শাটডাউন করা

ExecutorService-এর থ্রেডগুলো নিজে থেকে থামে না; শাটডাউন করতে ভুলে গেলে পুলের থ্রেডগুলো অনির্দিষ্টকাল চলতে থাকে। প্রথম পর্বে দেখা ডেমন থ্রেডের বিপরীতে এগুলো সাধারণ ইউজার থ্রেড, তাই JVM-কে বাঁচিয়ে রাখবে:

ExecutorService executor = Executors.newFixedThreadPool(4);
try {
    // এখানে কাজ জমা দিন
} finally {
    executor.shutdown(); // নতুন কাজ নেওয়া বন্ধ করে
    if (!executor.awaitTermination(10, TimeUnit.SECONDS)) {
        executor.shutdownNow(); // এখনও চলমান কাজগুলোতে বাধা দেয়
    }
}

shutdown() ইতিমধ্যে জমা দেওয়া কাজগুলো শেষ হতে দেয় কিন্তু নতুন কাজ আর গ্রহণ করে না; awaitTermination নির্দিষ্ট সময় পর্যন্ত অপেক্ষা করে। সময় পেরিয়ে গেলে shutdownNow() চলমান থ্রেডগুলোতে ইন্টারাপ্ট পাঠিয়ে দ্রুত বন্ধ করে।

Share