একটি একক ইন্সট্রাকশন দেখতে একটাই অবিভাজ্য কাজের মতো লাগতে পারে, অথচ আসলে তা মোটেই তা নয়। মেমরির একটি মানের সাথে পাঁচ যোগ করার উদাহরণ নেওয়া যাক:
add qword [rel counter], 5
ভেতরে ভেতরে প্রসেসরের মেমরিতে সরাসরি যোগ করার কোনো উপায় নেই। তাই এটি এই একটি ইন্সট্রাকশনকেই ভেঙে দেয় তিনটি ছোট ধাপে, যাদের বলা হয় মাইক্রো-অপারেশন:
mov rax, qword [rel counter] ; 1. load the current value
add rax, 5 ; 2. add five
mov qword [rel counter], rax ; 3. store the result back
এটি একটি কমন প্যাটার্ন, যাকে বলা হয় রিড-মডিফাই-রাইট (RMW)।
লক্ষ্য করুন, পড়া (বা লোড) আর লেখা (বা স্টোর) আলাদা দুটি ঘটনা, তাই এদের মাঝে একটি সময়ের ফাঁক থাকে। একটি একক কোরের ক্ষেত্রে আমরা সাধারণত এটি টের পাই না, কারণ সেখানে প্রতিটি ইন্সট্রাকশন নিশ্চিতভাবেই পরের ইন্সট্রাকশনের আগে তার পুরো প্রভাব ফেলে দেয়। তাই এই ফাঁকটি অদৃশ্য থাকে, আর add একটি অবিভাজ্য একক হিসেবে আচরণ করে।
তবে আধুনিক CPU খুব কমই একক কোরের হয়, আর অ্যাপ্লিকেশনগুলো প্রায়ই একসাথে অনেক কোরেই চলে, তাদের মধ্যে কোনো ক্রমবিন্যাস ছাড়াই। একই মুহূর্তে কয়েকটি কোর চললে, এই কোরের লোডের পরে ও স্টোরের আগে, অন্য একটি কোর ওই ফাঁকের ভেতরে counter পড়তে বা লিখতে পারে। দুটি থ্রেডই একই পুরনো মান লোড করে, দুটিই পাঁচ যোগ করে, আর দুটিই নিজের ফলাফল স্টোর করে। তাহলে দুটি যোগ হয়েছিল, কিন্তু মান বাড়ল মাত্র পাঁচ। একটি আপডেট চুপচাপ হারিয়ে গেল।
এটাই একটি ডেটা রেস, আর মাল্টি-থ্রেডেড কোডে এটি একটি কমন সমস্যা।
সব মানই এভাবে উন্মুক্ত থাকে না। প্রতিটি থ্রেডের নিজের রেজিস্টার ও নিজের স্ট্যাক থাকে, তাই কোনো রেজিস্টারে রাখা মান, বা থ্রেডের স্ট্যাকে থাকা কোনো লোকাল ভ্যারিয়েবল কেবল সেই থ্রেডেরই নিজস্ব, এবং সেটি রেসে জড়াতে পারে না। কেবল থ্রেডগুলো যেসব মেমরি শেয়ার করে, যেমন উপরের counter, সেটিকেই সুরক্ষা দিতে হয়।
x86-64 এই সমস্যার সমাধানে একগুচ্ছ ইন্সট্রাকশন দেয়, যেগুলো একটি ইন্সট্রাকশনকে অবিভাজ্য করে তোলে। এটি শুধু যে কোরটি এটি প্রসেস করে তার কাছেই নয়, বাকি প্রতিটি কোরের কাছেও একটি অবিভাজ্য একক হিসেবে আচরণ করে। যে অপারেশন একসাথে আটকে থাকে, যা অন্য কোনো কোর ভাগ করতে পারে না, তাকে বলা হয় অ্যাটমিক।
অনেক কোরের উপর চলা অ্যাপ্লিকেশনকে মাল্টি-থ্রেডেড বলা কমন। তবে একটি থ্রেড আর একটি কোর এক জিনিস নয়। দুটি থ্রেড একই কোরের উপর একসাথে চলতে পারে, একে অন্যের সাথে জড়িয়ে, আবার আলাদা আলাদা কোরের উপর সমান্তরালে চলতে পারে।
একে অন্যের সাথে জড়িয়ে চলা থ্রেডও একটি রিড-মডিফাই-রাইট জুড়ে রেসে জড়াতে পারে, যদি সেটিকে একাধিক ইন্সট্রাকশনে ভাগ করা হয়, কারণ OS যেকোনো দুটি ইন্সট্রাকশনের মাঝে থ্রেড বদলাতে পারে। তবে একটি একক ইন্সট্রাকশনের ভেতরের ফাঁক কেবল সত্যিকারের সমান্তরাল কোডেই উন্মুক্ত হয়। OS কেবল ইন্সট্রাকশনগুলোর মাঝেই থ্রেড বদলায়, কখনো একটি ইন্সট্রাকশনের ভেতরে নয়, তাই একটি একক কোরের উপর একটি একক ইন্সট্রাকশন স্বভাবতই নিরাপদ।
নিচের ইন্সট্রাকশনগুলো যেটি দেয়, সেটি হলো একাধিক কোর জুড়ে সত্যিকারের অ্যাটমিসিটি।
xchg ইন্সট্রাকশন দুটি অপারেন্ড সোয়াপ করে। ডেস্টিনেশন অপারেন্ডটি সোর্স অপারেন্ডের আগের মানের সমান হয়ে যায়, আর সোর্স অপারেন্ডটি ডেস্টিনেশন অপারেন্ডের আগের মানের সমান হয়ে যায়। ধারণাগতভাবে এটিকে একসাথে ঘটে যাওয়া দুটি mov ইন্সট্রাকশন হিসেবে ভাবা যায়।
বরাবরের মতোই, এটি দুটি রেজিস্টার অপারেন্ড দিয়ে, অথবা একটি মেমরি অপারেন্ড ও একটি রেজিস্টার অপারেন্ড দিয়ে ব্যবহার করা যায়:
mov eax, 1
xchg dword [rdi], eax ; [rdi] = 1, eax = the old value of [rdi]
mov ecx, 2
mov edx, 3
xchg edx, ecx ; edx = 2, ecx = 3
মেমরি অপারেন্ডের সাথে ব্যবহার করলে xchg সবসময়ই অ্যাটমিক।
অপারেন্ডের একটি মেমরি লোকেশন হলে xchg স্বয়ংক্রিয়ভাবেই অ্যাটমিক। এর মানে এই অবস্থায় অপারেশনটি অনেক ধীরও হয়।
অ্যাটমিসিটির দরকার না থাকলে, বদলে সাধারণ mov ইন্সট্রাকশন দিয়ে একটি অতিরিক্ত রেজিস্টারের মাধ্যমে সোয়াপটি করুন।
x86-64-এ একটি ইন্সট্রাকশনকে অ্যাটমিক করার সবচেয়ে কমন উপায় হলো lock প্রিফিক্স যোগ করা। এটি পড়া, পরিবর্তন আর লেখাকে জুড়ে দিয়ে একটি অবিভাজ্য ধাপ বানায়। এর মানে হলো, পুরো প্রক্রিয়া জুড়ে কোরটি মেমরিটিকে একচেটিয়াভাবে ধরে রাখে, তাই মাঝখানে অন্য কোনো কোর সেই লোকেশন পড়তে বা লিখতে পারে না।
lock add qword [rel counter], 5 ; the read, the modify, and the write are one step
lock কেবল তখনই কাজ করে যখন ডেস্টিনেশন মেমরি হয়, আর কেবল সেই ইন্সট্রাকশনগুলোর উপরই কাজ করে যেগুলো সেই মেমরিতে রিড-মডিফাই-রাইট করে:
add, sub, inc, dec, neg;and, or, xor, not;bts, btr, btc;xadd ও cmpxchg, যেগুলোর কথা নিচে বলা হয়েছে।একটি লোকেশন একচেটিয়াভাবে ধরে রেখে বাকি প্রতিটি কোরকে দূরে রাখা খরচ ছাড়া হয় না। lock প্রিফিক্সযুক্ত একটি অপারেশন তার সাধারণ রূপের চেয়ে স্পষ্টভাবেই ধীর, আর একই লোকেশনের জন্য কয়েকটি কোর যখন প্রতিযোগিতা করে তখন আরও ধীর।
এই প্রিফিক্সটি কেবল সেই মেমরির জন্য রাখা উচিত, যেটি একাধিক থ্রেড পরিবর্তন করবে বলে আশা করা হয়। মেমরিটি শেয়ার করা না হলে, বা সেটি কেবল পড়া হয়, তাহলে এটি ব্যবহার করা এড়িয়ে চলুন।
সাধারণ lock add মেমরি আপডেট করে, কিন্তু পুরনো মানটি ফেলে দেয়। অনেক সময় পুরনো মানটিই দরকার হয়, যেমন প্রতিটি থ্রেডকে আলাদা একটি টিকিট নম্বর দিতে।
xadd ইন্সট্রাকশনটি (x মানে এক্সচেঞ্জ) যোগ করার সাথে সাথে আগের মানটিও রিটার্ন করে। এটি যোগফলটি ডেস্টিনেশনে লেখে আর ডেস্টিনেশনের মূল মানটি সোর্স রেজিস্টারে রেখে দেয়।
mov rax, 1
lock xadd qword [rdi], rax ; [rdi] = [rdi] + rax = [rdi] + 1
; rax = the old value of [rdi]
lock প্রিফিক্সের সাথে এটি একটি অ্যাটমিক ফেচ-অ্যান্ড-অ্যাড। একই কাউন্টারে অনেক থ্রেড এটি চালালে প্রতিটি কল আলাদা একটি পুরনো মান রিটার্ন করে।
lock add-এর মতোই, কলের ঠিক সংখ্যাতেই কাউন্টার শেষ হয়। তবে lock add-এর বিপরীতে, এখানে মাঝের প্রতিটি মানও রিটার্ন হয়, প্রতিটি কলকারী একটি করে পায়।
xadd যোগ করে আর xchg উপরিলিখে দেয়, কিন্তু কোনোটিই নতুন মানকে বর্তমান মানের উপর নির্ভরশীল করে, আর ভেতরে কিছু না বদলালে তবেই তা প্রয়োগ করতে পারে না। সেই শর্তসাপেক্ষ আপডেটটাই cmpxchg, বা কম্পেয়ার-অ্যান্ড-এক্সচেঞ্জ দেয়, আর এই প্রিমিটিভগুলোর মধ্যে এটিই সবচেয়ে ব্যাপক।
cmpxchg dest, src একটি অন্তর্নিহিত অ্যাকুমুলেটর হিসেবে rax ব্যবহার করে, আর সেটিকে dest অপারেন্ডের সাথে তুলনা করে:
dest == rax হলে dest = src এবং ZF = 1।dest != rax হলে rax = dest এবং ZF = 0।লক্ষ্য করুন, dest কেবল তখনই আপডেট হয় যখন সেটি আগে rax রেজিস্টারে লোড করা প্রত্যাশিত মানের সমান হয়। এই সমতা নিশ্চিত করে যে dest এখনো সেই মানটিই ধরে আছে যা থেকে নতুন মানটি হিসাব করা হয়েছিল, তাই পুরনো মান পড়ে করা আপডেট কখনোই প্রয়োগ হয় না। এটিই cmpxchg ইন্সট্রাকশনটিকে একটি অ্যাটমিক আপডেটের বিল্ডিং ব্লক বানায়, যাকে কম্পেয়ার-অ্যান্ড-সোয়াপ (CAS)-ও বলা হয়:
mov rax, qword [rdi] ; rax = the value we expect to find
.retry:
lea rcx, [rax + 10] ; rcx = the new value we want to install
lock cmpxchg qword [rdi], rcx ; if [rdi] still equals rax, store rcx and set ZF
; otherwise reload rax with the current value, clear ZF
jnz .retry ; ZF is cleared, so another thread won the race. Recompute and retry
এই রিট্রাই লুপটিই লক-ফ্রি আপডেটের প্রাণ। পড়া আর কম্পেয়ার-অ্যান্ড-এক্সচেঞ্জের মাঝের ফাঁকটিই ঠিক সেই মুহূর্ত যখন অন্য একটি থ্রেড মাঝখানে ঢুকে পড়তে পারে, আর পুরনো মান পড়ে হিসাব করা কোনো মান স্টোর করতে অস্বীকার করে cmpxchg সেটি ধরে ফেলে।
এখন পর্যন্ত প্রতিটি অপারেশন কেবল একটি লোকেশনেই হাত দিয়েছে। থ্রেডগুলো যখন একাধিক লোকেশনের মাধ্যমে সমন্বয় করে, তখন নতুন একটি প্রশ্ন ওঠে: একটি থ্রেডের লেখাগুলো কোন ক্রমে অন্যটির কাছে দৃশ্যমান হয়। এই প্রশ্নের উত্তর দেয় প্রসেসরের মেমরি অর্ডারিং।
ব্রাঞ্চলেস-কোড কনসেপ্টে যে ধারণাটি এসেছিল, তা হলো একটি আধুনিক কোর একবারে একটি করে ইন্সট্রাকশন ধরে ধীরে ধীরে এগোয় না। এটি একসাথে অনেক ইন্সট্রাকশন চালু রাখে আর যেখানে পারে সেখানে আগে এগিয়ে যায়। এর মানে হলো, প্রোগ্রাম যা বোঝায় তার চেয়ে দেরিতেও একটি লেখা অন্য কোরগুলোর কাছে দৃশ্যমান হতে পারে, অথচ তার পরের ইন্সট্রাকশনগুলো ইতিমধ্যেই এগিয়ে গেছে।
x86-64 সাধারণ লোড ও স্টোরের মধ্যে একটি স্ট্রং মেমরি অর্ডারিং মেনে চলে, যাতে প্রতিটি কোরের ক্ষেত্রে:
একমাত্র সম্ভাব্য পুনর্বিন্যাস হলো, একটি স্টোর অন্য একটি অ্যাড্রেসের পরের লোডের পরে সম্পন্ন হয়েছে বলে মনে হওয়া।
lock প্রিফিক্সযুক্ত একটি ইন্সট্রাকশন, বা মেমরি অপারেন্ডসহ একটি xchg, একটি পূর্ণ ব্যারিয়ার: কোনোকিছুই এর আড়াআড়ি কোনো দিকেই সরে যেতে পারে বলে মনে হয় না। এজন্যই বেশিরভাগ পরিস্থিতিতে পূর্ণ অর্ডারিং নিশ্চিত করার জন্য এগুলোই যথেষ্ট।
pause
যে ইন্সট্রাকশনগুলো একটি ফ্ল্যাগ সেট করার সাথে সাথে তার আগের অবস্থাও রিটার্ন করে, তাদের বলা হয় টেস্ট-অ্যান্ড-সেট। এগুলোকে একটি স্পিনলকের ভিত্তি হিসেবে ব্যবহার করা যায়, যাতে একটি কোর কোডের কোনো অংশে একচেটিয়া প্রবেশাধিকার পায়।
বাইনারি ফ্ল্যাগের সাথে xchg ইন্সট্রাকশন ব্যবহার করে সামগ্রিক অ্যালগরিদমটি এমন:
0 দিয়ে।1 দিয়ে সোয়াপ করে।1 হলে, এর মানে লকটি অন্য একটি কোর ধরে রেখেছে। তখন বর্তমান কোরটি অপেক্ষা করে এবং আবার লক নেওয়ার চেষ্টা করে।0 হলে, এর মানে লকটি খালি ছিল। xchg এখন এটিকে 1 করে রেখেছে, আর অন্য কোরগুলো অপেক্ষা করবে যতক্ষণ না এই কোরটি এটিকে ছেড়ে দেয়।0 দিয়ে আপডেট করলেই লকটি ছেড়ে দেওয়া হয়।acquire:
mov eax, 1
xchg dword [rdi], eax ; try to take the lock; eax = its old value
test eax, eax
jnz .held ; old value was 1: someone else holds it
ret ; old value was 0: the lock is ours
.held:
pause ; wait before trying again
jmp acquire
অপেক্ষার লুপে থাকা pause ইন্সট্রাকশন কোডটি কী হিসাব করে তা বদলায় না। এটি প্রসেসরকে ইঙ্গিত দেয় যে এটি একটি স্পিন-ওয়েট। তখন CPU অপেক্ষারত থ্রেডের বিদ্যুৎ খরচ কমাতে পারে এবং একই কোর শেয়ার করা অন্য একটি থ্রেডকে জায়গা ছেড়ে দিতে পারে। pause ছাড়া একটি স্পিন লুপ এখনো সঠিকই থাকে, শুধু অপচয় বেশি হয়।
থ্রেডটি কাজ শেষ করলে, mov দিয়ে সাধারণ একটি স্টোর করে 0 লিখেই লকটি ছেড়ে দিতে পারে। mov ছাড়া আর কিছুই দরকার নেই, কারণ x86-64-এ লোড ও স্টোর কখনোই তার পরে আসা স্টোরের পরে পুনর্বিন্যস্ত হয় না। যে স্টোর তার আগের অ্যাক্সেসগুলোকে কখনো ছাপিয়ে যায় না, তাকে বলা হয় রিলিজ অর্ডারিং আছে, আর x86-এ প্রতিটি সাধারণ স্টোর তা বহন করে।
যে কোনো ইন্সট্রাকশন যা অ্যাটমিকভাবে একটি মেমরি লোকেশন টেস্ট করে সেট করে, সেটি স্পিনলকের জন্য ব্যবহার করা যায়। উদাহরণস্বরূপ, xchg-এর বদলে lock bts ব্যবহার করা যায়, যাতে একটি নির্দিষ্ট বিট আগেই সেট ছিল কিনা দেখতে দেখতে সেটি সেট করা হয়।
লক্ষ্য করুন, bts যে CF বদলায়, তার মতো ফ্ল্যাগগুলো একটি রেজিস্টার rflags-এর অংশ। এর মানে এগুলো প্রতিটি থ্রেডের জন্যই একচেটিয়া।
Grand Marquee-তে উদ্বোধনের রাত, আর গোটা কর্মীদল একসাথে কাজ করছে। প্রতিটি দরজা দিয়ে ভিড় ঢুকে পড়ছে, বক্স অফিসের বেশ কয়েকটি জানালা একই টিকিটের রোল থেকে বিক্রি করছে, প্রিমিয়ার প্রায় হাউসফুল, আর ওপরে প্রজেকশন বুথে রিলগুলো চলতেই থাকতে হবে।
কর্মীরা যা কিছু শেয়ার করে, তার সবই থাকে একটি মেমরি অ্যাড্রেসে, আর টেস্টগুলো একই সময়ে বেশ কয়েকটি থ্রেড থেকে আপনার ফাংশনগুলো কল করে। যে মান আলাদা আলাদা ধাপে পড়া হয়, বদলানো হয়, আর আবার লেখা হয়, সেই লোডের চাপে তার আপডেট হারিয়ে যাবে। তাই প্রতিটি ফাংশনকে নিজের আপডেট অ্যাটমিক করতে হবে।
এই অনুশীলনীর সব শেয়ার করা মান ৬৪-বিট সাইনড ইন্টিজার।
আপনার সামনে পাঁচটি কাজ আছে।
এই অনুশীলনীর আপডেটগুলো অ্যাটমিক ইনস্ট্রাকশন দিয়ে করা উচিত। টেস্টগুলো আপনার ফাংশনগুলো একসাথে চালায় এবং যাচাই করে যে কোনো আপডেট হারায়নি, তাই একটি অ-অ্যাটমিক রিড-মডিফাই-রাইট প্রায় নিশ্চিতভাবেই এগুলোতে ফেল করবে।
দরজাগুলো খোলা, আর প্রতিটি দরজায় একজন ইউশার টিকিট ছিঁড়ছে। যে দলকেই ঢোকানো হয়, তাকে একটি শেয়ার করা প্রবেশ-হিসাবে গোনা হয়, আর প্রতিটি দরজা একসাথে গুনলেও সর্বমোট হিসাব ঠিকই থাকতে হবে।
admit_group ফাংশনটি বাস্তবায়ন করুন, যা অ্যাটমিকভাবে হিসাবে একটি দলের আকার যোগ করে।
এই ফাংশনটি আর্গুমেন্ট হিসেবে নেয়, এই ক্রমে:
admissions: শেয়ার করা প্রবেশ-হিসাবের মেমরি অ্যাড্রেস, একটি ৬৪-বিট সাইনড ইন্টিজার।group_size: দলে থাকা মানুষের সংখ্যা, একটি ৬৪-বিট সাইনড ইন্টিজার, সর্বদা 0-এর চেয়ে বড়।admissions = 120, group_size = 4
// admissions is now 124
টেস্টগুলো একসাথে বেশ কয়েকটি দরজা দিয়ে দল ঢোকায়। শেষ হিসাবটি অবশ্যই প্রতিটি ঢোকানো দলের যোগফলের সমান হতে হবে।
এই ফাংশনটির কোনো রিটার্ন ভ্যালু নেই। হিসাবটি জায়গাতেই আপডেট হতে হবে।
প্রজেক্টরে একবারে একটি মাত্র রিল লাগানো যায়। পরের রিল লাগানো আর এখনকারটা খুলে নেওয়া একটিই নড়াচড়া হতে হবে: মেশিনে দুজন প্রজেকশনিস্ট থাকলে, আলাদা দুটি ধাপে একই রিল দুজনের হাতে চলে যেতে পারে, বা একটা মেঝেতে পড়ে যেতে পারে।
change_reel ফাংশনটি বাস্তবায়ন করুন, যা অ্যাটমিকভাবে পরের রিল লাগায় এবং আগে লাগানো রিলটি রিটার্ন করে।
এই ফাংশনটি আর্গুমেন্ট হিসেবে নেয়, এই ক্রমে:
mounted: লাগানো রিলের আইডেন্টিফায়ারের মেমরি অ্যাড্রেস, একটি ৬৪-বিট সাইনড ইন্টিজার।next_reel: যে রিলটি লাগাতে হবে তার আইডেন্টিফায়ার, একটি ৬৪-বিট সাইনড ইন্টিজার।mounted = 7, next_reel = 9
// => 7, and mounted is now 9
এই ফাংশনটি আগে লাগানো রিলের আইডেন্টিফায়ার একটি ৬৪-বিট সাইনড ইন্টিজার হিসেবে রিটার্ন করে। টেস্টগুলো একইসাথে একই মাউন্ট দিয়ে কয়েকটি থ্রেডকে রিল বদল করায়, আর খতিয়ে দেখে যে হাতে দেওয়া প্রতিটি রিল ঠিক একবারই ফিরে আসে।
প্রতিটি জানালা বিক্রি করে বিক্রি হওয়া টিকিটের একটিই শেয়ার করা গণনা থেকে। শুধু হিসাবে যোগ করা, যেমনটা দরজাগুলো করে, এখানে যথেষ্ট নয়: প্রতিটি বিক্রিকে নিজের পাওয়া নম্বরটাও জেনে নিতে হবে। কোনো দুটি বিক্রি একই নম্বর পেতে পারে না, আর ভিড় শেষে হিসাবটি অবশ্যই বিক্রি হওয়া টিকিটের সংখ্যার সঙ্গে হুবহু মিলতে হবে।
sell_ticket ফাংশনটি বাস্তবায়ন করুন, যা অ্যাটমিকভাবে হিসাবে এক যোগ করে এবং এইমাত্র বিক্রি হওয়া টিকিটের নম্বর রিটার্ন করে।
টিকিটের নম্বর হলো এই বিক্রির পরে হিসাবটি: হিসাব যখন 41 ছিল, তখন এই বিক্রিটি হলো টিকিট 42।
এই ফাংশনটি আর্গুমেন্ট হিসেবে নেয়:
tickets_sold: বিক্রি হওয়া টিকিটের শেয়ার করা গণনার মেমরি অ্যাড্রেস, একটি ৬৪-বিট সাইনড ইন্টিজার।tickets_sold = 0
// => 1, and tickets_sold is now 1
tickets_sold = 41
// => 42, and tickets_sold is now 42
এই ফাংশনটি টিকিটের নম্বর একটি ৬৪-বিট সাইনড ইন্টিজার হিসেবে রিটার্ন করে। টেস্টগুলো একসাথে বেশ কয়েকটি জানালা থেকে বিক্রি করে: প্রতিটি রিটার্ন করা নম্বর আলাদা হতে হবে, আর শেষ হিসাবটি অবশ্যই বিক্রির সংখ্যার সমান হতে হবে।
প্রিমিয়ারে নির্দিষ্ট সংখ্যক আসন আছে, আর প্রতিটি জানালা সেটাই বিক্রি করছে। আসন থাকা পর্যন্তই কেবল একটি বিক্রি হয়ে যেতে পারে, আর স্ক্রিনিংয়ে কখনোই আসনের বেশি টিকিট বিক্রি হতে পারবে না, এমনকি প্রতিটি জানালা একই মুহূর্তে শেষ আসনটির জন্য হাত বাড়ালেও নয়। হিসাব দেখা আর তারপর আলাদা দুটি ধাপে সেটিতে যোগ করা, মাঝখানে একটা ফাঁক রেখে দেয়; তাই দেখা আর আপডেট একসাথেই সফল হতে হবে, নয়তো একসাথেই ব্যর্থ।
claim_seat ফাংশনটি বাস্তবায়ন করুন, যা একটি আসন এখনো খালি থাকলে অ্যাটমিকভাবে পরের আসনটি দখল করে।
কোনো আসন দখল করা হলে ফাংশনটি আসনটির নম্বর রিটার্ন করে, যা এই দাবির পরে দখল করা আসনের সংখ্যা।
স্ক্রিনিং হাউসফুল হলে এটি 0 রিটার্ন করে এবং হিসাব অপরিবর্তিত রাখে।
এই ফাংশনটি আর্গুমেন্ট হিসেবে নেয়, এই ক্রমে:
seats_taken: আগেই দখল করা আসনের গণনার মেমরি অ্যাড্রেস, একটি ৬৪-বিট সাইনড ইন্টিজার, কখনোই capacity-র বেশি নয়।capacity: স্ক্রিনিংয়ে থাকা আসনের সংখ্যা, একটি ৬৪-বিট সাইনড ইন্টিজার, সর্বদা 0-এর চেয়ে বড়।seats_taken = 0, capacity = 150
// => 1, and seats_taken is now 1
seats_taken = 82, capacity = 150
// => 83, and seats_taken is now 83
seats_taken = 149, capacity = 150
// => 150, and seats_taken is now 150
seats_taken = 150, capacity = 150
// => 0, and seats_taken stays 150
এই ফাংশনটি আসন নম্বর, বা স্ক্রিনিং হাউসফুল হলে 0, একটি ৬৪-বিট সাইনড ইন্টিজার হিসেবে রিটার্ন করে।
প্রজেকশন বুথে একজনই ধরে।
মেমরিতে একটি ওয়ার্ড বলে দেয় বুথটি দখল করা আছে কি না: খালি হলে 0, দখল করা থাকলে 1।
সারারাত কর্মীরা আসে, প্রত্যেকের ভেতরে করার একটা কাজ থাকে: লগবইয়ে সই করা, প্রজেক্টরে ফিল্ম পরানো, রিল গুছিয়ে রাখা।
কাজ যাই হোক, বুথের নিয়ম একই, আর সেটি কার্যকর করা আপনার দায়িত্ব: একবারে একজনই ভেতরে থাকবে, তার কাজ শুরু থেকে শেষ পর্যন্ত।
visit_booth ফাংশনটি বাস্তবায়ন করুন, যা:
বুথ যদি অন্য কেউ দখল করে থাকে, তবে pause দিয়ে ভদ্রভাবে অপেক্ষা করা উচিত।
এই ফাংশনটি আর্গুমেন্ট হিসেবে নেয়, এই ক্রমে:
booth: দখল-বোঝানো ওয়ার্ডের মেমরি অ্যাড্রেস, একটি ৬৪-বিট সাইনড ইন্টিজার, যা হয় 0 নয়তো 1।task: দর্শনার্থীর কাজ, এমন একটি ফাংশন যা কোনো প্যারামিটার নেয় না এবং যার কোনো রিটার্ন ভ্যালু নেই।মনে রাখবেন, টেস্টগুলো এমন কাজ চালায় যেগুলো বুথ আর একটি শেয়ার করা লগবইয়ের দিকে তাকায়। এর মানে হলো পুরো কাজের সময় জুড়ে বুথ দখল করা থাকতে হবে, প্রতিটি ভিজিট তার কাজ ঠিক একবারই চালাবে, আর একসাথে বেশ কয়েকজন দর্শনার্থী এলে লগবই হুবহু মিলতে হবে।
দর্শনার্থীর কাজটি একটি সাধারণ ফাংশন, যা কলিং কনভেনশন মেনে চলে:
যেকোনো একটি নিয়ম ভাঙলে চেক ফেল করার বদলে ক্র্যাশ হয়ে পড়তে পারে।
Exercism-এ সাইন আপ করুন, x86-64 Assembly ট্র্যাকের 22টি কনসেপ্ট130টি অনুশীলনী আর সত্যিকারের মানুষের মেন্টরিং দিয়ে শিখুন ও দক্ষ হয়ে উঠুন, সম্পূর্ণ বিনামূল্যে।