আগুনে-ভুলে-যাওয়া মডেল যথেষ্ট নয়
আগের ধাপে তুমি দেখেছ কিভাবে ExecutorService থ্রেড তৈরি ও পরিচালনার দায়িত্ব নিয়ে নেয়: তুমি একটি Runnable জমা দাও, পুল সেটিকে একটি ওয়ার্কার থ্রেডে চালায়, আর তুমি এগিয়ে যাও। এই মডেল তখনই ভালো কাজ করে যখন কাজের শুধু পার্শ্ব-প্রতিক্রিয়া থাকে — যেমন একটি কিউতে লেখা, শেয়ার্ড কাউন্টার আপডেট করা, লগ করা। কিন্তু বাস্তবের অনেক কাজের একটি মান তৈরি করতে হয়: ডাটাবেস থেকে রেকর্ড আনা, দূরবর্তী সার্ভিস কল করা, ফাইল পার্স করা। আগুনে-ভুলে-যাওয়া পদ্ধতিতে সেই মান রাখার কোনো জায়গা নেই।
জাভার উত্তর হলো Callable, যা Runnable-এর এক আত্মীয়। এর call() মেথড একটি মান ফেরত দেয় এবং চেকড এক্সেপশন ছুঁড়তে পারে:
Callable<Integer> countLines = () -> {
try (var lines = Files.lines(Path.of("data.txt"))) {
return (int) lines.count();
}
};
এই কোড একটি Callable তৈরি করে যা data.txt ফাইলের লাইন সংখ্যা গণনা করে। call() মেথড ইন্টিজার রিটার্ন করবে এবং প্রয়োজনে IOException ছুঁড়তে পারে, যা Runnable পারে না। এভাবেই অ্যাসিঙ্ক্রোনাস কাজের ফলাফল ফেরত পাওয়ার দরজা খোলে।
Callable জমা দিলে Future পাওয়া যায়
যখন তুমি কোনো ExecutorService-এ Callable জমা দাও, তখন ফলাফল তাৎক্ষণিকভাবে পাও না — কাজটি হয়তো শুরুই হয়নি। তার বদলে তুমি পাও একটি Future, যা এমন একটি হাতল যেটি ভবিষ্যতে বিদ্যমান হবে এমন একটি ফলাফলকে উপস্থাপন করে।
ExecutorService pool = Executors.newFixedThreadPool(4);
Future<Integer> future = pool.submit(countLines);
// ব্যাকগ্রাউন্ডে কাজ চলার সময় এখানে অন্য কাজ করো
int lineCount = future.get(); // ফলাফল প্রস্তুত না হওয়া পর্যন্ত ব্লক করে
pool.shutdown();
এখানে submit একটি Future<Integer> ফেরত দেয়, যা অ্যাসিঙ্ক্রোনাস কাজের প্রতিনিধিত্ব করে। future.get() কলটি কলিং থ্রেডকে ব্লক করে রাখে যতক্ষণ না কাজ শেষ হয় এবং ফলাফল পাওয়া যায়। কাজ শেষে পুল বন্ধ করা গুরুত্বপূর্ণ।
Future কিছু ছোট কিন্তু দরকারি অপারেশন দেয়: isDone() দিয়ে ব্লক না করে চেক করা যায় কাজ শেষ কি না, cancel() দিয়ে কাজ বাতিলের চেষ্টা করা যায়, এবং get() (টাইমআউট সহ বা ছাড়া) দিয়ে ফলাফল নেওয়া যায়। যদি কাজটি এক্সেপশন ছুঁড়ে থাকে, তবে get() সেটিকে ExecutionException-এ মুড়ে পুনরায় ছুঁড়ে দেয়, ফলে একটি মাত্র কল সাইটকে সফল ফলাফল আর ব্যর্থতা দুটোই সামলাতে প্রস্তুত থাকতে হয়।
try {
int lineCount = future.get(2, TimeUnit.SECONDS);
} catch (ExecutionException e) {
Throwable cause = e.getCause(); // আসল ব্যর্থতা এখানে
} catch (TimeoutException e) {
// ২ সেকেন্ড পরেও কাজ শেষ হয়নি
}
উপরের স্নিপেটটি দেখায় কিভাবে টাইমআউট-সহ get ব্যবহার করে। ExecutionException ধরলে তার ভেতর থেকে getCause() কল করে মূল এক্সেপশন বের করতে হয়, কারণ Future সেটিকে লুকিয়ে রাখে। টাইমআউট হলে TimeoutException ধরা পড়ে, যাতে থ্রেড অনির্দিষ্টকাল আটকে না থাকে।
পোলিং এবং ব্লকিং get() কেন যথেষ্ট নয়
মনে হতে পারে Future পুরো সমস্যার সমাধান করে দিয়েছে, কিন্তু এটি কেবল ফলাফলের জন্য অপেক্ষা করার পদ্ধতি দেয় — ফলাফলে প্রতিক্রিয়া জানানোর উপায় নয়। একটি মাত্র অনুরোধের চেয়ে বেশি কিছু বানাতে গেলে দ্রুত দুটি সীমাবদ্ধতা চোখে পড়ে।
get() কল কলিং থ্রেডকে কাজ শেষ না হওয়া পর্যন্ত ব্লক করে রাখে, অর্থাৎ যে থ্রেডটি "অ্যাসিঙ্ক্রোনাস" আচরণ চেয়েছিল সেটি এখন অপেক্ষাতেই আটকে গেল। যদি সেই থ্রেডটি কোনো সার্ভলেট থ্রেড বা UI থ্রেড হয়, তাহলে তুমি ঠিক সেই খরচটাই পুনরায় চালু করলে যা এড়াতে চেয়েছিলে।
এছাড়া কাজ শিকলবদ্ধ করার কোনো পরিষ্কার উপায় নেই। যদি তুমি একটি Future-এর ফলাফল নিয়ে সেটিকে আরেকটি অ্যাসিঙ্ক্রোনাস ধাপে পাঠাতে চাও, তাহলে Future-এ কোনো then-স্টাইল অপারেশন নেই — তোমাকে প্রথম ফলাফলের জন্য get() করে (ব্লক করে) তারপর দ্বিতীয় কাজ হাতে জমা দিতে হয়। দুটি স্বাধীন ফিউচার একত্র করাও একই রকম ম্যানুয়াল: তুমি একটি get() করো, তারপর আরেকটি, ফলে যে অপেক্ষাগুলো একসঙ্গে চলতে পারত সেগুলো সিরিয়াল হয়ে যায়।
Future<String> userFuture = pool.submit(() -> fetchUser(userId));
Future<List<Order>> ordersFuture = pool.submit(() -> fetchOrders(userId));
// সমস্যা: একটির পর আরেকটি ব্লক করে – আসলে সমান্তরাল অপেক্ষা নয়
String user = userFuture.get(); // এখানে ব্লক
List<Order> orders = ordersFuture.get(); // তারপর এখানে ব্লক
// দুটি ফলাফল একসঙ্গে প্রক্রিয়া
System.out.println(user + " has " + orders.size() + " orders");
pool.shutdown();
এই উদাহরণে দুটি স্বাধীন কল একই সময়ে চললেও, ফলাফল নেওয়ার সময় আমরা প্রথমে userFuture.get()-এ ব্লক করি, তারপর ordersFuture.get()-এ। অথচ দুটি ফিউচারের জন্যই একসঙ্গে অপেক্ষা করা সম্ভব ছিল, কিন্তু Future সেই সুবিধা দেয় না। এটিই বোঝায় যে শুধু Future থাকাই অ্যাসিঙ্ক্রোনাস প্রোগ্রামিংয়ের পূর্ণ সমাধান নয়।