রেস কন্ডিশন: টাইমিং-ই যখন সঠিকতা নির্ধারণ করে
একটি প্রোগ্রামের সঠিকতা যখন একাধিক থ্রেডের আপেক্ষিক সময় বা ইন্টারলিভিংয়ের ওপর নির্ভর করে, তখন সেখানে রেস কন্ডিশন থাকে। কোডটি বিচ্ছিন্নভাবে ভুল নয় — এক থ্রেডে চালালে এটি নিখুঁত আচরণ করে। এটি তখনই ভেঙে যায় যখন লেখক যে দুটি ধাপ একসাথে হবে বলে ধরে নিয়েছিলেন, তার মাঝে দ্বিতীয় একটি থ্রেড ঢুকে পড়ে।
ক্লাসিক ঘটনা: চেক-তারপর-অ্যাক্ট
রেস কন্ডিশনের সবচেয়ে সাধারণ রূপ হলো আগে শর্ত পরীক্ষা করা, তারপর কাজ করা; এই ফাঁকে আরেকটি থ্রেড ঢুকে পড়তে পারে। সিঙ্ক্রোনাইজেশন ছাড়া লেখা একটি লেজি সিঙ্গেলটন হলো পাঠ্যবইয়ের উদাহরণ:
public class UnsafeLazySingleton {
private static UnsafeLazySingleton instance;
private UnsafeLazySingleton() {
System.out.println("Constructing " + this);
}
public static UnsafeLazySingleton getInstance() {
if (instance == null) { // চেক
instance = new UnsafeLazySingleton(); // অ্যাক্ট
}
return instance;
}
}
এখানে instance == null চেক করার পরই instance-এ নতুন অবজেক্ট বসানো হয়। এক থ্রেডে getInstance() কেবল একটি অবজেক্টই তৈরি করে। দুই থ্রেড কাছাকাছি সময়ে কল করলে দুজনেই চেক পার হয়ে গিয়ে দুটি আলাদা অবজেক্ট তৈরি করতে পারে।
এই ত্রুটি সাধারণত তিন জায়গায় চোখে পড়ে না:
- আলাদা টেস্টিংয়ে
- কোড রিভিউতে
- সাধারণ এক-থ্রেড ব্যবহারে
এটি তখনই দেখা যায় যখন দুটি কল সত্যিই একই সময়ে চলে, আর সিঙ্গেল-থ্রেডেড টেস্ট সংজ্ঞা অনুযায়ী সেটা তৈরি করতে পারে না। নিচের RaceDemo ক্লাসটি একই getInstance() মেথডে দুইটি থ্রেড থেকে কল পাঠায়:
public class RaceDemo {
public static void main(String[] args) throws InterruptedException {
Runnable task = () -> System.out.println(UnsafeLazySingleton.getInstance());
Thread t1 = new Thread(task);
Thread t2 = new Thread(task);
t1.start();
t2.start();
t1.join();
t2.join();
}
}
এখানে t1 ও t2 দুটি থ্রেড একই কাজ করে: UnsafeLazySingleton.getInstance() কল করে অবজেক্টের পরিচিতি প্রিন্ট করে। এই কোডটি যথেষ্টবার চালালে মাঝে মাঝে দুটি ভিন্ন অবজেক্ট পরিচিতি বা দুইবার Constructing লাইন দেখতে পাওয়া যায়। এমনটা ঘটলে বোঝা যায় দুই থ্রেডই রেসে জিতেছে।
কেন বেশি টেস্ট করলে এটি ধরা পড়ে না
সিকোয়েনশিয়াল টেস্ট স্যুট একবারে একটি থ্রেড চালায়, তাই এটি সেই ইন্টারলিভিং তৈরি করতে পারে না, যেটি বাগ ঘটায়। একটি কনকারেন্ট টেস্টও পরপর কয়েকশবার পাস করতে পারে, কারণ চেক ও অ্যাক্টের মধ্যের অনিরাপদ উইন্ডোটি প্রায়ই মাত্র কয়েক ন্যানোসেকেন্ডের হয়। দুই থ্রেডকে প্রায় নিখুঁতভাবে একই সময়ে সেখানে পৌঁছাতে হয়।
সমাধান হলো ডিজাইন বদলানো, প্যাচ নয়
মূল সমস্যা হলো, অন্য প্রতিটি থ্রেডের দৃষ্টিকোণ থেকে "চেক" ও "অ্যাক্ট" একটিমাত্র অবিভাজ্য ধাপ হিসেবে ঘটতে হবে — অর্থাৎ অপারেশনটি হতে হবে অ্যাটমিক। এই সিরিজের পরের পর্বগুলো এর আসল টুল (synchronized, লক, অ্যাটমিক ক্লাস) নিয়ে আলোচনা করে। তবে এই নিবন্ধ থেকে যে মানসিক মডেলটি প্রথমে নিতে হবে সেটাই সবচেয়ে গুরুত্বপূর্ণ: একটি থ্রেড শেয়ার্ড স্টেট পড়ে, তারপর যা পড়েছে তার ভিত্তিতে শেয়ার্ড স্টেটে লেখে — এমন প্রতিটি চেক-তারপর-অ্যাক্ট সিকোয়েন্সই একটি রেস কন্ডিশন, যতক্ষণ না সিকোয়েন্সটিকে অ্যাটমিক করা হয়।