Android 3.0 এবং তার পরবর্তী প্ল্যাটফর্ম ভার্সনগুলি মাল্টিপ্রসেসর আর্কিটেকচারকে সাপোর্ট করার জন্য অপ্টিমাইজ করা হয়েছে। এই ডকুমেন্টটি C, C++ এবং Java প্রোগ্রামিং ল্যাঙ্গুয়েজে (সংক্ষিপ্ততার জন্য এর পরে কেবল "Java" হিসাবে উল্লেখ করা হয়েছে) সিমেট্রিক মাল্টিপ্রসেসর সিস্টেমের জন্য মাল্টিথ্রেডেড কোড লেখার সময় যে সমস্যাগুলি দেখা দিতে পারে তার সাথে পরিচয় করিয়ে দেয়। এটি Android অ্যাপ ডেভেলপারদের জন্য একটি প্রাইমার হিসেবে তৈরি করা হয়েছে, এই বিষয়ে সম্পূর্ণ আলোচনা হিসেবে নয়।
ভূমিকা
SMP হল “Symmetric Multi-Processor”-এর সংক্ষিপ্ত রূপ। এটি এমন একটি ডিজাইনকে বর্ণনা করে যেখানে দুই বা ততোধিক অভিন্ন CPU কোর মূল মেমরিতে অ্যাক্সেস শেয়ার করে। কয়েক বছর আগে পর্যন্ত, সব Android ডিভাইস UP (ইউনি-প্রসেসর) ছিল।
সব না হলেও বেশিরভাগ Android ডিভাইসে একাধিক CPU থাকে, কিন্তু আগে সেগুলির মধ্যে একটিই অ্যাপ্লিকেশন চালানোর জন্য ব্যবহার করা হত এবং বাকিগুলি ডিভাইসের বিভিন্ন হার্ডওয়্যার (যেমন, রেডিও) ম্যানেজ করত। CPU-গুলির বিভিন্ন আর্কিটেকচার থাকতে পারে এবং সেগুলিতে চলা প্রোগ্রামগুলি একে অপরের সাথে যোগাযোগ করার জন্য প্রধান মেমরি ব্যবহার করতে পারে না।
বর্তমানে বিক্রি হওয়া বেশিরভাগ Android ডিভাইস SMP ডিজাইনের উপর ভিত্তি করে তৈরি করা হয়, যার ফলে সফ্টওয়্যার ডেভেলপারদের জন্য কাজ করা একটু বেশি জটিল হয়ে যায়। মাল্টি-থ্রেডেড প্রোগ্রামে রেস কন্ডিশন ইউনিপ্রসেসরে দৃশ্যমান সমস্যা সৃষ্টি নাও করতে পারে, কিন্তু আপনার দুই বা ততোধিক থ্রেড বিভিন্ন কোরে একসাথে চললে নিয়মিত ব্যর্থ হতে পারে। এছাড়াও, বিভিন্ন প্রসেসর আর্কিটেকচারে বা একই আর্কিটেকচারের বিভিন্ন ইমপ্লিমেন্টেশনে রান করার সময় কোড কম বা বেশি ব্যর্থ হতে পারে। x86-এ পুঙ্খানুপুঙ্খভাবে পরীক্ষা করা কোড ARM-এ খারাপভাবে ভেঙে যেতে পারে। আরও আধুনিক কম্পাইলারের সাহায্যে রিকম্পাইল করা হলে কোড কাজ করা বন্ধ করে দিতে পারে।
এই ডকুমেন্টের বাকি অংশে এর কারণ ব্যাখ্যা করা হয়েছে এবং আপনার কোড যাতে সঠিকভাবে কাজ করে তা নিশ্চিত করতে আপনাকে কী করতে হবে তা বলা হয়েছে।
মেমরি কনসিস্টেন্সি মডেল: SMP কেন একটু আলাদা
এটি একটি জটিল বিষয়ের দ্রুত, চকচকে ওভারভিউ। কিছু ক্ষেত্র অসম্পূর্ণ থাকবে, তবে এর কোনওটিই বিভ্রান্তিকর বা ভুল হওয়া উচিত নয়। আপনি পরবর্তী বিভাগে দেখতে পাবেন যে, এখানে দেওয়া বিবরণ সাধারণত গুরুত্বপূর্ণ নয়।
এই বিষয়ে আরও বিস্তারিত জানতে ডকুমেন্টের শেষে আরও পড়ুন বিভাগটি দেখুন।
মেমরি কনসিস্টেন্সি মডেল বা প্রায়শই শুধু "মেমরি মডেল" মেমরি অ্যাক্সেস সম্পর্কে প্রোগ্রামিং ল্যাঙ্গুয়েজ বা হার্ডওয়্যার আর্কিটেকচারের গ্যারান্টি বর্ণনা করে। যেমন, আপনি যদি ঠিকানা A-তে কোনও ভ্যালু লেখেন এবং তারপরে ঠিকানা B-তে কোনও ভ্যালু লেখেন, তাহলে মডেলটি গ্যারান্টি দিতে পারে যে প্রতিটি CPU কোর সেই ক্রমে সেই লেখাগুলি দেখতে পাবে।
বেশিরভাগ প্রোগ্রামার যে মডেলের সাথে অভ্যস্ত সেটি হল সিকোয়েন্সিয়াল কনসিস্টেন্সি, যা এইভাবে বর্ণনা করা হয়েছে (Adve এবং Gharachorloo):
- সব মেমরি অপারেশন একবারে একটি করে এক্সিকিউট হয় বলে মনে হচ্ছে
- একটি থ্রেডের সব অপারেশন সেই প্রসেসরের প্রোগ্রাম দ্বারা বর্ণিত ক্রম অনুসারে সম্পন্ন হয়।
ধরা যাক, আমাদের কাছে একটি খুব সহজ কম্পাইলার বা ইন্টারপ্রেটার আছে যা কোনও চমক দেখায় না: এটি সোর্স কোডে অ্যাসাইনমেন্টকে লোড ও স্টোর করার নির্দেশাবলীতে ঠিক একই ক্রমে অনুবাদ করে, প্রতিটি অ্যাক্সেসের জন্য একটি করে নির্দেশাবলী। আমরা আরও ধরে নেব যে সহজভাবে প্রতিটি থ্রেড তার নিজস্ব প্রসেসরে এক্সিকিউট হয়।
আপনি যদি কোডের একটি অংশ দেখেন এবং দেখেন যে এটি মেমরি থেকে কিছু রিড এবং রাইট করে, একটি ক্রমানুসারে-সামঞ্জস্যপূর্ণ CPU আর্কিটেকচারে আপনি জানেন যে কোডটি প্রত্যাশিত ক্রমে সেই রিড এবং রাইট করবে। এটি সম্ভব যে CPU আসলে নির্দেশাবলী রিঅর্ডার করছে এবং রিড ও রাইট করতে দেরি করছে, কিন্তু ডিভাইসে চলা কোডের পক্ষে এটি বলা সম্ভব নয় যে CPU নির্দেশাবলী সোজাভাবে এক্সিকিউট করা ছাড়া অন্য কিছু করছে। (আমরা মেমরি-ম্যাপ করা ডিভাইস ড্রাইভার I/O উপেক্ষা করব।)
এইসব পয়েন্ট ব্যাখ্যা করার জন্য কোডের ছোট স্নিপেট বিবেচনা করা উপযোগী, যেগুলিকে সাধারণত লিটমাস টেস্ট বলা হয়।
এখানে একটি সহজ উদাহরণ দেওয়া হল, যেখানে দুটি থ্রেডে কোড রান করছে:
| থ্রেড ১ | থ্রেড ২ |
|---|---|
A = 3 |
reg0 = B |
এই এবং ভবিষ্যতের সব লিটমাস উদাহরণে, মেমরি লোকেশন বড় হাতের অক্ষর (A, B, C) দ্বারা প্রতিনিধিত্ব করা হয় এবং CPU রেজিস্টার "reg" দিয়ে শুরু হয়। সব মেমরি প্রাথমিকভাবে শূন্য থাকে। নির্দেশাবলী উপর থেকে নিচে কার্যকর করা হয়। এখানে, থ্রেড ১ লোকেশন A-তে ৩ ভ্যালু এবং তারপর লোকেশন B-তে ৫ ভ্যালু স্টোর করে। থ্রেড ২ লোকেশন B থেকে reg0-তে ভ্যালু লোড করে এবং তারপর লোকেশন A থেকে reg1-এ ভ্যালু লোড করে। (মনে রাখবেন যে আমরা এক ক্রমে লিখছি এবং অন্য ক্রমে পড়ছি।)
থ্রেড ১ ও থ্রেড ২ আলাদা CPU কোরে এক্সিকিউট করা হয় বলে ধরে নেওয়া হয়। মাল্টি-থ্রেডেড কোড সম্পর্কে চিন্তা করার সময় আপনার সবসময় এই অনুমান করা উচিত।
সিকোয়েন্সিয়াল কনসিস্টেন্সি গ্যারেন্টি দেয় যে, দুটি থ্রেডই এক্সিকিউট করা শেষ হওয়ার পরে, রেজিস্টার নিম্নলিখিত যেকোনও একটি অবস্থায় থাকবে:
| রেজিস্টার | রাজ্য |
|---|---|
| reg0=5, reg1=3 | সম্ভব (থ্রেড ১ প্রথমে রান করেছে) |
| reg0=0, reg1=0 | সম্ভব (থ্রেড ২ প্রথমে রান করেছে) |
| reg0=0, reg1=3 | সম্ভব (কনকারেন্ট এক্সিকিউশন) |
| reg0=5, reg1=0 | কখনই না |
A-তে স্টোর করার আগে B=5 দেখার মতো পরিস্থিতিতে পড়তে হলে, রিড বা রাইট অপারেশনকে ক্রমের বাইরে ঘটতে হবে। একটি সিকোয়েন্সিয়াল-কনসিস্টেন্ট মেশিনে, এটি ঘটতে পারে না।
x86 এবং ARM সহ ইউনি-প্রসেসরগুলি সাধারণত ক্রমানুসারে সামঞ্জস্যপূর্ণ। OS কার্নেল থ্রেডগুলির মধ্যে সুইচ করার কারণে, থ্রেডগুলি ইন্টারলিভড ফ্যাশনে এক্সিকিউট হয় বলে মনে হয়। x86 এবং ARM সহ বেশিরভাগ SMP সিস্টেম, ধারাবাহিকভাবে সামঞ্জস্যপূর্ণ নয়। যেমন, হার্ডওয়্যার মেমরিতে স্টোর করার সময় বাফার করা সাধারণ ব্যাপার, যাতে সেগুলি সাথে সাথে মেমরিতে না পৌঁছায় এবং অন্যান্য কোরে দেখা যায়।
বিবরণগুলি যথেষ্ট আলাদা। যেমন, x86, ক্রমানুসারে সামঞ্জস্যপূর্ণ না হলেও, এখনও গ্যারেন্টি দেয় যে reg0 = 5 এবং reg1 = 0 অসম্ভব থেকে যায়। স্টোর বাফার করা হলেও, তাদের অর্ডার বজায় রাখা হয়। অন্যদিকে, ARM-এ তা হয় না। বাফার করা স্টোরের ক্রম বজায় রাখা হয় না এবং স্টোরগুলি একই সময়ে অন্য সব কোরে নাও পৌঁছাতে পারে। এই পার্থক্যগুলি অ্যাসেম্বলি প্রোগ্রামারদের জন্য গুরুত্বপূর্ণ। তবে, আমরা নিচে দেখতে পাব যে, C, C++ বা Java প্রোগ্রামাররা এমনভাবে প্রোগ্রাম করতে পারেন এবং করা উচিত যা এই ধরনের আর্কিটেকচারাল পার্থক্য লুকিয়ে রাখে।
এখনও পর্যন্ত, আমরা অবাস্তবভাবে ধরে নিয়েছি যে শুধুমাত্র হার্ডওয়্যারই নির্দেশাবলীকে নতুন করে সাজায়। বাস্তবে, পারফর্ম্যান্স উন্নত করতে কম্পাইলারও নির্দেশাবলীকে পুনরায় সাজায়। আমাদের উদাহরণে, কম্পাইলার হয়ত সিদ্ধান্ত নিতে পারে যে থ্রেড ২-এর কিছু পরবর্তী কোডের জন্য reg0-এর আগে reg1-এর ভ্যালু প্রয়োজন, এবং তাই প্রথমে reg1 লোড করে। অথবা আগে থেকে থাকা কিছু কোড হয়ত ইতিমধ্যেই A লোড করেছে এবং কম্পাইলার A আবার লোড করার পরিবর্তে সেই মানটি পুনরায় ব্যবহার করার সিদ্ধান্ত নিতে পারে। দুই ক্ষেত্রেই, reg0 এবং reg1-এ লোড আবার অর্ডার করা হতে পারে।
হার্ডওয়্যার বা কম্পাইলার, যেখানেই হোক না কেন, বিভিন্ন মেমরি লোকেশনে অ্যাক্সেস রিঅর্ডার করার অনুমতি দেওয়া হয়, কারণ এটি কোনও একটি থ্রেড এক্সিকিউট করার ক্ষেত্রে প্রভাব ফেলে না এবং এটি পারফর্ম্যান্স উল্লেখযোগ্যভাবে উন্নত করতে পারে। আমরা দেখব যে, একটু যত্ন সহকারে, আমরা মাল্টিথ্রেডেড প্রোগ্রামের ফলাফলকে প্রভাবিত করা থেকেও এটিকে আটকাতে পারি।
যেহেতু কম্পাইলারও মেমরি অ্যাক্সেস রিঅর্ডার করতে পারে, তাই এই সমস্যাটি আসলে SMP-এর জন্য নতুন নয়। এমনকি ইউনিপ্রসেসরেও, কম্পাইলার আমাদের উদাহরণে reg0 এবং reg1-এ লোডগুলি রিঅর্ডার করতে পারে এবং রিঅর্ডার করা নির্দেশাবলীর মধ্যে থ্রেড 1 শিডিউল করা যেতে পারে। কিন্তু আমাদের কম্পাইলার যদি রিঅর্ডার না করত, তাহলে আমরা কখনও এই সমস্যাটি লক্ষ্য করতাম না। বেশিরভাগ ARM SMP-তে, কম্পাইলার রিঅর্ডারিং ছাড়াই, রিঅর্ডারিং সম্ভবত দেখা যাবে, সম্ভবত খুব বেশি সংখ্যক সফল এক্সিকিউশনের পরে। আপনি অ্যাসেম্বলি ভাষায় প্রোগ্রামিং না করলে, SMP সাধারণত এমন সমস্যাগুলি দেখায় যেগুলি আগে থেকেই ছিল।
ডেটা-রেস-ফ্রি প্রোগ্রামিং
সৌভাগ্যবশত, এইসব বিবরণ নিয়ে চিন্তা না করার একটি সহজ উপায় সাধারণত আছে। আপনি যদি কিছু সহজ নিয়ম মেনে চলেন, তাহলে "সিকোয়েন্সিয়াল কনসিস্টেন্সি" অংশ ছাড়া আগের সব বিভাগ ভুলে যাওয়া সাধারণত নিরাপদ। আপনি ভুলবশত সেইসব নিয়ম লঙ্ঘন করলে, অন্যান্য জটিলতা দেখা দিতে পারে।
আধুনিক প্রোগ্রামিং ল্যাঙ্গুয়েজ "ডেটা-রেস-ফ্রি" প্রোগ্রামিং স্টাইলকে উৎসাহিত করে। যতক্ষণ আপনি "ডেটা রেস" না করার প্রতিশ্রুতি দিচ্ছেন, এবং কম্পাইলারকে অন্য কিছু বলে এমন কয়েকটি কনস্ট্রাক্ট এড়িয়ে চলছেন, ততক্ষণ কম্পাইলার এবং হার্ডওয়্যার ক্রমানুসারে সামঞ্জস্যপূর্ণ ফলাফল প্রদান করার প্রতিশ্রুতি দেয়। এর মানে এই নয় যে তারা মেমরি অ্যাক্সেস রিঅর্ডারিং এড়িয়ে যায়। এর অর্থ হল, আপনি নিয়ম মেনে চললে, মেমরি অ্যাক্সেস যে রিঅর্ডার করা হচ্ছে তা আপনি বলতে পারবেন না। এটি অনেকটা এইরকম যে, সসেজ একটি সুস্বাদু এবং ক্ষুধাবর্ধক খাবার, যতক্ষণ না আপনি সসেজ ফ্যাক্টরি পরিদর্শন না করার প্রতিশ্রুতি দিচ্ছেন। ডেটা রেস হল এমন একটি বিষয় যা মেমরি রিঅর্ডারিং সম্পর্কে কুৎসিত সত্যকে প্রকাশ করে।
"ডেটা রেস" কী?
কমপক্ষে দুটি থ্রেড একই সাধারণ ডেটা একই সাথে অ্যাক্সেস করলে এবং তাদের মধ্যে কমপক্ষে একটি এটি পরিবর্তন করলে, ডেটা রেস ঘটে। "সাধারণ ডেটা" বলতে আমরা এমন কিছু বুঝি যা থ্রেড কমিউনিকেশনের জন্য বিশেষভাবে সিঙ্ক্রোনাইজেশন অবজেক্ট নয়। মিউটেক্স, কন্ডিশন ভেরিয়েবল, Java ভোলাটাইল বা C++ অ্যাটমিক অবজেক্ট সাধারণ ডেটা নয় এবং এগুলির অ্যাক্সেস রেস করার অনুমতি দেওয়া হয়। আসলে, অন্যান্য অবজেক্টে ডেটা রেস প্রতিরোধ করতে এগুলি ব্যবহার করা হয়।
দুটি থ্রেড একই মেমরি লোকেশন একই সাথে অ্যাক্সেস করছে কিনা তা নির্ধারণ করতে, আমরা উপরে মেমরি-রিঅর্ডারিং সংক্রান্ত আলোচনা উপেক্ষা করতে পারি এবং
সিকোয়েন্সিয়াল কনসিস্টেন্সি অনুমান করতে পারি। নিম্নলিখিত প্রোগ্রামে ডেটা রেস নেই
যদি A এবং B সাধারণ বুলিয়ান ভেরিয়েবেল হয় যেগুলি
প্রাথমিকভাবে মিথ্যা:
| থ্রেড ১ | থ্রেড ২ |
|---|---|
if (A) B = true |
if (B) A = true |
যেহেতু অপারেশনগুলি পুনরায় সাজানো হয় না, তাই উভয় শর্তই মিথ্যা হিসাবে মূল্যায়ন করবে এবং
কোনও ভেরিয়েবল কখনও আপডেট করা হয় না। তাই ডেটা রেস হওয়ার কোনও সম্ভাবনা নেই। থ্রেড ১-এ A থেকে লোড করা এবং B-এ স্টোর করার বিষয়টি কোনওভাবে রিঅর্ডার করা হলে কী হতে পারে তা নিয়ে ভাবার
প্রয়োজন নেই। কম্পাইলারকে থ্রেড ১-এর ক্রম পরিবর্তন করার অনুমতি দেওয়া হয় না "B = true; if (!A) B = false" হিসেবে এটি আবার লিখে। এটি হবে
শহরের মাঝখানে দিনের আলোয় সসেজ তৈরি করার মতো।
ডেটা রেস আনুষ্ঠানিকভাবে ইন্টিজার এবং রেফারেন্স বা পয়েন্টারের মতো বেসিক বিল্ট-ইন টাইপের উপর সংজ্ঞায়িত করা হয়। একই সাথে অন্য থ্রেডে পড়ার সময় int-এ অ্যাসাইন করা
স্পষ্টতই একটি ডেটা রেস। কিন্তু C++ স্ট্যান্ডার্ড লাইব্রেরি এবং Java কালেকশন লাইব্রেরি, দুটিই এমনভাবে লেখা হয়েছে যাতে আপনি লাইব্রেরি লেভেলে ডেটা রেস সম্পর্কেও যুক্তি দিতে পারেন। তারা প্রতিশ্রুতি দেয় যে ডেটা রেস হবে না,
যদি না একই কন্টেনারে কনকারেন্ট অ্যাক্সেস থাকে, যার মধ্যে অন্তত একটি
এটি আপডেট করে। একটি থ্রেডে set<T> আপডেট করার সময়
অন্য একটি থ্রেডে সেটি একইসাথে পড়ার ফলে লাইব্রেরিতে ডেটা রেস
তৈরি হয় এবং এটিকে অনানুষ্ঠানিকভাবে "লাইব্রেরি-লেভেল ডেটা রেস" বলা যেতে পারে।
বিপরীতভাবে, একটি থ্রেডে একটি set<T> আপডেট করা, অন্য একটি থ্রেডে অন্য একটি পড়ার সময়, ডেটা রেসের ফলাফল দেয় না, কারণ
লাইব্রেরি সেই ক্ষেত্রে একটি (নিম্ন-স্তরের) ডেটা রেস প্রবর্তন না করার প্রতিশ্রুতি দেয়।
সাধারণত ডেটা স্ট্রাকচারের বিভিন্ন ফিল্ডে একই সাথে অ্যাক্সেস করা হলে ডেটা রেস হয় না। তবে এই নিয়মের একটি গুরুত্বপূর্ণ ব্যতিক্রম আছে: C বা C++-এ বিট-ফিল্ডের সংলগ্ন সিকোয়েন্সকে একটি "মেমরি লোকেশন" হিসেবে বিবেচনা করা হয়। এই ধরনের সিকোয়েন্সে যেকোনও বিট-ফিল্ড অ্যাক্সেস করাকে ডেটা রেসের অস্তিত্ব নির্ধারণের উদ্দেশ্যে সেগুলির সবকটি অ্যাক্সেস করা হিসেবে বিবেচনা করা হয়। এর থেকে বোঝা যায় যে সাধারণ হার্ডওয়্যার সংলগ্ন বিট না পড়ে এবং আবার না লিখে স্বতন্ত্র বিট আপডেট করতে পারে না। জাভা প্রোগ্রামারদের এই ধরনের কোনও সমস্যা নেই।
ডেটা রেস এড়ানো
আধুনিক প্রোগ্রামিং ল্যাঙ্গুয়েজ ডেটা রেস এড়াতে একাধিক সিঙ্ক্রোনাইজেশন মেকানিজম প্রদান করে। সবচেয়ে প্রাথমিক টুলগুলি হল:
- লক বা মিউটেক্স
- Mutexes (C++11
std::mutex, অথবাpthread_mutex_t), অথবা Java-তেsynchronizedব্লক ব্যবহার করে নিশ্চিত করা যেতে পারে যে কোডের নির্দিষ্ট অংশ একই ডেটা অ্যাক্সেস করা কোডের অন্যান্য অংশের সাথে একই সময়ে রান করবে না। আমরা এই এবং অন্যান্য একই ধরনের সুবিধাগুলিকে সাধারণভাবে "লক" হিসেবে উল্লেখ করব। শেয়ার করা ডেটা স্ট্রাকচার অ্যাক্সেস করার আগে নির্দিষ্ট লক ধারাবাহিকভাবে অ্যাকোয়ার করা এবং পরে তা রিলিজ করা, ডেটা স্ট্রাকচার অ্যাক্সেস করার সময় ডেটা রেস প্রতিরোধ করে। এছাড়াও, এটি নিশ্চিত করে যে আপডেট এবং অ্যাক্সেসগুলি অ্যাটমিক, অর্থাৎ ডেটা স্ট্রাকচারে অন্য কোনও আপডেট মাঝখানে চলতে পারে না। এটি ডেটা রেস প্রতিরোধ করার জন্য এখনও পর্যন্ত সবচেয়ে সাধারণ টুল। Javasynchronizedব্লক বা C++lock_guardবাunique_lock-এর ব্যবহার নিশ্চিত করে যে ব্যতিক্রমের ক্ষেত্রে লক সঠিকভাবে রিলিজ করা হয়েছে। - ভোলাটাইল/অ্যাটমিক ভেরিয়েবল
- Java
volatileটি ফিল্ড প্রদান করে যা ডেটা রেস ছাড়াই কনকারেন্ট অ্যাক্সেসকে সমর্থন করে। ২০১১ সাল থেকে, C এবং C++ একই ধরনের শব্দার্থ সহatomicভেরিয়েবল এবং ফিল্ডকে সাপোর্ট করে। এগুলি সাধারণত লকের চেয়ে ব্যবহার করা কঠিন, কারণ এগুলি শুধু নিশ্চিত করে যে একটি ভেরিয়েবলে স্বতন্ত্র অ্যাক্সেস অ্যাটমিক। (C++-এ এটি সাধারণত ইনক্রিমেন্টের মতো সহজ রিড-মডিফাই-রাইট অপারেশনে প্রসারিত হয়। Java-তে এর জন্য বিশেষ মেথড কল প্রয়োজন হয়।) লকের মতো,volatileবাatomicভেরিয়েবল সরাসরি ব্যবহার করা যায় না যাতে অন্যান্য থ্রেড দীর্ঘ কোড সিকোয়েন্সে হস্তক্ষেপ করতে না পারে।
মনে রাখবেন, C++ এবং Java-তে volatile-এর অর্থ
একেবারে আলাদা। C++-এ, volatile ডেটা রেস প্রতিরোধ করে না, যদিও পুরনো কোড প্রায়ই এটিকে atomic অবজেক্টের অভাবের জন্য একটি সমাধান হিসেবে ব্যবহার করে। এটি আর সাজেস্ট করা হয় না;
C++-এ, একাধিক থ্রেড দ্বারা একই সাথে অ্যাক্সেস করা যেতে পারে এমন ভেরিয়েবলের জন্য atomic<T> ব্যবহার করুন। C++ volatile-এর অর্থ হল
ডিভাইস রেজিস্টার এবং এই ধরনের অন্যান্য।
অন্যান্য ভেরিয়েবলে ডেটা রেস প্রতিরোধ করতে
C/C++ atomic ভেরিয়েবল বা Java volatile ভেরিয়েবল ব্যবহার করা যেতে পারে। flag-এর
টাইপ atomic<bool>
বা atomic_bool(C/C++) বা volatile boolean (Java) হিসেবে ঘোষণা করা হলে,
এবং প্রাথমিকভাবে false হলে, নিম্নলিখিত স্নিপেটটি ডেটা-রেস-ফ্রি:
| থ্রেড ১ | থ্রেড ২ |
|---|---|
A = ...
|
while (!flag) {}
|
যেহেতু থ্রেড ২ flag সেট হওয়ার জন্য অপেক্ষা করে, তাই থ্রেড ২-এ A-এর অ্যাক্সেস
থ্রেড ১-এ A-এর অ্যাসাইনমেন্টের পরে হতে হবে, একই সাথে নয়। তাই A-এ কোনও ডেটা রেস নেই। flag-এ রেসকে ডেটা রেস হিসেবে গণ্য করা হয় না,
কারণ ভোলাটাইল/অ্যাটমিক অ্যাক্সেস "সাধারণ মেমরি অ্যাক্সেস" নয়।
মেমরি রিঅর্ডারিং প্রতিরোধ বা লুকানোর জন্য ইমপ্লিমেন্টেশন প্রয়োজন যাতে পূর্ববর্তী লিটমাস টেস্টের মতো কোড প্রত্যাশিতভাবে আচরণ করে। এটি সাধারণত ভোলাটাইল/অ্যাটমিক মেমরি অ্যাক্সেসকে সাধারণ অ্যাক্সেসের চেয়ে যথেষ্ট বেশি ব্যয়বহুল করে তোলে।
যদিও পূর্ববর্তী উদাহরণটি ডেটা-রেস-ফ্রি, Java-তে Object.wait()-এর সাথে লক বা C/C++-এ কন্ডিশন ভেরিয়েবল সাধারণত
আরও ভালো সমাধান প্রদান করে, যার মধ্যে ব্যাটারির পাওয়ার শেষ করার সময় লুপে অপেক্ষা করার প্রয়োজন হয় না।
যখন মেমরি রিঅর্ডার করা দেখা যায়
ডেটা-রেস-ফ্রি প্রোগ্রামিং সাধারণত আমাদের মেমরি অ্যাক্সেস রিঅর্ডারিং সংক্রান্ত সমস্যা আলাদাভাবে সমাধান করার হাত থেকে বাঁচায়। তবে, এমন কিছু পরিস্থিতি আছে যেখানে পুনর্বিন্যাস করা দেখা যায়:- আপনার প্রোগ্রামে যদি এমন কোনও বাগ থাকে যার ফলে অনিচ্ছাকৃত ডেটা রেস হয়,
তাহলে কম্পাইলার এবং হার্ডওয়্যার ট্রান্সফর্মেশন দৃশ্যমান হতে পারে এবং আপনার প্রোগ্রামের
আচরণ আশ্চর্যজনক হতে পারে। যেমন, আমরা যদি আগের উদাহরণে
flagvolatile ঘোষণা করতে ভুলে যাই, তাহলে থ্রেড ২ একটি uninitializedAদেখতে পারে। অথবা কম্পাইলার সিদ্ধান্ত নিতে পারে যে থ্রেড ২-এর লুপ চলাকালীন ফ্ল্যাগ পরিবর্তন করা সম্ভব নয় এবং প্রোগ্রামকে এতে রূপান্তরিত করতে পারে
আপনি যখন ডিবাগ করেন, তখন আপনি দেখতে পাবেন যেথ্রেড ১ থ্রেড ২ A = ...
flag = truereg0 = flag; while (!reg0) {}
... = Aflagসত্য হওয়া সত্ত্বেও লুপটি চিরকাল চলতে থাকে। - C++ provides facilities for explicitly relaxing
sequential consistency even if there are no races. অ্যাটমিক অপারেশন
স্পষ্ট
memory_order_... আর্গুমেন্ট নিতে পারে। একইভাবে,java.util.concurrent.atomicপ্যাকেজ আরও সীমাবদ্ধ একই ধরনের সুবিধা প্রদান করে, বিশেষ করেlazySet()। এবং Java প্রোগ্রামাররা মাঝে মাঝে একই ধরনের প্রভাবের জন্য ইচ্ছাকৃত ডেটা রেস ব্যবহার করে। এইসব পদ্ধতি পারফর্ম্যান্স উন্নত করলেও, প্রোগ্রামিংয়ের জটিলতা অনেক বাড়িয়ে দেয়। আমরা নিচে সেগুলি সম্পর্কে সংক্ষেপে আলোচনা করি। - কিছু C এবং C++ কোড পুরনো স্টাইলে লেখা হয়, যা বর্তমান ভাষার স্ট্যান্ডার্ডের সাথে
সম্পূর্ণ সামঞ্জস্যপূর্ণ নয়, যেখানে
atomic-এর পরিবর্তেvolatileভেরিয়েবল ব্যবহার করা হয় এবং তথাকথিত ফেন্স বা ব্যারিয়ার যোগ করে মেমরি অর্ডারিং স্পষ্টভাবে নিষিদ্ধ করা হয়। This requires explicit reasoning about access reordering and understanding of hardware memory models. এই ধরনের কোডিং স্টাইল এখনও Linux কার্নেলে ব্যবহার করা হয়। এটি নতুন Android অ্যাপ্লিকেশনে ব্যবহার করা উচিত নয় এবং এখানে আরও আলোচনা করা হয়নি।
অনুশীলন
মেমরি কনসিস্টেন্সি সংক্রান্ত সমস্যার ডিবাগিং করা খুব কঠিন হতে পারে। যদি কোনও অনুপস্থিত
lock, atomic বা volatile ডিক্লারেশনের কারণে
কিছু কোড পুরনো ডেটা পড়ে, তাহলে আপনি হয়ত
ডিবাগার দিয়ে মেমরি ডাম্প পরীক্ষা করে কারণটি খুঁজে বের করতে পারবেন না। আপনি ডিবাগার কোয়েরি ইস্যু করার সময়, CPU কোর হয়ত অ্যাক্সেসের সম্পূর্ণ সেট পর্যবেক্ষণ করে ফেলেছে এবং মেমরি ও CPU রেজিস্টারের কন্টেন্ট "অসম্ভব" অবস্থায় আছে বলে মনে হবে।
C-তে কী করা উচিত নয়
এখানে আমরা ভুল কোডের কিছু উদাহরণ এবং সেগুলি ঠিক করার সহজ উপায় উপস্থাপন করছি। তবে তার আগে, আমাদের একটি বেসিক ভাষা ফিচার ব্যবহার করার বিষয়ে আলোচনা করতে হবে।
C/C++ এবং "volatile"
C এবং C++ volatile ডিক্লারেশন হল একটি বিশেষ উদ্দেশ্যে ব্যবহৃত টুল।
এগুলি কম্পাইলারকে ভোলাটাইল অ্যাক্সেস
পুনরায় সাজানো বা সরানো থেকে আটকায়। এটি হার্ডওয়্যার ডিভাইস রেজিস্টার অ্যাক্সেস করা কোড, একাধিক লোকেশনে ম্যাপ করা মেমরি অথবা setjmp-এর সাথে কানেকশনের ক্ষেত্রে সহায়ক হতে পারে। কিন্তু C এবং C++ volatile, Java-র মতো নয়
volatile, থ্রেড কমিউনিকেশনের জন্য ডিজাইন করা হয়নি।
C এবং C++-এ, volatile
ডেটার অ্যাক্সেস নন-ভোলাটাইল ডেটার অ্যাক্সেসের সাথে রিঅর্ডার করা হতে পারে এবং কোনও
অ্যাটোমিসিটি গ্যারান্টি নেই। তাই, পোর্টেবল কোডে থ্রেডের মধ্যে ডেটা শেয়ার করার জন্য volatile ব্যবহার করা যাবে না,
এমনকি ইউনিপ্রসেসরেও নয়। C volatile সাধারণত হার্ডওয়্যার দ্বারা অ্যাক্সেস রিঅর্ডারিং আটকায় না, তাই মাল্টি-থ্রেডেড SMP এনভায়রনমেন্টে এটি নিজে থেকে আরও কম উপযোগী। এই কারণেই C11 এবং C++11-এ
atomic অবজেক্ট কাজ করে। পরিবর্তে আপনার সেগুলি ব্যবহার করা উচিত।
অনেক পুরনো C এবং C++ কোড এখনও থ্রেড কমিউনিকেশনের জন্য volatile-এর অপব্যবহার করে। এটি প্রায়শই মেশিন রেজিস্টারে ফিট হওয়া ডেটার জন্য সঠিকভাবে কাজ করে, তবে এটি এক্সপ্লিসিট ফেন্সের সাথে অথবা মেমরি অর্ডারিং গুরুত্বপূর্ণ নয় এমন ক্ষেত্রে ব্যবহার করা হলে। তবে, এটি ভবিষ্যতের কম্পাইলারের সাথে সঠিকভাবে কাজ করবে
কিনা তার কোনও গ্যারান্টি নেই।
উদাহরণ
বেশিরভাগ ক্ষেত্রে, আপনি অ্যাটমিক অপারেশনের পরিবর্তে লক (যেমন
pthread_mutex_t বা C++11 std::mutex) ব্যবহার করলে ভালো হয়, তবে আমরা পরেরটি ব্যবহার করে দেখাব যে কীভাবে
সেগুলি বাস্তব পরিস্থিতিতে ব্যবহার করা যেতে পারে।
MyThing* gGlobalThing = NULL; // Wrong! See below. void initGlobalThing() // runs in Thread 1 { MyStruct* thing = malloc(sizeof(*thing)); memset(thing, 0, sizeof(*thing)); thing->x = 5; thing->y = 10; /* initialization complete, publish */ gGlobalThing = thing; } void useGlobalThing() // runs in Thread 2 { if (gGlobalThing != NULL) { int i = gGlobalThing->x; // could be 5, 0, or uninitialized data ... } }
এখানে আইডিয়া হল, আমরা একটি স্ট্রাকচার বরাদ্দ করি, এর ফিল্ডগুলি শুরু করি এবং একেবারে শেষে আমরা এটিকে একটি গ্লোবাল ভেরিয়েবেলে স্টোর করে "প্রকাশ" করি। সেই সময়, অন্য যেকোনও থ্রেড এটি দেখতে পাবে, কিন্তু এটি সম্পূর্ণভাবে শুরু হয়ে গেছে বলে এতে কোনও সমস্যা নেই, তাই তো?
সমস্যা হল, ফিল্ড শুরু করার আগে gGlobalThing-এ স্টোর করা ডেটা দেখা যেতে পারে, সাধারণত কম্পাইলার বা প্রসেসর gGlobalThing এবং thing->x-এ স্টোর করা ডেটা রিঅর্ডার করার কারণে এটি হয়। thing->x থেকে রিড করা অন্য থ্রেড
৫, ০ বা এমনকি আনইনিশিয়ালাইজড ডেটাও দেখতে পারে।
এখানে মূল সমস্যা হল gGlobalThing-এ ডেটা রেস।
থ্রেড ১ যদি initGlobalThing()-কে কল করে এবং থ্রেড ২ যদি
useGlobalThing()-কে কল করে, তাহলে লেখার সময় gGlobalThing
পড়া যেতে পারে।
gGlobalThing-কে অ্যাটমিক হিসেবে ঘোষণা করে এই সমস্যার সমাধান করা যেতে পারে। C++11-এ:
atomic<MyThing*> gGlobalThing(NULL);
এটি নিশ্চিত করে যে লেখাগুলি অন্যান্য থ্রেডে সঠিক ক্রমে দৃশ্যমান হবে। এছাড়াও, এটি অন্য কিছু ব্যর্থতার মোড প্রতিরোধ করার গ্যারান্টি দেয় যা অন্যথায় অনুমোদিত, কিন্তু আসল Android হার্ডওয়্যারে ঘটার সম্ভাবনা নেই। যেমন, এটি নিশ্চিত করে যে আমরা এমন কোনও
gGlobalThing পয়েন্টার দেখতে পাব না যা আংশিকভাবে লেখা হয়েছে।
জাভাতে কী করা উচিত নয়
আমরা কিছু প্রাসঙ্গিক Java ল্যাঙ্গুয়েজ ফিচার নিয়ে আলোচনা করিনি, তাই আমরা প্রথমে সেগুলি একবার দেখে নেব।
টেকনিক্যালি, Java-তে কোড ডেটা-রেস-ফ্রি হওয়ার প্রয়োজন নেই। এবং সেখানে খুব অল্প পরিমাণে খুব সাবধানে লেখা জাভা কোড আছে যা ডেটা রেসের উপস্থিতিতে সঠিকভাবে কাজ করে। তবে, এই ধরনের কোড লেখা অত্যন্ত কঠিন এবং আমরা নিচে এই বিষয়ে সংক্ষেপে আলোচনা করেছি। পরিস্থিতি আরও খারাপ করতে, যে বিশেষজ্ঞরা এই ধরনের কোডের অর্থ নির্দিষ্ট করেছেন তারা আর বিশ্বাস করেন না যে স্পেসিফিকেশন সঠিক। (ডেটা-রেস-ফ্রি কোডের জন্য স্পেসিফিকেশন ঠিক আছে।)
আপাতত আমরা ডেটা-রেস-ফ্রি মডেল মেনে চলব, যার জন্য Java মূলত C এবং C++-এর মতো একই গ্যারান্টি প্রদান করে। আবার, ভাষাটি কিছু প্রিমিটিভ প্রদান করে যা স্পষ্টভাবে সিকোয়েন্সিয়াল কনসিস্টেন্সি শিথিল করে, বিশেষ করে java.util.concurrent.atomic-এ lazySet() এবং weakCompareAndSet() কল।
C এবং C++-এর মতো, আমরা আপাতত এগুলি উপেক্ষা করব।
Java-র "synchronized" ও "volatile" কীওয়ার্ড
"synchronized" কীওয়ার্ড Java ভাষার ইন-বিল্ট লকিং মেকানিজম প্রদান করে। প্রতিটি অবজেক্টের সাথে একটি "মনিটর" যুক্ত থাকে যা পারস্পরিকভাবে এক্সক্লুসিভ অ্যাক্সেস প্রদান করতে ব্যবহার করা যেতে পারে। দুটি থ্রেড একই অবজেক্টে "সিঙ্ক" করার চেষ্টা করলে, একটি থ্রেড অন্যটি সম্পূর্ণ না হওয়া পর্যন্ত অপেক্ষা করবে।
আমরা উপরে যেমন উল্লেখ করেছি, Java-এর volatile T হল
C++11-এর atomic<T>-এর অ্যানালগ। volatile ফিল্ডে
একসাথে অ্যাক্সেস করার অনুমতি দেওয়া হয় এবং এর ফলে ডেটা রেস হয় না।
lazySet() ইত্যাদি এবং ডেটা রেস উপেক্ষা করে, ফলাফল এখনও ক্রমানুসারে সামঞ্জস্যপূর্ণ দেখাচ্ছে কিনা তা নিশ্চিত করা
Java VM-এর কাজ।
বিশেষ করে, থ্রেড ১ যদি volatile ফিল্ডে লেখে এবং
থ্রেড ২ পরে একই ফিল্ড থেকে পড়ে এবং নতুন লেখা
ভ্যালু দেখে, তাহলে থ্রেড ২-এর ক্ষেত্রেও থ্রেড ১-এর আগে করা সমস্ত লেখা দেখার
গ্যারান্টি থাকে। মেমরি এফেক্টের ক্ষেত্রে, ভোলাটাইল ভেরিয়েবলে
লেখা হল মনিটর রিলিজ করার মতো এবং
ভোলাটাইল ভেরিয়েবল থেকে পড়া হল মনিটর অ্যাকোয়ার করার মতো।
C++-এর atomic থেকে একটি উল্লেখযোগ্য পার্থক্য আছে:
আমরা যদি Java-তে volatile int x;
লিখি, তাহলে x++ ও x = x + 1 একই; এটি
অ্যাটমিক লোড পারফর্ম করে, ফলাফল ইনক্রিমেন্ট করে এবং তারপর অ্যাটমিক
স্টোর পারফর্ম করে। C++-এর মতো, ইনক্রিমেন্ট সামগ্রিকভাবে অ্যাটমিক নয়।
পরিবর্তে, অ্যাটমিক ইনক্রিমেন্ট অপারেশন
java.util.concurrent.atomic-এর মাধ্যমে প্রদান করা হয়।
উদাহরণ
এখানে একটি মনোটনিক কাউন্টারের সহজ, ভুল প্রয়োগ দেখানো হল: (Java theory and practice: Managing volatility).
class Counter { private int mValue; public int get() { return mValue; } public void incr() { mValue++; } }
ধরে নিন যে get() এবং incr() একাধিক থ্রেড থেকে কল করা হয়েছে এবং আমরা নিশ্চিত করতে চাই যে get() কল করা হলে প্রতিটি থ্রেড যেন বর্তমান গণনা দেখতে পায়। সবচেয়ে বড় সমস্যা হল
mValue++ আসলে তিনটি অপারেশন:
reg = mValuereg = reg + 1mValue = reg
incr()-এ দুটি থ্রেড একসাথে এক্সিকিউট করলে, একটি আপডেট
মুছে যেতে পারে। ইনক্রিমেন্ট অ্যাটমিক করতে, আমাদের
incr() “synchronized” ঘোষণা করতে হবে।
তবে, এটি এখনও কাজ করছে না, বিশেষ করে SMP-তে। এখনও ডেটা রেস আছে,
কারণ get(), incr()-এর সাথে একই সময়ে
mValue অ্যাক্সেস করতে পারে। Java-র নিয়ম অনুযায়ী, get() কলটি
অন্য কোডের সাপেক্ষে রিঅর্ডার করা হয়েছে বলে মনে হতে পারে। যেমন, আমরা যদি পরপর দুটি কাউন্টার রিড করি, তাহলে ফলাফলগুলি অসঙ্গতিপূর্ণ বলে মনে হতে পারে কারণ হার্ডওয়্যার বা কম্পাইলারের মাধ্যমে আমরা get()টি কল রিঅর্ডার করেছি। get()-কে সিঙ্ক করা হয়েছে বলে ঘোষণা করে আমরা সমস্যার সমাধান করতে পারি। এই পরিবর্তনের ফলে, কোডটি যে সঠিক তা স্পষ্ট।
দুর্ভাগ্যবশত, আমরা লক কনটেনশনের সম্ভাবনা চালু করেছি, যা
পারফর্ম্যান্সকে বাধাগ্রস্ত করতে পারে। get()-কে সিঙ্ক করা হয়েছে বলে ঘোষণা করার পরিবর্তে,
আমরা mValue-কে “volatile” হিসেবে ঘোষণা করতে পারি। (মনে রাখবেন,
incr()-কে এখনও synchronize ব্যবহার করতে হবে, কারণ
mValue++ অন্যথায় একটি একক অ্যাটমিক অপারেশন নয়।)
এটি সব ডেটা রেস এড়িয়ে যায়, তাই সিকোয়েন্সিয়াল কনসিস্টেন্সি বজায় থাকে।
incr() কিছুটা ধীরগতির হবে, কারণ এটি মনিটর এন্ট্রি/এক্সিট ওভারহেড এবং
একটি ভোলাটাইল স্টোরের সাথে যুক্ত ওভারহেড উভয়ই বহন করে, তবে
get() দ্রুততর হবে, তাই বিরোধের অনুপস্থিতিতেও এটি
একটি জয় যদি রিড অপারেশন রাইট অপারেশনের চেয়ে অনেক বেশি হয়। (সিঙ্ক্রোনাইজ করা ব্লক সম্পূর্ণভাবে সরানোর জন্য
AtomicInteger-ও দেখুন।)
এখানে আরেকটি উদাহরণ দেওয়া হল, যা আগের C উদাহরণের মতোই:
class MyGoodies { public int x, y; } class MyClass { static MyGoodies sGoodies; void initGoodies() { // runs in thread 1 MyGoodies goods = new MyGoodies(); goods.x = 5; goods.y = 10; sGoodies = goods; } void useGoodies() { // runs in thread 2 if (sGoodies != null) { int i = sGoodies.x; // could be 5 or 0 .... } } }
C কোডের মতো এখানেও একই সমস্যা আছে, অর্থাৎ sGoodies-এ ডেটা রেস
আছে। তাই, goods-এ ফিল্ডের ইনিশিয়ালাইজেশনের আগে
sGoodies = goods অ্যাসাইনমেন্ট
লক্ষ্য করা যেতে পারে। আপনি যদি sGoodies-কে
volatile কীওয়ার্ডের সাথে ঘোষণা করেন, তাহলে সিকোয়েন্সিয়াল কনসিস্টেন্সি রিস্টোর করা হয় এবং সবকিছু প্রত্যাশা অনুযায়ী
কাজ করবে।
মনে রাখবেন, শুধুমাত্র sGoodies রেফারেন্সটিই পরিবর্তনশীল। The
accesses to the fields inside it are not. sGoodies একবার
volatile হলে এবং মেমরি অর্ডারিং সঠিকভাবে সংরক্ষিত হলে, ফিল্ডগুলি
একই সাথে অ্যাক্সেস করা যাবে না। z =
sGoodies.x স্টেটমেন্টটি MyClass.sGoodies-এর একটি ভোলাটাইল লোড পারফর্ম করবে
তারপরে sGoodies.x-এর একটি নন-ভোলাটাইল লোড পারফর্ম করবে। আপনি লোকাল রেফারেন্স MyGoodies localGoods = sGoodies তৈরি করলে, পরবর্তী z =
localGoods.x কোনও ভোলাটাইল লোড পারফর্ম করবে না।
Java প্রোগ্রামিংয়ে আরও একটি সাধারণ ইডিয়ম হল কুখ্যাত "ডবল-চেক করা লকিং":
class MyClass { private Helper helper = null; public Helper getHelper() { if (helper == null) { synchronized (this) { if (helper == null) { helper = new Helper(); } } } return helper; } }
আইডিয়া হল, আমরা MyClass-এর ইনস্ট্যান্সের সাথে যুক্ত Helper
অবজেক্টের একটি ইনস্ট্যান্স চাই। আমাদের এটি একবারই তৈরি করতে হবে, তাই আমরা এটি তৈরি করি এবং একটি নির্দিষ্ট getHelper()
ফাংশনের মাধ্যমে এটি ফেরত দিই। দুটি থ্রেড যাতে ইনস্ট্যান্স তৈরি করার জন্য একে অপরের সাথে প্রতিযোগিতা না করে, তার জন্য আমাদের
অবজেক্ট তৈরি করার প্রসেস সিঙ্ক করতে হবে। তবে, আমরা প্রতিটি কলে "সিঙ্ক করা" ব্লকের জন্য ওভারহেড পেমেন্ট করতে চাই না, তাই helper বর্তমানে খালি থাকলে আমরা শুধুমাত্র সেই অংশটি করি।
এতে helper ফিল্ডে ডেটা রেস আছে। এটি অন্য থ্রেডে helper == null-এর সাথে
একইসাথে সেট করা যেতে পারে।
এটি কীভাবে ব্যর্থ হতে পারে তা দেখতে, একই কোড একটু অন্যভাবে লিখে দেখুন, যেন এটি C-এর মতো কোনও ভাষায় কম্পাইল করা হয়েছে
(আমি Helper’s কনস্ট্রাক্টর অ্যাক্টিভিটি দেখানোর জন্য
দুটি ইন্টিজার ফিল্ড যোগ করেছি):
if (helper == null) { synchronized() { if (helper == null) { newHelper = malloc(sizeof(Helper)); newHelper->x = 5; newHelper->y = 10; helper = newHelper; } } return helper; }
হার্ডওয়্যার বা কম্পাইলারকে x/y ফিল্ডের সাথে
helper-এ স্টোর রিঅর্ডার করা থেকে
আটকানোর কোনও উপায় নেই। অন্য থ্রেড
helper নন-নাল খুঁজে পেতে পারে, তবে এর ফিল্ড এখনও সেট করা হয়নি এবং ব্যবহারের জন্য প্রস্তুত নয়।
আরও বিবরণ এবং আরও ব্যর্থতার মোডের জন্য, আরও বিবরণের জন্য পরিশিষ্টে “‘Double Checked
Locking is Broken’ Declaration” লিঙ্কটি দেখুন, অথবা Josh Bloch-এর Effective Java,
2nd Edition-এ আইটেম
71 (“Use lazy initialization judiciously”) দেখুন।
এটি ঠিক করার দুটি উপায় আছে:
- সহজ কাজটি করুন এবং বাইরের চেকটি মুছে দিন। এটি নিশ্চিত করে যে আমরা কখনই
সিঙ্ক্রোনাইজ করা ব্লকের বাইরে
helper-এর ভ্যালু পরীক্ষা করি না। helper-কে পরিবর্তনশীল হিসেবে ঘোষণা করুন। এই একটি ছোট পরিবর্তনের মাধ্যমে, উদাহরণ J-3-এর কোড Java 1.5 এবং তার পরবর্তী ভার্সনে সঠিকভাবে কাজ করবে। (এটি যে সত্যি তা নিজেকে বোঝাতে আপনার এক মিনিট সময় লাগতে পারে।)
volatile-এর আচরণের আরেকটি উদাহরণ এখানে দেওয়া হল:
class MyClass { int data1, data2; volatile int vol1, vol2; void setValues() { // runs in Thread 1 data1 = 1; vol1 = 2; data2 = 3; } void useValues() { // runs in Thread 2 if (vol1 == 2) { int l1 = data1; // okay int l2 = data2; // wrong } } }
useValues()-এর দিকে তাকালে, Thread 2 যদি এখনও vol1-এ আপডেট না দেখে থাকে,
তাহলে এটি জানতে পারবে না যে data1 বা
data2 এখনও সেট করা হয়েছে কিনা। এটি vol1-এ আপডেট দেখতে পেলে,
এটি বুঝতে পারে যে ডেটা রেস ছাড়াই data1 নিরাপদে অ্যাক্সেস করা
এবং সঠিকভাবে পড়া যাবে। তবে,
এটি data2 সম্পর্কে কোনও অনুমান করতে পারে না, কারণ সেই স্টোরটি
অস্থির স্টোরের পরে পারফর্ম করা হয়েছিল।
মনে রাখবেন যে volatile ব্যবহার করে অন্যান্য মেমরি অ্যাক্সেসের
পুনর্বিন্যাস আটকানো যাবে না। এটি মেশিন মেমরি ফেন্স নির্দেশাবলী তৈরি করার
গ্যারান্টি দেয় না। অন্য থ্রেড নির্দিষ্ট শর্ত পূরণ করলে তবেই কোড এক্সিকিউট করে ডেটা রেস প্রতিরোধ করতে এটি ব্যবহার করা যেতে পারে।
কী করবেন
C/C++-এ, C++11
সিঙ্ক্রোনাইজেশন ক্লাস ব্যবহার করুন, যেমন std::mutex। তা না হলে, সংশ্লিষ্ট pthread অপারেশন ব্যবহার করুন।
এর মধ্যে রয়েছে সঠিক মেমরি ফেন্স, যা সমস্ত Android প্ল্যাটফর্ম ভার্সনে সঠিক (অন্যথায় নির্দিষ্ট না করা থাকলে ক্রমানুসারে সামঞ্জস্যপূর্ণ)
এবং দক্ষ আচরণ প্রদান করে। সেগুলি সঠিকভাবে ব্যবহার
করতে ভুলবেন না। যেমন, মনে রাখবেন যে কন্ডিশন ভেরিয়েবল অপেক্ষা, সিগন্যাল না পেয়েও
ভুল করে ফিরে আসতে পারে এবং তাই লুপে দেখানো উচিত।
আপনি যে ডেটা স্ট্রাকচার প্রয়োগ করছেন সেটি যদি কাউন্টারের মতো অত্যন্ত সহজ না হয়, তাহলে সরাসরি অ্যাটমিক ফাংশন ব্যবহার না করাই ভাল। pthread mutex লক ও আনলক করার জন্য প্রতিটির ক্ষেত্রে একটি করে অ্যাটমিক অপারেশন প্রয়োজন হয়, এবং কোনও বিরোধ না থাকলে, প্রায়শই একটি ক্যাশে মিসের চেয়ে কম খরচ হয়, তাই mutex কলকে অ্যাটমিক অপারেশন দিয়ে রিপ্লেস করে আপনি খুব বেশি সাশ্রয় করতে পারবেন না। নন-ট্রিভিয়াল ডেটা স্ট্রাকচারের জন্য লক-ফ্রি ডিজাইনের ক্ষেত্রে আরও বেশি যত্ন নিতে হয় যাতে ডেটা স্ট্রাকচারের উচ্চ স্তরের অপারেশনগুলি অ্যাটমিক (সামগ্রিকভাবে, শুধু তাদের স্পষ্টভাবে অ্যাটমিক অংশগুলি নয়) হিসেবে দেখা যায়।
আপনি যদি অ্যাটমিক অপারেশন ব্যবহার করেন, তাহলে
memory_order... বা lazySet()-এর সাথে রিল্যাক্সিং অর্ডারিং পারফর্ম্যান্সের
সুবিধা প্রদান করতে পারে, তবে আমরা এখনও পর্যন্ত যা বলেছি তার চেয়ে গভীর বোঝার প্রয়োজন।
এগুলি ব্যবহার করে আগে থেকে থাকা কোডের একটি বড় অংশে
পরে বাগ আছে বলে জানা গেছে। সম্ভব হলে এগুলি এড়িয়ে চলুন।
আপনার ব্যবহারের কেস যদি পরবর্তী বিভাগের কোনওটির সাথে হুবহু না মেলে,
তাহলে নিশ্চিত করুন যে আপনি একজন বিশেষজ্ঞ অথবা কোনও বিশেষজ্ঞের সাথে পরামর্শ করেছেন।
C/C++-এ থ্রেড কমিউনিকেশনের জন্য volatile ব্যবহার করা এড়িয়ে চলুন।
Java-তে, কনকারেন্সি সংক্রান্ত সমস্যা প্রায়ই
java.util.concurrent প্যাকেজ থেকে
উপযুক্ত ইউটিলিটি ক্লাস ব্যবহার করে সমাধান করা হয়। কোডটি ভালভাবে লেখা এবং SMP-তে
ভালভাবে পরীক্ষা করা হয়েছে।
সম্ভবত সবচেয়ে নিরাপদ জিনিস যা আপনি করতে পারেন তা হল আপনার অবজেক্ট অপরিবর্তনীয় করা। Java-র String এবং Integer-এর মতো ক্লাস থেকে অবজেক্ট ডেটা হোল্ড করে যা অবজেক্ট তৈরি হওয়ার পরে পরিবর্তন করা যায় না, এর ফলে এইসব অবজেক্টে ডেটা রেসের সম্ভাবনা এড়ানো যায়। Effective Java, 2nd Ed. বইতে “Item 15: Minimize Mutability” বিভাগে নির্দিষ্ট নির্দেশাবলী আছে। বিশেষ করে জাভা ফিল্ডকে “final" (Bloch) হিসেবে ঘোষণা করার গুরুত্বের কথা মনে রাখবেন।
কোনও অবজেক্ট অপরিবর্তনীয় হলেও, মনে রাখবেন যে কোনও ধরনের সিঙ্ক্রোনাইজেশন ছাড়া অন্য থ্রেডে
এটি কমিউনিকেট করা হল ডেটা রেস। এটি মাঝে মাঝে
Java-তে গ্রহণযোগ্য হতে পারে (নিচে দেখুন), তবে খুব যত্ন সহকারে করতে হবে এবং এর ফলে
কোড ভঙ্গুর হয়ে যেতে পারে। এটি যদি পারফর্ম্যান্সের ক্ষেত্রে অত্যন্ত গুরুত্বপূর্ণ না হয়, তাহলে একটি
volatile ঘোষণা যোগ করুন। C++-এ, কোনও ডেটা রেসের মতো,
যথাযথ সিঙ্ক না করে অপরিবর্তনীয় অবজেক্টে পয়েন্টার বা রেফারেন্স
কমিউনিকেট করা হল একটি বাগ।
এই ক্ষেত্রে, এটি মাঝে মাঝে ক্র্যাশ হওয়ার কারণ হতে পারে, কারণ,
উদাহরণস্বরূপ, স্টোর রিঅর্ডার করার কারণে রিসিভিং থ্রেড একটি আনইনিশিয়ালাইজড মেথড টেবিল
পয়েন্টার দেখতে পারে।
আগে থেকে থাকা লাইব্রেরি ক্লাস বা অপরিবর্তনীয় ক্লাস, কোনওটিই
উপযুক্ত না হলে, একাধিক থ্রেড অ্যাক্সেস করতে পারে এমন যেকোনও ফিল্ডে
অ্যাক্সেস সুরক্ষিত করতে Java synchronized স্টেটমেন্ট বা C++
lock_guard / unique_lock ব্যবহার করা উচিত। আপনার ক্ষেত্রে মিউটেক্স কাজ না করলে, আপনাকে শেয়ার করা ফিল্ড volatile বা atomic
ঘোষণা করতে হবে, তবে থ্রেডের মধ্যে ইন্টার্যাকশন বোঝার জন্য
আপনাকে খুব সতর্ক থাকতে হবে। এইসব ঘোষণা আপনাকে
সাধারণ কনকারেন্ট প্রোগ্রামিং সংক্রান্ত ভুল থেকে রক্ষা করবে না, তবে এগুলি আপনাকে
কম্পাইলার অপ্টিমাইজ করা এবং SMP
দুর্ঘটনার সাথে সম্পর্কিত রহস্যজনক ব্যর্থতা এড়াতে সাহায্য করবে।
আপনার উচিত কোনও অবজেক্টের রেফারেন্স "প্রকাশ" করা এড়ানো, অর্থাৎ, এর কনস্ট্রাক্টরে অন্যান্য থ্রেডের জন্য এটি উপলভ্য করা। C++-এ এটি কম গুরুত্বপূর্ণ অথবা আপনি যদি Java-তে আমাদের "কোনও ডেটা রেস নেই" সংক্রান্ত পরামর্শ মেনে চলেন। তবে এটি সবসময়ই ভালো পরামর্শ এবং আপনার জাভা কোড যদি অন্য কোনও প্রসঙ্গে রান করে যেখানে জাভা নিরাপত্তা মডেল গুরুত্বপূর্ণ, তাহলে এটি অত্যন্ত গুরুত্বপূর্ণ হয়ে ওঠে এবং অবিশ্বস্ত কোড সেই "লিক হওয়া" অবজেক্ট রেফারেন্স অ্যাক্সেস করে ডেটা রেস তৈরি করতে পারে। এছাড়াও, আপনি যদি আমাদের সতর্কতা উপেক্ষা করে পরবর্তী বিভাগে উল্লেখ করা কিছু কৌশল ব্যবহার করেন, তাহলে তা গুরুতর সমস্যা তৈরি করতে পারে। বিস্তারিত জানতে (Java-তে নিরাপদ নির্মাণ কৌশল) দেখুন
দুর্বল মেমরি অর্ডার সম্পর্কে আরও কিছু তথ্য
C++11 এবং পরবর্তী ভার্সন ডেটা-রেস-ফ্রি প্রোগ্রামের জন্য সিকোয়েন্সিয়াল
কনসিস্টেন্সি গ্যারান্টি শিথিল করার জন্য এক্সপ্লিসিট মেকানিজম প্রদান করে। অ্যাটমিক অপারেশনের জন্য এক্সপ্লিসিট
memory_order_relaxed, memory_order_acquire (শুধুমাত্র
লোড) এবং memory_order_release(শুধুমাত্র স্টোর) আর্গুমেন্ট, ডিফল্ট, সাধারণত
ইমপ্লিসিট, memory_order_seq_cst-এর তুলনায় অনেক কম গ্যারান্টি দেয়। memory_order_acq_rel
অ্যাটমিক রিড-মডিফাই রাইট অপারেশনের জন্য
memory_order_acquire এবং
memory_order_release গ্যারেন্টি, দুটিই প্রদান করে। memory_order_consume এখনও পর্যন্ত যথেষ্ট
ভালভাবে নির্দিষ্ট বা কার্যকর করা হয়নি, তাই আপাতত এটি উপেক্ষা করা উচিত।
Java.util.concurrent.atomic-এ lazySet পদ্ধতি
C++ memory_order_release স্টোরের মতো। Java-র
সাধারণ ভেরিয়েবেল কখনও কখনও memory_order_relaxed অ্যাক্সেসের
পরিবর্তে ব্যবহার করা হয়, যদিও সেগুলি আসলে
আরও দুর্বল। C++-এর মতো, volatile হিসেবে ঘোষিত ভেরিয়েবলে
অর্ডার না করা অ্যাক্সেসের জন্য কোনও বাস্তব মেকানিজম নেই।
পারফর্ম্যান্সের উপর চাপ সৃষ্টি করার মতো কোনও কারণ না থাকলে, সাধারণত এগুলি ব্যবহার করা এড়িয়ে চলুন। ARM-এর মতো দুর্বলভাবে অর্ডার করা মেশিন আর্কিটেকচারে, সেগুলি ব্যবহার করলে সাধারণত প্রতিটি অ্যাটমিক অপারেশনের জন্য কয়েক ডজন মেশিন সাইকেলের অর্ডারে সেভ হবে। x86-এ, পারফর্ম্যান্সের উন্নতি স্টোরে সীমিত এবং সম্ভবত কম লক্ষণীয়। কিছুটা বিপরীতভাবে, আরও বেশি কোর কাউন্টের সাথে সুবিধা কমে যেতে পারে, কারণ মেমরি সিস্টেম আরও বেশি সীমাবদ্ধ ফ্যাক্টর হয়ে ওঠে।
দুর্বলভাবে অর্ডার করা অ্যাটমিকের সম্পূর্ণ শব্দার্থবিদ্যা জটিল। সাধারণত, এগুলি বোঝার জন্য ভাষার নিয়ম সম্পর্কে সঠিক ধারণা থাকা প্রয়োজন, যা আমরা এখানে আলোচনা করব না। যেমন:
- কম্পাইলার বা হার্ডওয়্যার, লক অ্যাকুইজিশন ও রিলিজের মাধ্যমে বাউন্ড করা ক্রিটিকাল সেকশনে
memory_order_relaxedঅ্যাক্সেস সরাতে পারে (তবে বাইরে নয়)। এর অর্থ হল, দুটিmemory_order_relaxedস্টোর ক্রমের বাইরে দৃশ্যমান হতে পারে, এমনকি সেগুলি কোনও গুরুত্বপূর্ণ বিভাগ দ্বারা আলাদা করা থাকলেও। - একটি সাধারণ জাভা ভেরিয়েবল, যখন শেয়ার করা কাউন্টার হিসেবে অপব্যবহার করা হয়, তখন
অন্য থ্রেডে কমতে পারে, যদিও এটি শুধুমাত্র অন্য একটি থ্রেড দ্বারা
বৃদ্ধি পায়। কিন্তু C++ অ্যাটমিকের ক্ষেত্রে এটি প্রযোজ্য নয়
memory_order_relaxed।
সেই সতর্কতা মাথায় রেখে, এখানে আমরা অল্প কিছু ইডিয়ম উল্লেখ করছি যা দুর্বলভাবে অর্ডার করা অ্যাটমিকের অনেক ব্যবহারের ক্ষেত্রে প্রযোজ্য বলে মনে হয়। এর মধ্যে অনেকগুলি শুধুমাত্র C++-এর ক্ষেত্রে প্রযোজ্য।
রেসিং নয় এমন অ্যাক্সেস
এটি খুবই সাধারণ যে একটি ভেরিয়েবল অ্যাটমিক কারণ এটি কখনও কখনও
রাইটের সাথে একই সাথে পড়া হয়, কিন্তু সব অ্যাক্সেসে এই সমস্যা হয় না।
যেমন, কোনও ভেরিয়েবল
অ্যাটমিক হতে পারে কারণ এটি ক্রিটিক্যাল সেকশনের বাইরে পড়া হয়, কিন্তু সব
আপডেট লক দ্বারা সুরক্ষিত থাকে। সেই ক্ষেত্রে, একই লকের মাধ্যমে সুরক্ষিত
রিড অপারেশন
রেস করতে পারে না, কারণ একই সাথে রাইট অপারেশন করা যায় না। এই ধরনের ক্ষেত্রে, নন-রেসিং অ্যাক্সেস (এই ক্ষেত্রে লোড), C++ কোডের সঠিকতা পরিবর্তন না করেই
memory_order_relaxed-এর সাথে অ্যানোটেট করা যেতে পারে।
লক প্রয়োগ করার ফলে অন্যান্য থ্রেডের অ্যাক্সেসের ক্ষেত্রে প্রয়োজনীয় মেমরি অর্ডারিং
ইতিমধ্যেই এনফোর্স করা হয় এবং memory_order_relaxed
নির্দিষ্ট করে যে অ্যাটমিক অ্যাক্সেসের জন্য মূলত কোনও অতিরিক্ত অর্ডারিং কনস্ট্রেন্ট
এনফোর্স করার প্রয়োজন নেই।
Java-তে এর কোনও আসল অ্যানালগ নেই।
ফলাফল নির্ভুল কিনা তা নিয়ে সন্দেহ আছে
আমরা যখন শুধুমাত্র একটি ইঙ্গিত তৈরি করার জন্য রেসিং লোড ব্যবহার করি, তখন সাধারণত লোডের জন্য কোনও মেমরি অর্ডারিং প্রয়োগ না করাও ঠিক আছে। ভ্যালু নির্ভরযোগ্য না হলে, অন্যান্য ভেরিয়েবল সম্পর্কে কিছু অনুমান করার জন্য আমরা ফলাফলটি নির্ভরযোগ্যভাবে
ব্যবহার করতে পারব না। তাই, মেমরি অর্ডারিংয়ের গ্যারান্টি না থাকলেও
এবং লোড memory_order_relaxed আর্গুমেন্টের সাথে
প্রদান করা হলেও কোনও সমস্যা নেই।
এর একটি সাধারণ
উদাহরণ হল C++ compare_exchange-এর ব্যবহার
যা x-কে f(x) দ্বারা অ্যাটমিক্যালি রিপ্লেস করে।
f(x) গণনা করার জন্য x-এর প্রাথমিক লোড
নির্ভরযোগ্য হওয়ার প্রয়োজন নেই। আমরা ভুল করলে,
compare_exchange ব্যর্থ হবে এবং আমরা আবার চেষ্টা করব।
x-এর প্রাথমিক লোডের জন্য memory_order_relaxed আর্গুমেন্ট ব্যবহার করা
ঠিক আছে; শুধুমাত্র আসল compare_exchange-এর জন্য
মেমরি অর্ডারিং গুরুত্বপূর্ণ।
অ্যাটমিকভাবে পরিবর্তিত কিন্তু না পড়া ডেটা
কখনও কখনও একাধিক থ্রেড দ্বারা সমান্তরালভাবে ডেটা পরিবর্তন করা হয়, কিন্তু
সমান্তরাল গণনা সম্পূর্ণ না হওয়া পর্যন্ত পরীক্ষা করা হয় না। এর একটি ভালো
উদাহরণ হল এমন একটি কাউন্টার যা সমান্তরালভাবে একাধিক থ্রেড দ্বারা অ্যাটমিক্যালি বৃদ্ধি করা হয় (যেমন
C++-এ fetch_add() বা
atomic_fetch_add_explicit()
C-তে ব্যবহার করা), কিন্তু এইসব কলের ফলাফল
সবসময় উপেক্ষা করা হয়। সব আপডেট সম্পূর্ণ হওয়ার পরে,
ফলাফলের ভ্যালু শুধুমাত্র শেষে পড়া হয়।
এই ক্ষেত্রে, এই ডেটার অ্যাক্সেস রিঅর্ডার করা হয়েছে কিনা তা বলার কোনও উপায় নেই, এবং তাই C++ কোড একটি memory_order_relaxed
আর্গুমেন্ট ব্যবহার করতে পারে।
সাধারণ ইভেন্ট কাউন্টার হল এর একটি সাধারণ উদাহরণ। যেহেতু এটি খুবই সাধারণ, তাই এই কেস সম্পর্কে কিছু পর্যবেক্ষণ করা দরকার:
-
memory_order_relaxedব্যবহার করলে পারফর্ম্যান্স উন্নত হয়, তবে সবচেয়ে গুরুত্বপূর্ণ পারফর্ম্যান্স সংক্রান্ত সমস্যার সমাধান নাও হতে পারে: প্রতিটি আপডেটের জন্য কাউন্টার হোল্ড করা ক্যাশে লাইনে এক্সক্লুসিভ অ্যাক্সেস প্রয়োজন। এর ফলে, প্রতিবার নতুন থ্রেড কাউন্টার অ্যাক্সেস করার সময় ক্যাশে মিস হয়। আপডেট ঘন ঘন হলে এবং থ্রেডের মধ্যে বিকল্প হলে, প্রতিবার শেয়ার করা কাউন্টার আপডেট করা এড়িয়ে গেলে অনেক দ্রুত হয়, যেমন, থ্রেড-লোকাল কাউন্টার ব্যবহার করা এবং শেষে সেগুলি যোগ করা। - এই টেকনিক আগের বিভাগের সাথে ব্যবহার করা যেতে পারে:
memory_order_relaxedব্যবহার করে সব অপারেশন করার সময়, আপডেট করা হচ্ছে এমন আনুমানিক ও নির্ভরযোগ্য নয় এমন ভ্যালু একসাথে পড়া সম্ভব। তবে, ফলাফল হিসেবে পাওয়া ভ্যালু সম্পূর্ণভাবে নির্ভরযোগ্য নয় বলে ধরে নেওয়া গুরুত্বপূর্ণ। কাউন্ট একবার বেড়েছে বলে মনে হচ্ছে মানেই এই নয় যে অন্য থ্রেডও সেই পয়েন্টে পৌঁছে গেছে যেখানে ইনক্রিমেন্ট করা হয়েছে। পরিবর্তে, ইনক্রিমেন্টটি আগের কোডের সাথে পুনরায় সাজানো হতে পারে। (আমরা আগে যে একই ধরনের কেসের কথা উল্লেখ করেছি, C++ গ্যারান্টি দেয় যে এই ধরনের কাউন্টারের দ্বিতীয় লোড একই থ্রেডে আগের লোডের থেকে কম ভ্যালু রিটার্ন করবে না। অবশ্যই কাউন্টার ওভারফ্লো না হলে।) - এমন কোড খুঁজে পাওয়া সাধারণ ব্যাপার যা আলাদা আলাদা অ্যাটমিক (বা অ্যাটমিক নয়) রিড ও রাইট পারফর্ম করে কাউন্টার ভ্যালুর আনুমানিক হিসেব করার চেষ্টা করে, কিন্তু পুরো অ্যাটমিকের ইনক্রিমেন্ট করে না। সাধারণত যুক্তি দেওয়া হয় যে পারফর্ম্যান্স কাউন্টার বা এই ধরনের কিছুর জন্য এটি "যথেষ্ট কাছাকাছি"। সাধারণত তা হয় না। যখন আপডেটগুলি যথেষ্ট ঘন ঘন হয় (এমন একটি ঘটনা যা আপনার কাছে গুরুত্বপূর্ণ), তখন গণনার একটি বড় অংশ সাধারণত হারিয়ে যায়। কোয়াড কোর ডিভাইসে, সাধারণত অর্ধেকের বেশি গণনা হারিয়ে যেতে পারে। (সহজ এক্সারসাইজ: এমন একটি টু-থ্রেড সিনারিও তৈরি করুন যেখানে কাউন্টারটি দশ লক্ষ বার আপডেট করা হয়েছে, কিন্তু চূড়ান্ত কাউন্টার ভ্যালু হল এক।)
সহজ ফ্ল্যাগ কমিউনিকেশন
A memory_order_release স্টোর (বা রিড-মডিফাই-রাইট অপারেশন)
নিশ্চিত করে যে, পরবর্তীতে memory_order_acquire লোড
(বা রিড-মডিফাই-রাইট অপারেশন) লিখিত ভ্যালু রিড করলে, এটি
A memory_order_release স্টোরের আগে থাকা যেকোনও স্টোরও (সাধারণ বা অ্যাটমিক)
পর্যবেক্ষণ করবে। বিপরীতভাবে, memory_order_release-এর আগে
কোনও লোড, memory_order_acquire লোড অনুসরণ করে এমন কোনও
স্টোর পর্যবেক্ষণ করবে না।
memory_order_relaxed-এর মতো নয়, এটি এই ধরনের অ্যাটমিক অপারেশনকে
একটি থ্রেডের প্রগ্রেস অন্য থ্রেডে কমিউনিকেট করার অনুমতি দেয়।
যেমন, আমরা উপরে উল্লেখ করা ডবল-চেক করা লকিংয়ের উদাহরণ C++-এ এইভাবে আবার লিখতে পারি
class MyClass { private: atomic<Helper*> helper {nullptr}; mutex mtx; public: Helper* getHelper() { Helper* myHelper = helper.load(memory_order_acquire); if (myHelper == nullptr) { lock_guard<mutex> lg(mtx); myHelper = helper.load(memory_order_relaxed); if (myHelper == nullptr) { myHelper = new Helper(); helper.store(myHelper, memory_order_release); } } return myHelper; } };
অ্যাকোয়ার লোড এবং রিলিজ স্টোর নিশ্চিত করে যে আমরা যদি একটি নন-নাল
helper দেখতে পাই, তাহলে আমরা এর ফিল্ডগুলিও সঠিকভাবে শুরু করা দেখতে পাব।
আমরা আগে লক্ষ্য করেছি যে নন-রেসিং লোড memory_order_relaxed ব্যবহার করতে পারে।
একজন জাভা প্রোগ্রামার helper-কে java.util.concurrent.atomic.AtomicReference<Helper> হিসেবে দেখাতে পারেন এবং রিলিজ স্টোর হিসেবে lazySet() ব্যবহার করতে পারেন। লোড
অপারেশনগুলি প্লেইন get() কল ব্যবহার করা চালিয়ে যাবে।
In both cases, our performance tweaking concentrated on the initialization path, which is unlikely to be performance critical. আরও সহজে পড়া যায় এমন একটি বিকল্প হতে পারে:
Helper* getHelper() { Helper* myHelper = helper.load(memory_order_acquire); if (myHelper != nullptr) { return myHelper; } lock_guard<mutex> lg(mtx); if (helper == nullptr) { helper = new Helper(); } return helper; }
এটি একই দ্রুত পাথ প্রদান করে, তবে পারফর্ম্যান্স-ক্রিটিক্যাল নয় এমন স্লো পাথে ডিফল্ট, সিকোয়েন্সিয়াল-কনসিস্টেন্ট, অপারেশন অবলম্বন করে।
এমনকি এখানেও, helper.load(memory_order_acquire)
সম্ভবত বর্তমান Android-সমর্থিত আর্কিটেকচারে একই কোড তৈরি করবে
যেমন helper-এর জন্য একটি প্লেইন (সিকোয়েন্সিয়াল-কনসিস্টেন্ট)
রেফারেন্স। এখানে সবচেয়ে বেশি সুবিধা পাওয়া যাবে
myHelper-এর মাধ্যমে, যা দ্বিতীয়বার লোড করার প্রয়োজনীয়তা
দূর করে, যদিও ভবিষ্যতে কম্পাইলার এটি অটোমেটিক করতে পারে।
অর্ডার অ্যাকোয়ার/রিলিজ করলে স্টোর দৃশ্যমান হতে দেরি হওয়া আটকানো যায় না এবং
অন্যান্য থ্রেডে স্টোরগুলি যাতে একই ক্রমে দৃশ্যমান হয় তা নিশ্চিত করা যায় না। এর ফলে, এটি একটি জটিল,
কিন্তু মোটামুটি সাধারণ কোডিং প্যাটার্ন সাপোর্ট করে না, যার উদাহরণ হল ডেকারের মিউচুয়াল এক্সক্লুশন
অ্যালগরিদম: সব থ্রেড প্রথমে একটি ফ্ল্যাগ সেট করে যা দেখায় যে তারা কিছু
করতে চায়; যদি কোনও থ্রেড t তারপর লক্ষ্য করে যে অন্য কোনও থ্রেড
কিছু করার চেষ্টা করছে না, তাহলে এটি নিরাপদে এগিয়ে যেতে পারে, কারণ এটি জানে যে সেখানে
কোনও বাধা থাকবে না। অন্য কোনও থ্রেড
এগিয়ে যেতে পারবে না, কারণ t-এর ফ্ল্যাগ এখনও সেট করা আছে। অ্যাকোয়ার/রিলিজ অর্ডারিং ব্যবহার করে ফ্ল্যাগ অ্যাক্সেস করা হলে
এটি কাজ করে না, কারণ এটি
কোনও থ্রেডের ফ্ল্যাগ অন্যদের কাছে দেরিতে দৃশ্যমান হওয়া আটকাতে পারে না, যখন তারা ভুলভাবে এগিয়ে যায়। ডিফল্ট memory_order_seq_cst
এটি আটকায়।
অপরিবর্তনীয় ফিল্ড
কোনও অবজেক্ট ফিল্ড প্রথমবার ব্যবহার করার সময় ইনিশিয়ালাইজ করা হলে এবং তারপর কখনও পরিবর্তন করা না হলে,
দুর্বলভাবে অর্ডার করা অ্যাক্সেস ব্যবহার করে এটি ইনিশিয়ালাইজ করা এবং পরে পড়া
সম্ভব হতে পারে। C++-এ, এটি atomic হিসেবে ঘোষণা করা যেতে পারে
এবং memory_order_relaxed ব্যবহার করে অ্যাক্সেস করা যেতে পারে অথবা Java-তে, এটি
volatile ছাড়া ঘোষণা করা যেতে পারে এবং বিশেষ ব্যবস্থা ছাড়াই
অ্যাক্সেস করা যেতে পারে। এর জন্য নিম্নলিখিত সবকটি শর্ত পূরণ করতে হবে:
- এটি আগে থেকেই শুরু করা হয়েছে কিনা তা ফিল্ডের ভ্যালু থেকেই বোঝা যেতে হবে। ফিল্ড অ্যাক্সেস করতে, ফাস্ট পাথ টেস্ট-অ্যান্ড-রিটার্ন ভ্যালু ফিল্ডটি একবারই পড়তে হবে। জাভাতে পরেরটি অপরিহার্য। এমনকি ফিল্ডটি শুরু করা হিসাবে পরীক্ষা করা হলেও, একটি দ্বিতীয় লোড আগের শুরু না করা মান পড়তে পারে। C++-এ "একবার পড়ুন" নিয়মটি শুধুমাত্র একটি ভালো প্র্যাকটিস।
- শুরু করা এবং পরবর্তী লোড, দুটিই অ্যাটমিক হতে হবে,
যাতে আংশিক আপডেট দেখা না যায়। Java-এর জন্য, ফিল্ডটি
longবাdoubleহওয়া উচিত নয়। C++-এর জন্য, অ্যাটমিক অ্যাসাইনমেন্ট প্রয়োজন; এটি যথাস্থানে তৈরি করলে কাজ হবে না, কারণatomic-এর কনস্ট্রাকশন অ্যাটমিক নয়। - একাধিক থ্রেড একই সাথে আনইনিশিয়ালাইজ করা ভ্যালু পড়তে পারে বলে, বারবার ইনিশিয়ালাইজ করা নিরাপদ হতে হবে। C++-এ, এটি সাধারণত সব অ্যাটমিক টাইপের জন্য আরোপ করা "ট্রিভিয়ালি কপিএবল" প্রয়োজনীয়তা অনুসরণ করে; নেস্টেড ওনার পয়েন্টার সহ টাইপের ক্ষেত্রে কপি কনস্ট্রাক্টরে ডিলোকেশনের প্রয়োজন হবে এবং ট্রিভিয়ালি কপিএবল হবে না। Java-র ক্ষেত্রে, নির্দিষ্ট কিছু রেফারেন্সের ধরন গ্রহণযোগ্য:
- Java রেফারেন্স শুধুমাত্র অপরিবর্তনীয় প্রকারের মধ্যে সীমাবদ্ধ থাকে যার মধ্যে শুধুমাত্র ফাইনাল ফিল্ড থাকে। অপরিবর্তনীয় টাইপের কনস্ট্রাক্টরকে অবজেক্টের রেফারেন্স প্রকাশ করা উচিত নয়। এই ক্ষেত্রে Java ফাইনাল ফিল্ডের নিয়ম নিশ্চিত করে যে কোনও রিডার রেফারেন্স দেখলে, এটি ইনিশিয়ালাইজ করা ফাইনাল ফিল্ডও দেখবে। C++-এ এই নিয়মগুলির কোনও অ্যানালগ নেই এবং এই কারণেও মালিকানাধীন অবজেক্টের পয়েন্টার গ্রহণযোগ্য নয় ("সহজে কপি করা যায়" সংক্রান্ত প্রয়োজনীয়তা লঙ্ঘন করার পাশাপাশি)।
ক্লোজিং নোট
এই ডকুমেন্টটি কেবল উপরিভাগের আঁচড় কাটার চেয়ে বেশি কিছু করলেও, এটি অগভীর গর্তের চেয়ে বেশি কিছু করতে পারে না। এটি একটি খুব বিস্তৃত এবং গভীর বিষয়। আরও এক্সপ্লোর করার জন্য কিছু ক্ষেত্র:
- প্রকৃত Java এবং C++ মেমরি মডেলগুলি একটি
আগে-ঘটে সম্পর্কের পরিপ্রেক্ষিতে প্রকাশ করা হয় যা নির্দিষ্ট করে যে কখন দুটি অ্যাকশন একটি নির্দিষ্ট ক্রমে
ঘটার গ্যারান্টি দেওয়া হয়। ডেটা রেস সংজ্ঞায়িত করার সময়, আমরা অনানুষ্ঠানিকভাবে "একই সাথে" হওয়া দুটি মেমরি অ্যাক্সেস সম্পর্কে কথা বলেছি।
অফিসিয়ালি, এটি এমনভাবে সংজ্ঞায়িত করা হয় যাতে একটির আগে অন্যটি না ঘটে।
Java বা C++ মেমরি মডেলে happens-before
এবং synchronizes-with-এর আসল সংজ্ঞা জানাটা শিক্ষণীয়।
যদিও "একই সাথে" শব্দটির স্বজ্ঞাত ধারণা সাধারণত যথেষ্ট ভালো, এই সংজ্ঞাগুলি শিক্ষামূলক, বিশেষ করে যদি আপনি
C++-এ দুর্বলভাবে অর্ডার করা অ্যাটমিক অপারেশন ব্যবহার করার কথা ভাবছেন।
(বর্তমান জাভা স্পেসিফিকেশন শুধুমাত্র
lazySet()খুব অনানুষ্ঠানিকভাবে সংজ্ঞায়িত করে।) - কোড রিঅর্ডার করার সময় কম্পাইলার কী করতে পারে এবং কী করতে পারে না তা এক্সপ্লোর করুন। (JSR-133 স্পেসিফিকেশনে আইনি ট্রান্সফর্মেশনের কিছু দুর্দান্ত উদাহরণ রয়েছে যা অপ্রত্যাশিত ফলাফলের দিকে পরিচালিত করে।)
- Java ও C++-এ কীভাবে ইমিউটেবল ক্লাস লিখতে হয় তা খুঁজে দেখুন। (এতে "কনস্ট্রাকশনের পরে কিছু পরিবর্তন করবেন না" ছাড়াও আরও অনেক কিছু আছে।)
- Effective Java, 2nd Edition-এর কনকারেন্সি বিভাগে দেওয়া সাজেশনগুলি আত্মস্থ করুন। (যেমন, সিঙ্ক্রোনাইজ করা ব্লকের মধ্যে ওভাররাইড করার জন্য তৈরি করা কলিং পদ্ধতি এড়িয়ে চলা উচিত।)
- কী উপলভ্য আছে তা দেখতে
java.util.concurrentএবংjava.util.concurrent.atomicAPI-এর মাধ্যমে পড়ুন।@ThreadSafeএবং@GuardedBy(net.jcip.annotations থেকে)-এর মতো কনকারেন্সি অ্যানোটেশন ব্যবহার করার কথা বিবেচনা করুন।
পরিশিষ্টের আরও পড়ুন বিভাগে এমন ডকুমেন্ট ও ওয়েবসাইটের লিঙ্ক আছে যা এইসব বিষয় আরও ভালোভাবে বুঝতে সাহায্য করবে।
অ্যাপেনডিক্স
সিঙ্ক্রোনাইজেশন স্টোর প্রয়োগ করা
(এটি এমন কিছু নয় যা বেশিরভাগ প্রোগ্রামাররা নিজেদের প্রয়োগ করতে দেখবেন, তবে আলোচনাটি আলোকিত করে।)
int-এর মতো ছোট বিল্ট-ইন টাইপ এবং Android-এর মাধ্যমে কাজ করে এমন হার্ডওয়্যারের ক্ষেত্রে,
সাধারণ লোড ও স্টোর সংক্রান্ত নির্দেশাবলী নিশ্চিত করে যে, একই লোকেশন লোড করা অন্য প্রসেসরের কাছে
স্টোরটি সম্পূর্ণভাবে দৃশ্যমান হবে,
অথবা একেবারেই দৃশ্যমান হবে না। তাই "অ্যাটমিসিটি"র কিছু প্রাথমিক ধারণা
ফ্রিতে প্রদান করা হয়।
আমরা আগেই দেখেছি যে এটি যথেষ্ট নয়। In order to ensure sequential consistency we also need to prevent reordering of operations, and to ensure that memory operations become visible to other processes in a consistent order. দেখা গেছে যে Android-সমর্থিত হার্ডওয়্যারে পরেরটি অটোমেটিক হয়, যদি আমরা আগেরটি প্রয়োগ করার জন্য বিচক্ষণ পছন্দ করি, তাই আমরা এখানে এটিকে মূলত উপেক্ষা করি।
কম্পাইলার ও হার্ডওয়্যার, উভয়ের দ্বারা রিঅর্ডার করা আটকানোর মাধ্যমে মেমরি অপারেশনের অর্ডার সংরক্ষিত হয়। এখানে আমরা পরেরটির উপর ফোকাস করি।
ARMv7, x86 ও MIPS-এ মেমরি অর্ডারিং "ফেন্স" নির্দেশাবলীর মাধ্যমে
এনফোর্স করা হয় যা
ফেন্সের আগে থাকা নির্দেশাবলীর আগে ফেন্সের পরে থাকা নির্দেশাবলী
দৃশ্যমান হওয়াকে মোটামুটিভাবে আটকায়। (এগুলিকে সাধারণত "ব্যারিয়ার" নির্দেশাবলীও বলা হয়, তবে এটি pthread_barrier-স্টাইল ব্যারিয়ারের সাথে বিভ্রান্তির ঝুঁকি তৈরি করে, যা এর চেয়ে অনেক বেশি কাজ করে।) ফেন্স নির্দেশাবলীর সুনির্দিষ্ট অর্থ হল একটি মোটামুটি জটিল বিষয় যা বিভিন্ন ধরণের ফেন্সের মাধ্যমে প্রদত্ত গ্যারান্টিগুলি যেভাবে ইন্টার্যাক্ট করে এবং কীভাবে এগুলি সাধারণত হার্ডওয়্যার দ্বারা প্রদত্ত অন্যান্য অর্ডারিং গ্যারান্টির সাথে একত্রিত হয় তা সমাধান করতে হয়। এটি একটি হাই লেভেল ওভারভিউ, তাই আমরা
এইসব বিবরণ এড়িয়ে যাব।
সবচেয়ে সাধারণ ধরনের অর্ডারিং গ্যারান্টি হল C++
memory_order_acquire এবং memory_order_release
অ্যাটমিক অপারেশন দ্বারা প্রদান করা গ্যারান্টি: রিলিজ স্টোরের আগে মেমরি অপারেশন
অ্যাকোয়ার লোডের পরে দেখা যেতে হবে। ARMv7-এ, এটি
এনফোর্স করা হয়:
- স্টোর সংক্রান্ত নির্দেশের আগে উপযুক্ত ফেন্স সংক্রান্ত নির্দেশ দেওয়া। এটি স্টোর নির্দেশের সাথে আগের সমস্ত মেমরি অ্যাক্সেসকে পুনরায় সাজানো থেকে আটকায়। (এটি স্টোরের পরবর্তী নির্দেশাবলী সহ অপ্রয়োজনীয়ভাবে পুনরায় অর্ডার করা আটকায়।)
- উপযুক্ত ফেন্স নির্দেশাবলী সহ লোড নির্দেশাবলী অনুসরণ করে, পরবর্তী অ্যাক্সেসের সাথে লোডকে রিঅর্ডার করা থেকে আটকানো। (এবং আরও একবার অন্তত আগের লোডের সাথে অপ্রয়োজনীয় অর্ডার প্রদান করা।)
Together these suffice for C++ acquire/release ordering.
Java volatile বা C++ ক্রমানুসারে সামঞ্জস্যপূর্ণ atomic-এর জন্য
এগুলি প্রয়োজনীয়, তবে যথেষ্ট নয়।
আমাদের আর কী কী প্রয়োজন তা দেখতে, ডেকারের অ্যালগরিদমের ফ্র্যাগমেন্টের কথা ভাবুন
যা আমরা আগে সংক্ষেপে উল্লেখ করেছি।
flag1 এবং flag2 হল C++ atomic
বা Java volatile ভেরিয়েবল, দুটিই প্রাথমিকভাবে false.
| থ্রেড ১ | থ্রেড ২ |
|---|---|
flag1 = true |
flag2 = true |
সিকোয়েন্সিয়াল কনসিস্টেন্সি বলতে বোঝায় যে
flagn-এ অ্যাসাইনমেন্টের মধ্যে একটি প্রথমে এক্সিকিউট করতে হবে এবং
অন্য থ্রেডে টেস্টের মাধ্যমে দেখতে হবে। তাই, আমরা কখনই দেখব না যে
এই থ্রেডগুলি একই সাথে "গুরুত্বপূর্ণ-জিনিস" কার্যকর করছে।
কিন্তু, অ্যাকোয়ার-রিলিজ অর্ডারিংয়ের জন্য প্রয়োজনীয় ফেন্সিং শুধুমাত্র প্রতিটি থ্রেডের শুরুতে ও শেষে ফেন্স যোগ করে, যা এখানে কোনও কাজে লাগে না। এছাড়াও, আমাদের নিশ্চিত করতে হবে যে
volatile/atomic স্টোরের পরে
volatile/atomic লোড হলে, দুটি যেন রিঅর্ডার না হয়।
এটি সাধারণত একটি বেড়া যোগ করে প্রয়োগ করা হয়, শুধু একটির আগে নয়
ক্রমানুসারে সামঞ্জস্যপূর্ণ স্টোর, কিন্তু এর পরেও।
(এটি আবার প্রয়োজনের চেয়ে অনেক বেশি শক্তিশালী, কারণ এই ফেন্স সাধারণত
পরবর্তী সমস্ত মেমরি অ্যাক্সেসের সাপেক্ষে আগের সমস্ত মেমরি অ্যাক্সেস অর্ডার করে।)
পরিবর্তে আমরা অতিরিক্ত ফেন্সকে ক্রমানুসারে সামঞ্জস্যপূর্ণ লোডের সাথে যুক্ত করতে পারি। যেহেতু স্টোর কম ঘন ঘন হয়, তাই আমরা যে কনভেনশন বর্ণনা করেছি তা আরও সাধারণ এবং Android-এ ব্যবহৃত হয়।
আগের বিভাগে যেমন দেখেছি, দুটি অপারেশনের মধ্যে আমাদের স্টোর/লোড ব্যারিয়ার যোগ করতে হবে। অস্থায়ী অ্যাক্সেসের জন্য VM-এ এক্সিকিউট করা কোড কিছুটা এইরকম দেখতে হবে:
| অস্থির লোড | অস্থির স্টোর |
|---|---|
reg = A |
fence for "release" (2) |
আসল মেশিন আর্কিটেকচার সাধারণত একাধিক ধরনের ফেন্স প্রদান করে, যা বিভিন্ন ধরনের অ্যাক্সেস অর্ডার করে এবং আলাদা খরচ হতে পারে। The choice between these is subtle, and influenced by the need to ensure that stores are made visible to other cores in a consistent order, and that the memory ordering imposed by the combination of multiple fences composes correctly. আরও তথ্যের জন্য, অ্যাটমিক থেকে আসল প্রসেসরের সংগৃহীত ম্যাপিং সহ কেমব্রিজ বিশ্ববিদ্যালয়ের পৃষ্ঠা দেখুন।
কিছু আর্কিটেকচারে, বিশেষ করে x86-এ, "অ্যাকোয়ার" ও "রিলিজ" ব্যারিয়ারের প্রয়োজন হয় না, কারণ হার্ডওয়্যার সবসময় অন্তর্নিহিতভাবে পর্যাপ্ত অর্ডারিং এনফোর্স করে। তাই x86-এ শুধুমাত্র শেষ ফেন্স (৩) আসলে তৈরি হয়। একইভাবে x86-এ, অ্যাটমিক রিড-মডিফাই-রাইট অপারেশন অন্তর্নিহিতভাবে একটি শক্তিশালী ফেন্স অন্তর্ভুক্ত করে। তাই এদের জন্য কখনও বেড়ার প্রয়োজন হয় না। ARMv7-এ উপরে আলোচনা করা সব ফেন্স প্রয়োজন।
ARMv8-এ LDAR এবং STLR নির্দেশাবলী রয়েছে যা সরাসরি Java volatile বা C++ সিকোয়েন্সিয়াল কনসিস্টেন্ট লোড ও স্টোরের প্রয়োজনীয়তা এনফোর্স করে। এগুলি উপরে উল্লেখ করা অপ্রয়োজনীয় রিঅর্ডার সংক্রান্ত সীমাবদ্ধতা এড়িয়ে যায়। ARM-এ 64-বিট Android কোড এগুলি ব্যবহার করে; আমরা এখানে ARMv7 ফেন্স প্লেসমেন্টের উপর ফোকাস করার সিদ্ধান্ত নিয়েছি কারণ এটি আসল প্রয়োজনীয়তার উপর আরও বেশি আলোকপাত করে।
আরও পড়ুন
ওয়েব পেজ এবং ডকুমেন্ট যা আরও গভীর বা বিস্তৃত তথ্য প্রদান করে। সাধারণত আরও বেশি উপযোগী নিবন্ধ তালিকার উপরের দিকে থাকে।
- শেয়ার করা মেমরি কনসিস্টেন্সি মডেল: একটি টিউটোরিয়াল
- ১৯৯৫ সালে অ্যাডভে ও ঘরাচরলু এটি লিখেছেন, আপনি যদি মেমরি কনসিস্টেন্সি মডেল সম্পর্কে আরও গভীরভাবে জানতে চান, তাহলে এটি শুরু করার জন্য একটি ভালো জায়গা।
http://www.hpl.hp.com/techreports/Compaq-DEC/WRL-95-7.pdf - মেমরি ব্যারিয়ার
- সমস্যাগুলি সংক্ষেপে বর্ণনা করে একটি সুন্দর ছোট নিবন্ধ।
https://en.wikipedia.org/wiki/Memory_barrier - Threads-এর প্রাথমিক তথ্য
- হ্যান্স বোয়েম রচিত, C++ এবং Java-তে মাল্টি-থ্রেডেড প্রোগ্রামিংয়ের ভূমিকা। ডেটা রেস এবং প্রাথমিক সিঙ্ক্রোনাইজেশন পদ্ধতি সম্পর্কে আলোচনা।
http://www.hboehm.info/c++mm/threadsintro.html - Java Concurrency In Practice
- ২০০৬ সালে প্রকাশিত এই বইতে অনেক বিষয় বিস্তারিতভাবে আলোচনা করা হয়েছে। জাভাতে মাল্টি-থ্রেডেড কোড লেখেন এমন যেকোনও ব্যক্তির জন্য অত্যন্ত সাজেস্ট করা হয়।
http://www.javaconcurrencyinpractice.com - JSR-133 (জাভা মেমরি মডেল) সংক্রান্ত প্রায়শই জিজ্ঞাসিত প্রশ্ন
- জাভা মেমরি মডেলের একটি সহজ ভূমিকা, যার মধ্যে সিঙ্ক্রোনাইজেশন, ভোলাটাইল ভেরিয়েবল এবং ফাইনাল ফিল্ডের কনস্ট্রাকশনের ব্যাখ্যা অন্তর্ভুক্ত।
(একটু পুরনো, বিশেষ করে যখন এটি অন্যান্য ভাষা নিয়ে আলোচনা করে।)
http://www.cs.umd.edu/~pugh/java/memoryModel/jsr-133-faq.html - Java মেমরি মডেলে প্রোগ্রাম ট্রান্সফর্মেশনের বৈধতা
- Java মেমরি মডেলের সাথে বাকি থাকা সমস্যার
আরও টেকনিক্যাল ব্যাখ্যা। এইসব সমস্যা ডেটা-রেস-ফ্রি প্রোগ্রামের ক্ষেত্রে প্রযোজ্য নয়।
http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.112.1790&rep=rep1&type=pdf - java.util.concurrent প্যাকেজের ওভারভিউ
java.util.concurrentপ্যাকেজের ডকুমেন্টেশন। পৃষ্ঠার নিচের দিকে "মেমরি কনসিস্টেন্সি প্রপার্টি" নামে একটি বিভাগ আছে যা বিভিন্ন ক্লাসের মাধ্যমে দেওয়া গ্যারান্টির ব্যাখ্যা করে।java.util.concurrentপ্যাকেজের সারাংশ- Java থিওরি ও প্র্যাক্টিস: Java-তে নিরাপদ কনস্ট্রাকশন টেকনিক
- এই নিবন্ধে অবজেক্ট কনস্ট্রাকশনের সময় রেফারেন্স এড়িয়ে যাওয়ার বিপদ সম্পর্কে বিস্তারিত আলোচনা করা হয়েছে এবং থ্রেড-সেফ কনস্ট্রাক্টরের জন্য নির্দেশিকা প্রদান করা হয়েছে।
http://www.ibm.com/developerworks/java/library/j-jtp0618.html - Java থিওরি ও প্র্যাক্টিস: ভোলাটিলিটি ম্যানেজ করা
- Java-তে ভোলাটাইল ফিল্ডের মাধ্যমে আপনি কী করতে পারবেন এবং কী করতে পারবেন না তা বর্ণনা করে একটি সুন্দর নিবন্ধ।
http://www.ibm.com/developerworks/java/library/j-jtp06197.html - “ডবল-চেক করা লকিং কাজ করছে না” সংক্রান্ত ঘোষণা
- বিল পুঘের বিস্তারিত ব্যাখ্যা, যেখানে
volatileবাatomicছাড়াই ডবল-চেক করা লকিং ভেঙে যায়। C/C++ এবং Java অন্তর্ভুক্ত।
http://www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html - [ARM] Barrier Litmus Tests and Cookbook
- ARM SMP সংক্রান্ত সমস্যার আলোচনা, ARM কোডের ছোট স্নিপেটের মাধ্যমে ব্যাখ্যা করা হয়েছে। এই পৃষ্ঠায় দেওয়া উদাহরণগুলি যদি আপনার কাছে খুব বেশি অস্পষ্ট বলে মনে হয় অথবা আপনি যদি DMB নির্দেশের ফর্মাল বিবরণ পড়তে চান, তাহলে এটি পড়ুন। এছাড়াও, এক্সিকিউটেবল কোডে মেমরি ব্যারিয়ারের জন্য ব্যবহৃত নির্দেশাবলী বর্ণনা করে (আপনি যদি ফ্লাইতে কোড জেনারেট করেন তাহলে সম্ভবত এটি কার্যকর)। মনে রাখবেন যে এটি ARMv8-এর আগেকার, যা
অতিরিক্ত মেমরি অর্ডারিং নির্দেশাবলীও সমর্থন করে এবং কিছুটা শক্তিশালী
মেমরি মডেলে স্থানান্তরিত হয়েছে। (বিস্তারিত জানতে "ARMv8-A আর্কিটেকচার প্রোফাইলের জন্য ARM® আর্কিটেকচার রেফারেন্স ম্যানুয়াল ARMv8" দেখুন।)
http://infocenter.arm.com/help/topic/com.arm.doc.genc007826/Barrier_Litmus_Tests_and_Cookbook_A08.pdf - Linux কার্নেল মেমরি ব্যারিয়ার
- Linux কার্নেল মেমরি ব্যারিয়ারের জন্য ডকুমেন্টেশন। কিছু প্রয়োজনীয় উদাহরণ ও ASCII আর্ট অন্তর্ভুক্ত।
http://www.kernel.org/doc/Documentation/memory-barriers.txt - ISO/IEC JTC1 SC22 WG21 (C++ স্ট্যান্ডার্ড) 14882 (C++ প্রোগ্রামিং ল্যাঙ্গুয়েজ), বিভাগ ১.১০ এবং ধারা ২৯ (“অ্যাটমিক অপারেশন লাইব্রেরি”)
- C++ অ্যাটমিক অপারেশন ফিচারের জন্য ড্রাফ্ট স্ট্যান্ডার্ড। এই ভার্সনটি
C++14 স্ট্যান্ডার্ডের কাছাকাছি, যার মধ্যে C++11 থেকে এই অংশে
সামান্য পরিবর্তন অন্তর্ভুক্ত রয়েছে।
http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2015/n4527.pdf
(intro: http://www.hpl.hp.com/techreports/2008/HPL-2008-56.pdf) - ISO/IEC JTC1 SC22 WG14 (C স্ট্যান্ডার্ড) 9899 (C প্রোগ্রামিং ল্যাঙ্গুয়েজ) অধ্যায় ৭.১৬ (“Atomics <stdatomic.h>”)
- ISO/IEC 9899-201x C অ্যাটমিক অপারেশন ফিচারের জন্য ড্রাফ্ট স্ট্যান্ডার্ড।
বিস্তারিত জানতে, পরে প্রকাশিত ত্রুটি সংক্রান্ত রিপোর্টও চেক করুন।
http://www.open-std.org/jtc1/sc22/wg14/www/docs/n1570.pdf - C/C++11 mappings to processors (University of Cambridge)
- Jaroslav Sevcik এবং Peter Sewell-এর C++ অ্যাটমিক থেকে বিভিন্ন সাধারণ প্রসেসর ইনস্ট্রাকশন সেটের অনুবাদের সংগ্রহ।
http://www.cl.cam.ac.uk/~pes20/cpp/cpp0xmappings.html - Dekker’s algorithm
- “কনকারেন্ট প্রোগ্রামিংয়ে মিউচুয়াল এক্সক্লুশন সমস্যার প্রথম জানা সঠিক সমাধান”। উইকিপিডিয়া নিবন্ধে সম্পূর্ণ অ্যালগরিদম আছে, সাথে আধুনিক অপ্টিমাইজিং কম্পাইলার ও SMP হার্ডওয়্যারের সাথে কাজ করার জন্য এটি কীভাবে আপডেট করতে হবে সেই বিষয়ে আলোচনাও আছে।
https://en.wikipedia.org/wiki/Dekker's_algorithm - ARM বনাম Alpha এবং অ্যাড্রেস ডিপেন্ডেন্সি সম্পর্কে কমেন্ট
- Catalin Marinas-এর থেকে arm-kernel মেলিং লিস্টে একটি ইমেল। Includes a nice summary of address and control dependencies.
http://linux.derkeiler.com/Mailing-Lists/Kernel/2009-05/msg11811.html - What Every Programmer Should Know About Memory
- উলরিচ ড্রেপারের লেখা বিভিন্ন ধরনের মেমরি, বিশেষ করে সিপিইউ ক্যাশে সম্পর্কে একটি খুব দীর্ঘ এবং বিস্তারিত নিবন্ধ।
http://www.akkadia.org/drepper/cpumemory.pdf - ARM-এর দুর্বলভাবে সামঞ্জস্যপূর্ণ মেমরি মডেল সম্পর্কে যুক্তি
- এই পেপারটি ARM, Ltd.-এর চং ও ইশতিয়াক লিখেছেন। এতে ARM SMP মেমরি মডেলটি কঠোর কিন্তু অ্যাক্সেসযোগ্য উপায়ে বর্ণনা করার চেষ্টা করা হয়েছে। এখানে ব্যবহৃত “পর্যবেক্ষণযোগ্যতা” শব্দটির সংজ্ঞা এই পেপার থেকে নেওয়া হয়েছে। আবার, এটি ARMv8-এর আগেকার।
http://portal.acm.org/ft_gateway.cfm?id=1353528&type=pdf&coll=&dl=&CFID=96099715&CFTOKEN=57505711 - কম্পাইলার লেখকদের জন্য JSR-133 কুকবুক
- Doug Lea JSR-133 (Java Memory Model) ডকুমেন্টেশনের সাথে এটি লিখেছেন। এতে জাভা মেমরি মডেলের জন্য প্রাথমিক সেট অফ ইমপ্লিমেন্টেশন গাইডলাইন রয়েছে
যা অনেক কম্পাইলার লেখক ব্যবহার করেছেন এবং এখনও
ব্যাপকভাবে উদ্ধৃত করা হয় এবং সম্ভবত ইনসাইট প্রদান করে।
দুর্ভাগ্যবশত, এখানে আলোচনা করা চার ধরনের ফেন্স Android-সমর্থিত আর্কিটেকচারের
জন্য উপযুক্ত নয় এবং উপরে উল্লেখ করা C++11 ম্যাপিং এখন
আরও ভালো সোর্স, এমনকি Java-র জন্যও।
http://g.oswego.edu/dl/jmm/cookbook.html - x86-TSO: x86 মাল্টিপ্রসেসরের জন্য একটি কঠোর এবং ব্যবহারযোগ্য প্রোগ্রামারের মডেল
- x86 মেমরি মডেলের একটি সুনির্দিষ্ট বিবরণ। ARM মেমরি মডেলের
সঠিক বিবরণ দুর্ভাগ্যবশত অনেক বেশি জটিল।
http://www.cl.cam.ac.uk/~pes20/weakmemory/cacm.pdf