কনটেক্সট সুইচিং এবং একটি থ্রেডের প্রকৃত খরচ

একটি থ্রেড তৈরি করা সহজ মনে হয়: new Thread(...).start() এক লাইনেই সম্ভব। কিন্তু JVM আপনার হয়ে যে প্রতিটি OS থ্রেড তৈরি করে, তার বাস্তব ও চলমান খরচ আছে। একটি কনটেক্সট সুইচের আসল মূল্য এবং একটি OS থ্রেড শুধু বিদ্যমান থাকার জন্যই কত খরচ করে — এই ধারণাগুলো বোঝা গেলেই থ্রেড পুল ও সীমিত কনকারেন্সি কেন গুরুত্বপূর্ণ তা পরিষ্কার হয়। এই সিরিজের পরবর্তী ধাপগুলোতে আলোচিত প্রায় সব উন্নত কনকারেন্সি টুলের পেছনে এই বোঝাপড়াই কাজ করে।

মেমোরি: প্রতিটি থ্রেডের জন্য স্ট্যাক সংরক্ষিত

প্রতিটি থ্রেডের নিজস্ব কল স্ট্যাক প্রয়োজন, এবং JVM আগে থেকেই এর জন্য মেমোরি সংরক্ষণ করে। ডিফল্ট স্ট্যাক সাইজ প্ল্যাটফর্ম-নির্ভর, সাধারণত ৫১২ KB থেকে ১ MB প্রতি থ্রেড, যা -Xss ফ্ল্যাগ দিয়ে নিয়ন্ত্রণ করা যায়। থ্রেডটি সেই মেমোরির বেশিরভাগ কখনো ব্যবহার করুক বা না করুক, সংরক্ষিত মেমোরি থেকে যায়।

public class ThreadMemoryCost {
    public static void main(String[] args) throws InterruptedException {
        int count = 5_000;
        Thread[] threads = new Thread[count];
        for (int i = 0; i < count; i++) {
            threads[i] = new Thread(() -> {
                try {
                    Thread.sleep(60_000);
                } catch (InterruptedException ignored) {
                }
            });
            threads[i].start();
        }
        System.out.println("Created " + count + " live threads");
        Runtime rt = Runtime.getRuntime();
        System.out.printf("Used memory: %.1f MB%n",
                (rt.totalMemory() - rt.freeMemory()) / (1024.0 * 1024));
        for (Thread t : threads) {
            t.interrupt();
        }
    }
}

এই কোড ৫,০০০টি থ্রেড তৈরি করে, প্রতিটি ৬০ সেকেন্ড ঘুমিয়ে থাকে, তারপর ব্যবহৃত হিপ মেমোরি প্রিন্ট করে। চালালে দেখা যাবে নিষ্ক্রিয় থ্রেডের সংখ্যা বাড়ার সাথে সাথে মেমোরি ব্যবহার ক্রমাগত বাড়ছে। সীমিত হিপ বা OS-এর থ্রেড লিমিট থাকলে সংখ্যাটি যথেষ্ট বাড়ালে OutOfMemoryError: unable to create new native thread পাওয়া যাবে — JVM-এর হিপ ফুল হওয়ার কারণে নয়, বরং অপারেটিং সিস্টেম নেটিভ থ্রেডের জন্য রিসোর্স শেষ করে ফেলেছে বলে।

সময়: প্রতিটি সুইচের ওভারহেড

যখন OS একটি CPU কোরকে এক থ্রেড থেকে আরেক থ্রেড চালানোর জন্য সরিয়ে নেয়, তখন তাকে বর্তমান থ্রেডের রেজিস্টার ও প্রোগ্রাম কাউন্টার সংরক্ষণ করতে হয়, পরবর্তী থ্রেডের সংরক্ষিত অবস্থা লোড করতে হয়, এবং প্রায়ই আগের থ্রেডের ডেটার সাথে খাপ খাইয়ে নেওয়া CPU ক্যাশগুলো বাতিল করতে হয়। এই কনটেক্সট সুইচ বাস্তব সময় নেয়, সাধারণত কয়েক মাইক্রোসেকেন্ডের মধ্যে, এবং এটি অধিকাংশ ডেভেলপারের ধারণার চেয়ে অনেক বেশি ঘটে — যখনই কোনো থ্রেড I/O-তে ব্লক হয়, লকের জন্য অপেক্ষা করে, ঘুমায়, অথবা তার শিডিউলিং কোয়ান্টাম শেষ হয়ে যায়, তখনই সুইচ ঘটে।

একটি একক সুইচ সস্তা মনে হয়। সমস্যা হলো স্কেল: যদি CPU কোরের তুলনায় অনেক বেশি রানেবল থ্রেড থাকে, তাহলে শিডিউলার দরকারি কাজ করার বদলে থ্রেডগুলোর মধ্যে সুইচ করতেই সময়ের বড় অংশ ব্যয় করে, একে কখনো কখনো থ্র্যাশিং বলা হয়।

এই খরচগুলো কী নির্দেশ করে

প্রতি থ্রেডের মেমোরি খরচ এবং প্রতি সুইচের সময় — এই দুই খরচই পরবর্তী ধাপে আলোচিত দুটি ধারণার সম্পূর্ণ যৌক্তিকতা তৈরি করে: একবারে কতটি থ্রেড থাকবে তা সীমিত রাখতে প্রতি কাজের জন্য নতুন থ্রেড না বানিয়ে থ্রেড পুল ব্যবহার করা, এবং আধুনিক JVM-এ ভার্চুয়াল থ্রেড ব্যবহার করা, যেগুলো JVM নিজেই শিডিউল করে এবং প্ল্যাটফর্ম থ্রেডের তুলনায় তৈরি ও পার্ক করতে অনেক সস্তা। এই ধারণাগুলো তখনই অর্থবহ হয় যখন আপনি মেনে নেন যে একটি থ্রেড একটি বাস্তব, সীমিত ও মূল্যবান রিসোর্স — কোডকে “একই সময়ে” চালানোর কোনো বিনামূল্যের মাধ্যম নয়।

Share