অ্

অ্যাটমিকস মধ্যে x86-64 Assembly

১টি অনুশীলনী

অ্যাটমিকস সম্পর্কে

একটি একক নির্দেশনা দেখতে একটি অবিভাজ্য কাজের মতো লাগতে পারে, অথচ আসলে তা একেবারেই তেমন কিছু নয়। ধরুন, মেমোরিতে থাকা একটি মানের সাথে পাঁচ যোগ করছেন:

add qword [rel counter], 5

ভেতরে ভেতরে দেখলে প্রসেসরের মেমোরিতে সরাসরি যোগ করার কোনো উপায় নেই। এটি এই একটি নির্দেশনাকেই ভেঙে তিনটি ছোট ধাপে ফেলে, যাদের বলা হয় মাইক্রো-অপারেশন:

  1. মেমোরি থেকে বর্তমান মানটি পড়া;
  2. একটি রেজিস্টারে এই মানটি পরিবর্তন করা, তার সাথে পাঁচ যোগ করে;
  3. ফলাফলটি আবার লেখা।
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, সেটিকেই সুরক্ষা দিতে হয়।

Note

স্ট্যাকের গোপনীয়তা কোনো সুরক্ষা নয়, বরং একটি রীতি: একটি থ্রেডের স্ট্যাক একই শেয়ার করা মেমোরিতেই থাকে, আর নিজের কোনো লোকালের ঠিকানা দিয়ে দিলে অন্য থ্রেড সেখানে পৌঁছে যেতে পারে। অন্যদিকে রেজিস্টারগুলো হার্ডওয়্যারেই একচেটিয়া: প্রতিটি কোরের নিজস্ব সেট থাকে, আর থ্রেড বদলানোর সময় অপারেটিং সিস্টেম সেগুলো সংরক্ষণ ও পুনরুদ্ধার করে।

x86-64 এই সমস্যার সমাধানে একগুচ্ছ নির্দেশনা দেয়, যেগুলো একটি নির্দেশনাকে অবিভাজ্য করে তোলে। এটি শুধু যে কোর এটিকে প্রসেস করে তার কাছেই নয়, অন্য প্রতিটি কোরের কাছেও একটি একক ইউনিট হিসেবে আচরণ করে। যে অপারেশন একসাথে অটুট থাকে, যা অন্য কোনো কোর ভাঙতে পারে না, তাকে বলা হয় অ্যাটমিক।

Note

অনেক কোর-এ চলা অ্যাপ্লিকেশনকে মাল্টি-থ্রেডেড বলা একটি কমন ব্যাপার। তবে থ্রেড আর কোর এক জিনিস নয়। দুটি থ্রেড একই কোর-এ একসাথে চলতে পারে, একে অন্যের সাথে ইন্টারলিভ করে, বা ভিন্ন কোর-এ সমান্তরালে চলতে পারে।

ইন্টারলিভ করা থ্রেডগুলো একটি রিড-মডিফাই-রাইটকে ঘিরে আগেই রেস করতে পারে, যদি সেটি একাধিক নির্দেশনায় ভাগ হয়ে যায়, কারণ অপারেটিং সিস্টেম যেকোনো দুটি নির্দেশনার মাঝে থ্রেড বদলাতে পারে। তবে একটি নির্দেশনার ভেতরের ফাঁক কেবল সত্যিকারের সমান্তরাল কোডেই উন্মুক্ত হয়। যেহেতু অপারেটিং সিস্টেম কেবল নির্দেশনাগুলোর মাঝে থ্রেড বদলায়, কখনোই একটি নির্দেশনার ভেতরে নয়, তাই একটি একক কোর-এ একটি নির্দেশনা স্বভাবতই নিরাপদ।

একাধিক কোর জুড়ে সত্যিকারের অ্যাটমিসিটি-ই নিচের নির্দেশনাগুলো দেয়।

অ্যাটমিক এক্সচেঞ্জ

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 সবসময়ই অ্যাটমিক। এই সময়টা জুড়ে প্রসেসর ওই লোকেশনটির উপর একটি লক বসায়, যেটি সেই একই পদ্ধতি যা lock প্রিফিক্স অন্য নির্দেশনাগুলোর জন্য ব্যবহার করে, যা পরে বর্ণনা করা হয়েছে।

Caution

অপারেন্ডগুলোর একটি মেমোরি লোকেশন হলে xchg স্বয়ংক্রিয়ভাবেই অ্যাটমিক হয়। এর মানে এই অবস্থায় অপারেশনটি অনেক ধীরও হয়।

আপনার যদি অ্যাটমিসিটির দরকার না থাকে, তাহলে বদলটি বরং সাধারণ mov নির্দেশনা দিয়ে একটি অতিরিক্ত রেজিস্টারের মাধ্যমে করে নিন।

lock প্রিফিক্স

x86-64-এ একটি নির্দেশনাকে অ্যাটমিক করার সবচেয়ে কমন উপায় হলো lock প্রিফিক্স যোগ করা। এটি পড়া, পরিবর্তন আর লেখাকে এক অবিভাজ্য ধাপে জুড়ে দেয়। এর মানে, পুরো প্রক্রিয়া জুড়ে কোরটি মেমোরিটি একচেটিয়াভাবে ধরে রাখে, তাই এর মাঝে অন্য কোনো কোর ওই লোকেশন পড়তে বা লিখতে পারে না।

lock add qword [rel counter], 5 ; the read, the modify, and the write are one step

lock কেবল তখনই কাজ করে যখন ডেস্টিনেশনটি মেমোরি, আর কেবল সেই নির্দেশনাগুলোর উপরেই যারা ওই মেমোরি রিড-মডিফাই-রাইট করে:

  1. পাটিগণিতের অপারেশন, যেমন add, sub, inc, dec, neg;
  2. বিটভিত্তিক অপারেশন, যেমন and, or, xor, not;
  3. বিট অপারেশন bts, btr, btc;
  4. আরও কিছু বিশেষায়িত নির্দেশনা, যেমন xadd ও cmpxchg, যা নিচে বর্ণনা করা হয়েছে।

অন্য যেকোনো নির্দেশনায়, যেমন সাধারণ mov, প্রিফিক্সটি অবৈধ, আর প্রসেসর একটি ইনভ্যালিড-অপকোড ফল্ট তোলে। একা একটি লোড বা একা একটি স্টোরের কোনো প্রিফিক্স লাগে না: স্বাভাবিকভাবে অ্যালাইন করা অ্যাক্সেস নিজে থেকেই অ্যাটমিক।

Caution

একটি লোকেশন একচেটিয়াভাবে ধরে রেখে বাকি সব কোরকে বাইরে রাখা ফ্রি নয়। lock-প্রিফিক্সযুক্ত অপারেশন তার সাধারণ রূপের চেয়ে স্পষ্টভাবে ধীর, আর যখন কয়েকটি কোর একই লোকেশনের জন্য প্রতিদ্বন্দ্বিতা করে তখন আরও ধীর।

এই প্রিফিক্সটি কেবল সেই মেমোরির জন্য রাখা উচিত যেটি একাধিক থ্রেড পরিবর্তন করবে বলে আশা করা হয়। মেমোরিটি শেয়ার করা না হলে, বা সেটি কেবল পড়া হয়, তাহলে এটি ব্যবহার করা এড়িয়ে চলুন।

Note

শেয়ার করা একটি অ্যাটমিক মানকে স্বাভাবিকভাবে অ্যালাইন করে রাখুন, যাতে এটি একটি ক্যাশ লাইনের মধ্যেই থাকে। দুটি ক্যাশ লাইনে ছড়িয়ে থাকা একটি লকড অ্যাক্সেস, যাকে বলা হয় স্প্লিট লক, বৈধ, কিন্তু নাটকীয়ভাবে ধীর, কারণ প্রসেসরকে একাধিক লাইন লক করতে হয়, আর কিছু সাম্প্রতিক সিস্টেম সরাসরি এটিকে ট্র্যাপ করে।

অ্যাটমিক বিট অপারেশন

লকযোগ্য নির্দেশনাগুলোর মধ্যে বিট টেস্টগুলো এই ডকুমেন্টের শেষে থাকা স্পিনলকের সাথে স্বাভাবিকভাবেই মানানসই। bts, btr আর btc প্রত্যেকটি একটি বাছাই করা বিট ক্যারি ফ্ল্যাগে কপি করে, তারপর সেটি সেট, ক্লিয়ার বা ফ্লিপ করে। lock প্রিফিক্সের সাথে এটি এক-বিটের রিড-মডিফাই-রাইট হয়ে যায়: বিটটি অ্যাটমিকভাবে বদলে যায়, আর CF জানায় আগে এটি কী ছিল।

    lock bts qword [rdi], 0 ; set bit 0; CF = the bit's previous value
    setc al                 ; al = 1 when the bit was already set

সেট করার সময় দেখা গেল বিটটি আগেই সেট, তার মানে অন্য একটি থ্রেড সেটি আগে দখল করেছে। এটি হলো একটি সম্পূর্ণ ওয়ার্ডের জন্য xchg যে ফ্ল্যাগ-দখল করে, তারই এক-বিট সংস্করণ।

এক্সচেঞ্জ অ্যান্ড অ্যাড

সাধারণ lock add মেমোরি আপডেট করে, কিন্তু পুরনো মানটি ফেলে দেয়। প্রায়ই পুরনো মানটিই ঠিক যা দরকার, যেমন প্রতিটি থ্রেডকে একটি আলাদা টিকিট নম্বর দেওয়ার জন্য।

xadd নির্দেশনা (x মানে exchange) যোগ করার সময়ই আগের মানটি রিটার্ন করে। এটি যোগফলটি ডেস্টিনেশনে লেখে এবং ডেস্টিনেশনের আসল মানটি সোর্স রেজিস্টারে রেখে দেয়।

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, অর্থাৎ compare-and-exchange, আর এই প্রিমিটিভগুলোর মধ্যে এটিই সবচেয়ে সার্বজনীন।

cmpxchg dest, src একটি অন্তর্নিহিত অ্যাকুমুলেটর হিসেবে rax ব্যবহার করে এবং এটিকে dest-এর সাথে তুলনা করে:

  • যদি dest == rax হয়, তাহলে dest = src এবং ZF = 1।
  • যদি dest != rax হয়, তাহলে rax = dest এবং ZF = 0।

অ্যাকুমুলেটরটি অপারেন্ডের আকার অনুসরণ করে: এক বাইটের জন্য al, আর বড় রূপগুলোর জন্য ax, eax ও rax।

লক্ষ্য করুন, 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 পুরনো পড়া থেকে হিসাব করা একটি মান সংরক্ষণ করতে অস্বীকার করে সেটি ধরে ফেলে।

একটি সূক্ষ্ম বিষয় জেনে রাখা ভালো। cmpxchg যাচাই করে মানটি পড়া মানের সমান কি না, এই নয় যে লোকেশনটিতে কখনো হাত পড়েনি। যদি অন্য একটি থ্রেড ওই ফাঁকের ভেতরে মানটিকে A থেকে B-তে নিয়ে গিয়ে আবার A-তে ফিরিয়ে আনে, তাহলে এক্সচেঞ্জটি তবুও সফল হয়, যদিও ভেতরে সবকিছু বদলে গেছে।

সাধারণ একটি কাউন্টারের জন্য এটি নিরীহ, কারণ সমান সংখ্যা মানেই সমান ভালো একটি সংখ্যা। কিন্তু লক-ফ্রি লিস্টের মাথায় থাকা একটি মেমোরি অ্যাড্রেসের ক্ষেত্রে তা নয়, কারণ একই অ্যাড্রেসটি এখন অন্য কোথাও নিয়ে যেতে পারে। এটিই ABA সমস্যা।

এই সমস্যার প্রচলিত সমাধান হলো মানটির সাথে একটি "স্ট্যাম্প" জুড়ে দেওয়া, যেমন এমন একটি কাউন্টার যা কেবল বাড়তেই থাকে। যেহেতু স্ট্যাম্পটি কেবল এগিয়েই চলে, তাই মিলে যাওয়া একটি স্ট্যাম্প প্রমাণ করে যে মাঝে লোকেশনটিতে হাত পড়েনি, কেবল এই নয় যে এটি অপরিবর্তিত দেখাচ্ছে।

তবে মাল্টি-থ্রেডেড পরিবেশে এটি কাজ করতে হলে মেমোরি অ্যাড্রেস ও তার স্ট্যাম্প দুটোই একসাথে আপডেট করতে হবে। যেহেতু x86-64-এ একটি মেমোরি অ্যাড্রেস একটি পূর্ণ কোয়াডওয়ার্ড, তাই সেটি এবং একইসাথে আরেকটি মান আপডেট করতে cmpxchg যথেষ্ট নয়।

cmpxchg16b নামে একটি ডাবল-প্রস্থের নির্দেশনা আছে, যা অর্থগতভাবে cmpxchg-এর মতোই, কিন্তু একসাথে ১৬ বাইট নিয়ে কাজ করে এবং ওই পরিস্থিতিতে ব্যবহার করা যায়। এটির কোনো সোর্স অপারেন্ড নেই, বরং রিপ্লেসমেন্ট ভ্যালু হিসেবে rcx:rbx ব্যবহার করে। এর একমাত্র অপারেন্ড একটি ১৬ বাইটে অ্যালাইন করা মেমোরি লোকেশন। এই নির্দেশনাটি কেবল rax-এর বদলে rdx:rax-কে অ্যাকুমুলেটর হিসেবে ব্যবহার করে।

মেমোরি অর্ডারিং

এখন পর্যন্ত প্রতিটি অপারেশন একটি একক লোকেশনেই হাত দিয়েছে। যখন থ্রেডগুলো একাধিক লোকেশনের মাধ্যমে সমন্বয় করে, তখন নতুন একটি প্রশ্ন ওঠে: একটি থ্রেডের লেখাগুলো কোন ক্রমে অন্যটির কাছে দৃশ্যমান হয়। এই প্রশ্নের উত্তর দেয় প্রসেসরের মেমোরি অর্ডারিং।

branchless-code কনসেপ্টে এই ধারণাটি এসেছিল যে একটি আধুনিক কোর একবারে একটি করে নির্দেশনা ধরে ধরে হাঁটে না। এটি একসাথে অনেকগুলোকে চলমান রাখে এবং যেখানে পারে আগে এগিয়ে যায়। এর মানে হলো, একটি লেখা প্রোগ্রাম যা ইঙ্গিত করে তার চেয়ে দেরিতে অন্য কোরগুলোর কাছে দৃশ্যমান হতে পারে, অথচ তার পরের নির্দেশনাগুলো ইতিমধ্যেই এগিয়ে গেছে।

x86-64 সাধারণ লোড ও স্টোরের মধ্যে একটি স্ট্রং মেমোরি অর্ডারিং রক্ষা করে, ফলে প্রতিটি কোর-এ:

  1. একটি লোড কখনোই পরের একটি লোডের পরে পুনর্বিন্যস্ত হয় না;
  2. একটি স্টোর কখনোই পরের একটি স্টোরের পরে পুনর্বিন্যস্ত হয় না;
  3. একটি লোড কখনোই পরের একটি স্টোরের পরে পুনর্বিন্যস্ত হয় না।

একমাত্র সম্ভাব্য পুনর্বিন্যাস হলো, একটি স্টোর ভিন্ন একটি অ্যাড্রেসের পরের লোডের পরে সম্পন্ন হয়েছে বলে মনে হওয়া।

lock-প্রিফিক্সযুক্ত নির্দেশনা, বা মেমোরি অপারেন্ডসহ xchg, একটি পূর্ণ ব্যারিয়ার: এর আড়াআড়ি কোনো দিকেই কিছু সরে গেছে বলে মনে হয় না। এই কারণেই বেশিরভাগ পরিস্থিতিতে পূর্ণ অর্ডারিং নিশ্চিত করতে এগুলোই যথেষ্ট।

টিকে থাকা ওই একটি পুনর্বিন্যাসের পেছনের কার্যপ্রণালীটি হলো স্টোর বাফার। এগিয়ে চলতে থাকতে একটি কোর প্রতিটি স্টোরকে ওই বাফারে রেখে দেয়, আর স্টোরটি অন্য কোরগুলোর দেখতে পাওয়া মেমোরিতে পৌঁছানোর আগেই পরের নির্দেশনাগুলোকে এগিয়ে যেতে দেয়। তাই একটি লেখার পরে অন্য একটি লোকেশনের পড়া ক্রমবহির্ভূত বলে মনে হতে পারে: পরের পড়াটি এগিয়ে গেলেও লেখাটি তখনো বাফারে বসে থাকতে পারে। এই মডেলটি, অর্থাৎ ওই লেখা-তারপর-পড়ার ফাঁকটি ছাড়া বাকি সব ক্রমবদ্ধ, তাকে বলা হয় টোটাল স্টোর অর্ডার।

ওই একক ফাঁকটিই একটি স্টোর-তারপর-চেক হ্যান্ডশেক ভেঙে দিতে যথেষ্ট। যদি দুটি থ্রেড প্রত্যেকে নিজের ফ্ল্যাগ লেখে, তারপর অন্যেরটি পড়ে, তাহলে দুটি পড়াই সেট না হওয়া অবস্থায় ফিরে আসতে পারে, কারণ কোনোটির স্টোরই তখনো নিজের স্টোর বাফার ছাড়েনি। mfence নির্দেশনা স্টোর বাফার খালি করে ফাঁকটি বন্ধ করে: এর আগের প্রতিটি লেখা এর পরের যেকোনো পড়ার আগেই দৃশ্যমান হয়ে যায়।

    mov dword [rdi], 1    ; publish: this thread is ready
    mfence                ; the store above must land before the load below
    mov eax, dword [rsi]  ; observe whether the other thread is ready

আরও দুটি সংকীর্ণ ব্যারিয়ার আছে:

  • sfence লেখাগুলোর পারস্পরিক ক্রম ঠিক করে এবং মূলত স্ট্রিমিং, নন-টেম্পোরাল স্টোরের ক্ষেত্রে প্রযোজ্য;
  • lfence পড়াগুলোর ক্রম ঠিক করে।

উপরের সাধারণ অ্যাটমিক প্যাটার্নগুলোর জন্য lock-প্রিফিক্সযুক্ত নির্দেশনাগুলোই ইতিমধ্যেই পূর্ণ অর্ডারিং বহন করে, তাই উপরের মতো সাধারণ স্টোর ও লোড দিয়ে গড়া হ্যান্ডশেকের জন্যই কেবল একটি স্পষ্ট ব্যারিয়ার দরকার।

এই নিশ্চয়তাগুলোর নাম x86 ছাড়িয়ে আরও দূরে যায়। একটি অপারেশনের অ্যাকোয়ার অর্ডারিং থাকে যখন তার পরে কোনো অ্যাক্সেসই তার আগে সরে গেছে বলে মনে হতে পারে না, আর রিলিজ অর্ডারিং থাকে যখন তার আগে কোনো অ্যাক্সেসই তার পরে সরে গেছে বলে মনে হতে পারে না। x86-এ প্রতিটি লোড একটি অ্যাকোয়ার, প্রতিটি স্টোর একটি রিলিজ, আর lock-প্রিফিক্সযুক্ত নির্দেশনা, বা মেমোরিসহ xchg, একসাথে দুটোই।

স্পিনিং আর pause

যে নির্দেশনাগুলো একটি ফ্ল্যাগ সেট করার সাথে সাথে তার আগের অবস্থাও রিটার্ন করে, সেগুলোকে বলা হয় টেস্ট-অ্যান্ড-সেট। এগুলো একটি স্পিনলক-এর ভিত্তি হিসেবে ব্যবহার করা যায়, যা নিশ্চিত করে যে কোডের কোনো অংশে একটি কোরের একচেটিয়া অ্যাক্সেস আছে।

বাইনারি ফ্ল্যাগসহ xchg নির্দেশনা ব্যবহার করে সামগ্রিক অ্যালগরিদমটি এইরকম:

  1. ফ্ল্যাগটি 0 দিয়ে শুরু হয়।
  2. লক নিতে একটি কোর ফ্ল্যাগের মানটি 1-এর সাথে অদলবদল করে।
  3. যদি রিটার্ন করা মানটি 1 হয়, তার মানে লকটি অন্য একটি কোর ধরে রেখেছে। তখন বর্তমান কোরটি অপেক্ষা করে এবং আবার লক নেওয়ার চেষ্টা করে।
  4. যদি রিটার্ন করা মানটি 0 হয়, তার মানে লকটি খালি ছিল। xchg এখন এটি 1 করে সেট করেছে, আর অন্য কোরগুলো অপেক্ষা করবে যতক্ষণ না এই কোরটি এটি ছেড়ে দেয়।
  5. বর্তমান কোরটি কাজ শেষ করলে, ফ্ল্যাগটি 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 ছাড়া একটি স্পিন লুপও সঠিক, শুধু অপচয়ী।

লক্ষ্য করুন, একটি টেস্ট-অ্যান্ড-সেটে xchg-এর মতো একটি নির্দেশনা সবসময়ই ফ্ল্যাগ সেট করে, এমনকি যখন এটি আগেই সেট ছিল তখনও। এর মানে হলো, লকটি তখনো ধরে রাখা থাকার সম্ভাবনাই বেশি হলেও প্রতিটি স্পিনে এটি মেমোরির একচেটিয়া অ্যাক্সেস পায়।

এই অ্যালগরিদমের একটি ভিন্ন রূপ আছে, যাকে বলা হয় টেস্ট-অ্যান্ড-টেস্ট-অ্যান্ড-সেট, যা প্রথমে ফ্ল্যাগটি টেস্ট (পড়া) করে দেখে সেটি আগেই সেট কি না। তারপর লকটি খালি বলে মনে হলেই কেবল এটি টেস্ট-অ্যান্ড-সেট (এক্ষেত্রে xchg) করার চেষ্টা করে:

acquire:
    mov  eax, 1
    xchg dword [rdi], eax ; try to take the lock
    test eax, eax
    jz   .taken           ; got it
.held:
    pause                 ; wait before trying again
    cmp  dword [rdi], 0   ; watch the lock without writing to it
    jne  .held            ; still held
    jmp  acquire          ; looks free: retry
.taken:
    ret

থ্রেডটি কাজ শেষ করলে, mov দিয়ে সাধারণভাবে 0 স্টোর করে লকটি ছেড়ে দিতে পারে। mov ছাড়া আর কিছুই লাগে না, কারণ x86-64-এ লোড ও স্টোর কখনোই পরের একটি স্টোরের পরে পুনর্বিন্যস্ত হয় না। যে স্টোর তার আগের অ্যাক্সেসগুলোকে কখনো ছাপিয়ে যায় না, তার রিলিজ অর্ডারিং আছে বলা হয়, আর x86-এ প্রতিটি সাধারণ স্টোরই তা বহন করে।

Note

যে কোনো নির্দেশনাই যা একটি মেমোরি লোকেশন অ্যাটমিকভাবে টেস্ট ও সেট করে, স্পিনলকের জন্য ব্যবহার করা যায়। যেমন, xchg-এর বদলে lock bts ব্যবহার করা যায়, যা একটি নির্দিষ্ট বিট সেট করার সময়ই দেখে নেয় সেটি আগেই সেট ছিল কি না।

লক্ষ্য করুন, bts যে CF পরিবর্তন করে, তার মতো ফ্ল্যাগগুলো rflags নামের একটি রেজিস্টারের অংশ। এর মানে সেগুলো প্রতিটি থ্রেডের জন্যই একচেটিয়া।

Note

এখানে উল্লেখ করা প্রতিটি নির্দেশনার পূর্ণ রেফারেন্স পাওয়া যায় x86 instruction reference-এ।

GitHub-এর মাধ্যমে সম্পাদনা করুন লিংকটি নতুন একটি উইন্ডো বা ট্যাবে খুলবে

অ্যাটমিকস শিখুন