এটি একটি উদাহরণ দিয়ে সবচেয়ে ভাল চিত্রিত করা হয়।
মনে করুন আমাদের একটি সাধারণ কাজ রয়েছে যা আমরা সমান্তরালভাবে একাধিকবার সম্পাদন করতে চাই এবং আমরা বিশ্বব্যাপী এই কার্যটি কতবার সম্পাদিত হয়েছে তার উদাহরণ রাখতে চাই, উদাহরণস্বরূপ, ওয়েব পৃষ্ঠায় হিট গণনা করা।
যখন প্রতিটি থ্রেড সেই বিন্দুতে পৌঁছে যায় যেখানে এটি গণনা বাড়িয়ে তুলছে, এর কার্যকরকরণটি এরকম দেখতে পাবেন:
- প্রসেসরের রেজিস্ট্রারে মেমরি থেকে হিট সংখ্যা পড়ুন
- সংখ্যাটি বৃদ্ধি করুন।
- সেই নম্বরটি স্মৃতিতে ফিরে লিখুন
মনে রাখবেন যে প্রতিটি থ্রেড এই প্রক্রিয়াটির যে কোনও সময়ে স্থগিত করতে পারে। সুতরাং থ্রেড এ যদি পদক্ষেপ 1 সম্পাদন করে এবং তারপরে স্থগিত হয়ে যায়, থ্রেড বি দ্বারা তিনটি ধাপ সম্পাদন করে, থ্রেড এ পুনরায় চালু করা হবে, তখন এর রেজিস্টারে ভুল সংখ্যক হিট থাকবে: এর নিবন্ধগুলি পুনরুদ্ধার করা হবে, এটি আনন্দের সাথে পুরানো সংখ্যাটিকে বাড়িয়ে তুলবে হিটগুলির, এবং সেই বর্ধিত সংখ্যাটি সংরক্ষণ করুন।
এ ছাড়া, থ্রেড এ স্থগিতের সময়ে অন্য যে কোনও সংখ্যক থ্রেড চলতে পারে, সুতরাং শেষে কাউন্ট থ্রেড লিখতে পারে সঠিক গণনার নীচে।
এই কারণে, এটি নিশ্চিত করা দরকার যে কোনও থ্রেড যদি পদক্ষেপ 1 সম্পাদন করে তবে অন্য কোনও থ্রেডকে ধাপ 1 সম্পাদন করার অনুমতি দেওয়ার আগে এটি অবশ্যই 3 পদক্ষেপটি সম্পাদন করবে, যা এই প্রক্রিয়াটি শুরু করার আগে সমস্ত থ্রেড একক লক পাওয়ার জন্য অপেক্ষা করতে সক্ষম হবে can , এবং প্রক্রিয়াটি সম্পূর্ণ হওয়ার পরেই লকটি মুক্ত করা, যাতে কোডটির এই "সমালোচনা বিভাগ" ভুলভাবে আন্তঃবিভক্ত করা যায় না, ফলস্বরূপ একটি ভুল গণনা ঘটে।
তবে অপারেশনটি যদি পারমাণবিক হত?
হ্যাঁ, যাদুকরী ইউনিকর্ন এবং রামধনুদের জমিতে, যেখানে ইনক্রিমেন্ট অপারেশনটি পারমাণবিক, তবে উপরের উদাহরণের জন্য লকিং প্রয়োজনীয় হবে না।
তবে এটি উপলব্ধি করা জরুরী যে আমরা যাদুকর ইউনিকর্ন এবং রংধনু বিশ্বে খুব কম সময় ব্যয় করি। প্রায় প্রতিটি প্রোগ্রামিং ভাষায়, ইনক্রিমেন্ট অপারেশন উপরের তিনটি ধাপে বিভক্ত হয়। কারণ, এমনকি যদি প্রসেসর একটি পারমাণবিক বৃদ্ধি ক্রিয়াকলাপ সমর্থন করে, তবুও অপারেশনটি উল্লেখযোগ্যভাবে ব্যয়বহুল: এটি মেমরি থেকে পড়তে হবে, সংখ্যাটি সংশোধন করে মেমরিতে ফিরে লিখতে হবে ... এবং সাধারণত পারমাণবিক বৃদ্ধি ক্রিয়াকলাপ একটি অপারেশন যা ব্যর্থ হতে পারে, উপরের সরল ক্রমটি একটি লুপের সাথে প্রতিস্থাপন করতে হবে (আমরা নীচে দেখব)।
যেহেতু, মাল্টিথ্রেডেড কোডেও, অনেকগুলি ভেরিয়েবলগুলি একক থ্রেডে স্থানীয় রাখা হয়, তাই তারা প্রতিটি ভেরিয়েবলকে একক থ্রেডে স্থানীয় মনে করে এবং প্রোগ্রামারগুলিকে থ্রেডের মধ্যে ভাগ করে নেওয়া অবস্থার সুরক্ষার যত্ন নিতে দেয়। বিশেষত প্রদত্ত যে পারমাণবিক ক্রিয়াকলাপগুলি সাধারণত থ্রেডিংয়ের সমস্যাগুলি সমাধান করার পক্ষে পর্যাপ্ত হয় না, যেমন আমরা পরে দেখব।
উদ্বায়ী ভেরিয়েবল
যদি আমরা এই নির্দিষ্ট সমস্যার জন্য লকগুলি এড়াতে চেয়েছিলাম তবে আমাদের প্রথমে বুঝতে হবে যে আমাদের প্রথম উদাহরণে চিত্রিত পদক্ষেপগুলি আসলে আধুনিক সংকলিত কোডে ঘটে না are যেহেতু সংকলকরা ধরে নিচ্ছেন যে কেবল একটি থ্রেড ভেরিয়েবলটি সংশোধন করছে, প্রতিটি থ্রেড ভেরিয়েবলের নিজস্ব ক্যাশেড অনুলিপি রাখবে, যতক্ষণ না প্রসেসরের রেজিস্টার অন্য কোনও কিছুর প্রয়োজন হয়। যতক্ষণ না এটি ক্যাশেড অনুলিপি রয়েছে ততক্ষণ এটি ধরে নিয়েছে এটির মেমোরিতে ফিরে গিয়ে আবার পড়ার দরকার নেই (যা ব্যয়বহুল হবে)। তারা ভেরিয়েবলটিকে মেমরিতে ফিরে না লিখে যতক্ষণ এটি রেজিস্টারে রাখা হয় না write
ভেরিয়েবলটিকে অস্থির হিসাবে চিহ্নিত করে আমরা প্রথম উদাহরণে (উপরে বর্ণিত সমস্ত একই থ্রেডিং সমস্যা সহ) আমরা যে পরিস্থিতিটি দিয়েছিলাম তা ফিরে পেতে পারি , যা সংকলককে বলে যে এই ভেরিয়েবলটি অন্যদের দ্বারা সংশোধন করা হচ্ছে, এবং তাই থেকে পড়তে হবে বা স্মৃতিতে যখনই এটি অ্যাক্সেস করা বা সংশোধন করা হয় তখন লিখিত।
সুতরাং অস্থির হিসাবে চিহ্নিত একটি পরিবর্তনশীল আমাদেরকে পারমাণবিক বৃদ্ধি ক্রিয়াকলাপের দেশে নিয়ে যাবে না, এটি কেবল আমাদের ততই কাছাকাছি পৌঁছেছিল যা আমরা ভেবেছিলাম যে আমরা ইতিমধ্যে ছিলাম were
বর্ধিত পারমাণবিক তৈরি করা
একবার আমরা একটি অস্থির পরিবর্তনশীল ব্যবহার করার পরে, আমরা একটি নিম্ন-স্তরের শর্তসাপেক্ষ সেট অপারেশন ব্যবহার করে আমাদের বর্ধিত ক্রিয়াকলাপকে পারমাণবিক করে তুলতে পারি যা বেশিরভাগ আধুনিক সিপিইউ সমর্থন করে (প্রায়শই তুলনামূলক এবং সেট বা তুলনা এবং সোয়াপ বলে )। উদাহরণস্বরূপ, জাভার অ্যাটমিকআইন্টিজার শ্রেণিতে এই পদ্ধতির ব্যবস্থা নেওয়া হয়েছে :
197 /**
198 * Atomically increments by one the current value.
199 *
200 * @return the updated value
201 */
202 public final int incrementAndGet() {
203 for (;;) {
204 int current = get();
205 int next = current + 1;
206 if (compareAndSet(current, next))
207 return next;
208 }
209 }
উপরের লুপটি বারবার নিম্নলিখিত পদক্ষেপগুলি সম্পাদন করে, 3 তম পদক্ষেপটি সফল না হওয়া পর্যন্ত:
- সরাসরি মেমরি থেকে একটি উদ্বায়ী ভেরিয়েবলের মান পড়ুন।
- যে মান বৃদ্ধি।
- মানটি (মূল স্মৃতিতে) পরিবর্তন করুন এবং কেবল যদি প্রধান মেমরিতে তার বর্তমান মান আমরা প্রথমে একটি বিশেষ পারমাণবিক ক্রিয়াকলাপ ব্যবহার করে যে মানটি শুরুতে পঠিত হয় তার সমান হয়।
যদি পদক্ষেপ 3 ব্যর্থ হয় (কারণ মান 1 ধাপের পরে আলাদা থ্রেড দ্বারা পরিবর্তিত হয়েছিল), এটি আবার মূল স্মৃতি থেকে সরাসরি পরিবর্তনশীলটি পড়ে এবং আবার চেষ্টা করে।
তুলনা এবং অদলবদল অপারেশন ব্যয়বহুল হলেও, এই ক্ষেত্রে লকিং ব্যবহার করার চেয়ে এটি কিছুটা ভাল, কারণ যদি প্রথম থ্রেড 1 পরে কোনও থ্রেড স্থগিত করা হয়, তবে অন্য থ্রেডগুলি যা প্রথম থ্রেডে অপেক্ষা করতে হবে না, যা ব্যয়বহুল প্রসঙ্গে স্যুইচিং প্রতিরোধ করতে পারে। যখন প্রথম থ্রেডটি আবার শুরু হবে, ভেরিয়েবলটি লেখার প্রথম প্রয়াসে এটি ব্যর্থ হবে, তবে ভেরিয়েবলটি পুনরায় পাঠ করে চালিয়ে যেতে সক্ষম হবে, যা লকিংয়ের সাথে প্রয়োজনীয় কনটেক্সট সুইচের চেয়ে কম ব্যয়বহুল।
সুতরাং, আমরা তুলনা এবং অদলবদলের মাধ্যমে প্রকৃত লক ব্যবহার না করে পারমাণবিক বর্ধনের (বা একক ভেরিয়েবলের অন্যান্য ক্রিয়াকলাপের) দেশে যেতে পারি।
সুতরাং যখন লকিং কঠোরভাবে প্রয়োজনীয়?
যদি আপনাকে পারমাণবিক ক্রিয়াকলাপে একাধিক ভেরিয়েবল পরিবর্তন করতে হয়, তবে লক করা প্রয়োজনীয় হবে, আপনি তার জন্য কোনও বিশেষ প্রসেসরের নির্দেশ খুঁজে পাবেন না।
যতক্ষণ আপনি একটি একক ভেরিয়েবলের উপর কাজ করছেন, এবং আপনি ব্যর্থ হওয়ার জন্য এবং ভেরিয়েবলটি পড়তে হবে এবং আবার শুরু করতে হবে এমন যে কোনও কাজের জন্য আপনি প্রস্তুত রয়েছেন, তবে তুলনা-ও-সোয়াপ যথেষ্ট ভাল হবে।
আসুন একটি উদাহরণ বিবেচনা করুন যেখানে প্রতিটি থ্রেড প্রথমে ভেরিয়েবল এক্সে 2 যোগ করে এবং তারপরে এক্সটিকে দুটি দ্বারা গুণিত করে।
যদি এক্স প্রাথমিকভাবে এক হয় এবং দুটি থ্রেড চলতে থাকে তবে আমরা ফলাফলটি আশা করি (((1 + 2) * 2) + 2) * 2 = 16)।
যাইহোক, যদি থ্রেডগুলি আন্তঃবিভক্ত হয় তবে আমরা সমস্ত ক্রিয়াকলাপকেও পারমাণবিক করে তুলতে পারতাম, পরিবর্তে উভয় সংযোজন আগে ঘটতে পারে এবং গুণগুলি পরে আসে, যার ফলস্বরূপ (1 + 2 + 2) * 2 * 2 = 20 হয়।
এটি ঘটে কারণ গুণ এবং সংযোজন ক্রমবর্ধমান ক্রিয়াকলাপ নয়।
সুতরাং, অপারেশনগুলি নিজেরাই পারমাণবিক হওয়াই যথেষ্ট নয়, আমাদের অবশ্যই অপারেশনের পারমাণবিক সমন্বয় তৈরি করতে হবে।
প্রক্রিয়াটি সিরিয়ালাইজ করার জন্য লক ব্যবহার করে আমরা এটি করতে পারি, অথবা আমরা যখন আমাদের গণনা শুরু করি, তখন মধ্যবর্তী পদক্ষেপের জন্য দ্বিতীয় স্থানীয় ভেরিয়েবল এবং এক্স এর মান সঞ্চয় করতে আমরা একটি স্থানীয় ভেরিয়েবল ব্যবহার করতে পারি এবং তারপরে তুলনা-ও-অদলবদল ব্যবহার করতে পারি এক্সের বর্তমান মানটি যদি এক্সের মূল মানের সমান হয় তবেই একটি নতুন মান সেট করুন we যদি আমরা ব্যর্থ হই তবে এক্স পাঠ করে আবার গণনা সম্পাদন করে আবার শুরু করতে হবে।
বেশ কয়েকটি ট্রেড অফ জড়িত রয়েছে: গণনাগুলি দীর্ঘ হওয়ার সাথে সাথে চলমান থ্রেড স্থগিত হয়ে যাওয়ার সম্ভাবনা অনেক বেশি হয়ে যায় এবং আমরা আবার শুরু করার আগেই অন্য থ্রেডের মাধ্যমে মানটি সংশোধন করা হবে যার অর্থ ব্যর্থতা আরও বেশি সম্ভাবনা হয়ে যায়, যার ফলে নষ্ট হয়ে যায় leading প্রসেসরের সময়। খুব দীর্ঘ চলমান গণনা সহ প্রচুর পরিমাণে থ্রেডের চূড়ান্ত ক্ষেত্রে, আমাদের 100 টি থ্রেড চলকটি পড়তে পারে এবং গণনায় জড়িত থাকতে পারে, সেই ক্ষেত্রে কেবলমাত্র প্রথমটি শেষ করা নতুন মান লিখতে সফল হবে, অন্য 99 টি এখনও থাকবে তাদের গণনাগুলি সম্পন্ন করুন, তবে সম্পূর্ণ হওয়ার পরে আবিষ্কার করুন যে তারা মানটি আপডেট করতে পারে না ... যে বিন্দুতে তারা প্রত্যেকেই মানটি পড়বে এবং গণনা শুরু করবে। আমাদের সম্ভবত বাকি 99 টি থ্রেড একই সমস্যার পুনরাবৃত্তি করবে, প্রসেসরের প্রচুর সময় নষ্ট করবে।
লকগুলির মাধ্যমে সমালোচনামূলক বিভাগটির সম্পূর্ণ সিরিয়ালাইজেশন সেই পরিস্থিতিতে আরও ভাল হবে: 99 টি থ্রেডগুলি লকটি না পেলে স্থগিত হয়ে যায় এবং আমরা লকিং পয়েন্টে পৌঁছানোর জন্য প্রতিটি থ্রেড চালাতাম।
সিরিয়ালাইজেশন যদি সমালোচনামূলক না হয় (যেমন আমাদের ইনক্রিমেন্টিং কেস হিসাবে থাকে), এবং নম্বর আপডেট করতে ব্যর্থ হলে যে হিসাবগুলি নষ্ট হবে তা ন্যূনতম হলে তুলনা-ও-সোয়াপ অপারেশনটি ব্যবহার করে একটি উল্লেখযোগ্য সুবিধা পাওয়া যেতে পারে, কারণ সেই অপারেশন লকিংয়ের চেয়ে কম ব্যয়বহুল।