নন-ডিটারমিনিজম: কেন কনকারেন্সি বাগ বারবার রিপ্রোডিউস হয় না
এই ধাপের আগের প্রতিটি লেখায় একটি নির্দিষ্ট ব্যর্থতা নিয়ে আলোচনা করা হয়েছে: রেস কন্ডিশন, ভিজিবিলিটি গ্যাপ, নন-অ্যাটমিক অপারেশন, ডেডলক, রিঅর্ডারিং হ্যাজার্ড। এই লেখাটি এমন একটি বৈশিষ্ট্য নিয়ে, যা ওই সবগুলো ব্যর্থতার মধ্যেই উপস্থিত: ত্রুটিপূর্ণ কোডটি চোখের সামনে সরাসরি থাকলেও এই বাগগুলো সাধারণ লজিক ত্রুটির চেয়ে অনেক বেশি কঠিন কেন।
শিডিউলার সিদ্ধান্ত নেয়, কিন্তু আগে থেকে জানায় না
একটি সিকোয়েনশিয়াল প্রোগ্রামে এক্সিকিউশন অর্ডার একটাই থাকে: কোড যা বলছে। অন্যদিকে কনকারেন্ট প্রোগ্রামে কয়েকটি থ্রেড থাকলে অনেকগুলো সম্ভাব্য এক্সিকিউশন অর্ডার তৈরি হয়। কারণ অপারেটিং সিস্টেমের শিডিউলার প্রতিটি থ্রেডের আলাদা ধাপ প্রায় যেকোনো ক্রমে ইন্টারলিভ করতে পারে।
দুটি থ্রেড, প্রতিটি তিনটি আনসিঙ্ক্রোনাইজড ধাপ করলে, ইতিমধ্যেই বেশ কিছু বৈধ ইন্টারলিভিং থাকে; বাস্তব প্রোগ্রামে তিনের বেশি ধাপ এবং দুইয়ের বেশি থ্রেড থাকে।
শিডিউলার প্রতি রানে একটি ইন্টারলিভিং বেছে নেয়। এই পছন্দের পেছনে আপনার প্রোগ্রামের বাইরের বিষয়গুলো কাজ করে: অন্য প্রসেসের সিপিইউ লোড, ক্যাশ স্টেট, গার্বেজ কালেকশন পজ, এমনকি জাভা ভার্চুয়াল মেশিন ঠিক কোন মিলিসেকেন্ডে শুরু হলো।
বেশিরভাগ ইন্টারলিভিং হয়তো ক্ষতিকর নয়। তবে সাধারণত একটি খারাপ ইন্টারলিভিং, যা খুব কম সময় ঘটে, ভুল ফলাফল তৈরি করার জন্য যথেষ্ট।
public class FlakyCounter {
static int total = 0;
public static void main(String[] args) throws InterruptedException {
for (int trial = 1; trial <= 20; trial++) {
total = 0;
Runnable add = () -> {
for (int i = 0; i < 100_000; i++) {
total++;
}
};
Thread t1 = new Thread(add);
Thread t2 = new Thread(add);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("Trial " + trial + ": total = " + total);
}
}
}
কোডটি ২০টি ট্রায়াল চালায়। প্রতিটি ট্রায়ালে দুটি থ্রেড total++ অপারেশন ১,০০,০০০ বার করে, তাই সঠিক ফলাফল হওয়া উচিত 200000। কিন্তু total++ একটি নন-অ্যাটমিক অপারেশন—এটি পড়া, মান বাড়ানো এবং লেখা এই ধাপে ঘটে। দুই থ্রেড একই মান পড়ে ফেললে একটি আপডেট হারিয়ে যায়, ফলে কিছু ট্রায়ালে 200000-এর চেয়ে কম সংখ্যা দেখা যায়।
কেন পাস করা টেস্ট মানে কোড সঠিক নয়
সিকোয়েনশিয়াল কোডে একবার টেস্ট চালানো মোটামুটি শক্তিশালী সংকেত দেয়, কারণ একই ইনপুট নির্ভরযোগ্যভাবে একই এক্সিকিউশন পথ তৈরি করে।
শেয়ার্ড মিউটেবল স্টেট ব্যবহার করা কনকারেন্ট কোডে টেস্ট চালানো হলো সম্ভাব্য ফলাফলের একটি ডিস্ট্রিবিউশন থেকে একটিমাত্র নমুনা, কোনো প্রমাণ নয়।
একটি বাগের ফেইলিউর রেট এত কম থাকতে পারে যে সেটি কয়েক মাস কন্টিনিউয়াস ইন্টিগ্রেশন স্যুটে পাস করে যায়। কিন্তু পরে কোডটি ব্যস্ত প্রোডাকশন সার্ভারে, ভিন্ন সংখ্যক সিপিইউ কোর, বা বেশি লোডের কারণে বদলে যাওয়া টাইমিংয়ে চললে খারাপ ইন্টারলিভিং বিরল না হয়ে সাধারণ হয়ে উঠতে পারে—তখন এটি নিয়মিত ফেইল করে।
কনকারেন্ট কোড নিয়ে কাজ করার ক্ষেত্রে এর মানে কী
এটি কনকারেন্ট কোড টেস্ট করার বিপক্ষে কোনো যুক্তি নয়; এটি সিকোয়েনশিয়াল কোডের মতো একটি পাস করা টেস্টকে বিশ্বাস করার বিপক্ষে যুক্তি। এখানে যেসব কৌশল বেশি গুরুত্ব পায়:
- কোড রিভিউ
- স্ট্রেস টেস্টিং
- স্ট্যাটিক অ্যানালাইসিস
- সম্ভাব্য ইন্টারলিভিং নিয়ে স্পষ্টভাবে চিন্তা করা