পাঠ ১৩ · ৫৬-এর মধ্যে · মডিউল ৩
Home / Courses / Operating Systems (OS) / মাল্টিপ্রসেসর শিডিউলিং

মাল্টিপ্রসেসর শিডিউলিং

Multiprocessor scheduling
৭ মিনিট পড়া মধ্যম · Intermediate Python কোডসহ সম্পূর্ণ বাংলায়

এই পাঠে যা শিখবেন

  • একক-CPU শিডিউলিং থেকে মাল্টি-কোর শিডিউলিংয়ে যাওয়ার সময় কী নতুন সমস্যা তৈরি হয়
  • লোড ব্যালেন্সিং — পুশ মাইগ্রেশন বনাম পুল মাইগ্রেশন
  • প্রসেসর অ্যাফিনিটি (সফট ও হার্ড) এবং কেন ক্যাশ-ওয়ার্মথ গুরুত্বপূর্ণ
  • সিমেট্রিক বনাম অ্যাসিমেট্রিক মাল্টিপ্রসেসিং
  • Python দিয়ে একটি বাস্তব লোড-ব্যালেন্সিং সিমুলেশন — অসম কোর-লোড থেকে ব্যালেন্সড অবস্থায় পৌঁছানো

১ · একক CPU থেকে মাল্টি-কোরে

L09 থেকে L12 পর্যন্ত আমরা যা শিখেছি তা ধরে নিয়েছিল মাত্র একটি CPU আছে — সেই একটি CPU-এর জন্য কোন প্রসেস পরবর্তীতে চলবে তা ঠিক করাই ছিল শিডিউলারের একমাত্র কাজ। কিন্তু আধুনিক প্রায় সব ডিভাইসেই একাধিক CPU কোরCoreএকটি একক প্রসেসর চিপের ভেতরে থাকা স্বাধীন প্রসেসিং ইউনিট, যা একই সাথে ভিন্ন ভিন্ন প্রসেসের ইন্সট্রাকশন চালাতে পারে। থাকে — এখন প্রশ্নটা শুধু "পরবর্তীতে কে চলবে" নয়, বরং "পরবর্তীতে কে, কোন কোরে চলবে, এবং কাজ কীভাবে কোরগুলোর মধ্যে সমানভাবে ভাগ হবে।"

২ · লোড ব্যালেন্সিং — পুশ বনাম পুল মাইগ্রেশন

লোড ব্যালেন্সিং (Load Balancing)Load Balancingএকাধিক CPU কোরের মধ্যে কাজ সমানভাবে বণ্টিত রাখার প্রক্রিয়া, যাতে কোনো কোর অলস বসে না থাকে অথচ অন্য কোর ওভারলোডেড না হয়। নিশ্চিত করে কোনো একটি কোর অতিরিক্ত ব্যস্ত থাকা অবস্থায় আরেকটি কোর অলস বসে না থাকে। দুটি পদ্ধতি — পুশ মাইগ্রেশন: একটি কেন্দ্রীয় প্রক্রিয়া নিয়মিতভাবে ব্যস্ত কোর থেকে অলস কোরে প্রসেস সরিয়ে দেয়। পুল মাইগ্রেশন: অলস কোর নিজেই সক্রিয়ভাবে ব্যস্ত কোরের কিউ থেকে কাজ "চুরি" করে নেয় (work stealing)।

৩ · প্রসেসর অ্যাফিনিটি

একটি প্রসেস একবার কোনো কোরে চললে, তার ডেটার কিছু অংশ সেই কোরের ক্যাশে থেকে যায় — একে অন্য কোরে সরালে সেই ক্যাশ-উষ্ণতা (warm cache) নষ্ট হয়ে যায় এবং নতুন কোরে ক্যাশ মিস (ব্যয়বহুল) বেড়ে যায়। তাই শিডিউলার যতটা সম্ভব একটি প্রসেসকে একই কোরে রাখার চেষ্টা করে — একে প্রসেসর অ্যাফিনিটি (Processor Affinity)Processor Affinityএকটি প্রসেসকে একই CPU কোরে রাখার প্রবণতা বা প্রয়োজনীয়তা, যাতে ক্যাশ-উষ্ণতার সুবিধা বজায় থাকে। বলা হয়। সফট অ্যাফিনিটি একটি পছন্দ মাত্র (সম্ভব হলে একই কোরে রাখা হবে), আর হার্ড অ্যাফিনিটি একটি স্পষ্ট বাধ্যবাধকতা (প্রসেসটিকে নির্দিষ্ট কোর(গুলো) ছাড়া অন্য কোথাও চালানোই যাবে না)।

সিমেট্রিক মাল্টিপ্রসেসিং (SMP)
প্রতিটি কোর নিজে নিজে শিডিউলিং সিদ্ধান্ত নেয় (নিজস্ব বা শেয়ার্ড রেডি কিউ দেখে) — কোনো একক বটলনেক নেই।
অ্যাসিমেট্রিক মাল্টিপ্রসেসিং
একটি নির্দিষ্ট কোর বাকি সবার জন্য শিডিউলিং সিদ্ধান্ত নেয় — সরল কিন্তু সম্ভাব্য বটলনেক।
একটি বাস্তব ট্রেড-অফ

লোড ব্যালেন্সিং ও প্রসেসর অ্যাফিনিটি প্রায়শই একে অপরের বিপরীতমুখী টান তৈরি করে — লোড ব্যালেন্সিং একটি প্রসেসকে অন্য কোরে সরাতে চায় (সমতার জন্য), কিন্তু অ্যাফিনিটি তাকে একই কোরে রাখতে চায় (ক্যাশ-উষ্ণতার জন্য)। বাস্তব শিডিউলার এই দুটির মধ্যে ভারসাম্য রাখে — সাধারণত ছোটখাটো অসামঞ্জস্যের জন্য প্রসেস সরায় না, কিন্তু বড় অসামঞ্জস্য হলে অ্যাফিনিটির সুবিধা ছেড়ে দিয়ে হলেও মাইগ্রেট করে।

৪ · একটি লোড-ব্যালেন্সিং সিমুলেশন

নিচের কোড সেলে প্রতিটি কোরের জন্য একটি "কিউ" (বার্স্ট টাইমের তালিকা) আছে। rebalance() ফাংশনটি সবচেয়ে ব্যস্ত ও সবচেয়ে অলস কোরের লোডের পার্থক্য একটি থ্রেশহোল্ডের চেয়ে বেশি হলে, ব্যস্ত কোর থেকে ছোট একটি কাজ অলস কোরে সরিয়ে দেয় (পুল মাইগ্রেশনের মতো — অলস কোর কাজ "টেনে নেয়")।

Python
# সরল লোড-ব্যালেন্সিং সিমুলেশন -- toy in-memory ডেটা, real CPU cores নয়
cores = {
    "core0": [8, 7],
    "core1": [3],
    "core2": [2, 2],
}

def total_load(core_queue):
    return sum(core_queue)

def show(cores):
    for name, q in cores.items():
        print(f"  {name}: {q} -> load = {total_load(q)}")

def rebalance(cores, threshold=3):
    loads = {name: total_load(q) for name, q in cores.items()}
    busiest = max(loads, key=loads.get)
    idlest = min(loads, key=loads.get)
    if loads[busiest] - loads[idlest] <= threshold or not cores[busiest]:
        return False
    job = min(cores[busiest])       # সবচেয়ে ছোট বার্স্ট মাইগ্রেট করি -- ওভারশুট এড়াতে
    cores[busiest].remove(job)
    cores[idlest].append(job)
    print(f"  মাইগ্রেশন: {busiest} (load {loads[busiest]}) -> {idlest} (load {loads[idlest]}), burst={job}")
    return True

print("--- প্রাথমিক অবস্থা (আনব্যালান্সড) ---")
show(cores)

round_num = 1
while rebalance(cores, threshold=3) and round_num <= 5:
    print(f"\n--- রিব্যালান্সিং রাউন্ড {round_num}-এর পর ---")
    show(cores)
    round_num += 1

print("\n--- চূড়ান্ত (ব্যালান্সড) অবস্থা ---")
show(cores)

    
লক্ষ্য করুন — প্রথম রাউন্ডে সবচেয়ে ব্যস্ত core0 (load ১৫) থেকে সবচেয়ে অলস core1 (load ৩)-এ একটি কাজ সরে, যা core0-কে load ৮-এ নামায় এবং core1-কে load ১০-এ তোলে। এরপর core1 নতুন সবচেয়ে ব্যস্ত কোর হয়ে যায় এবং পরবর্তী রাউন্ডে core2-তে একটি কাজ সরায়। শেষে তিনটি কোরের লোড (৮, ৭, ৭) থ্রেশহোল্ড (৩)-এর মধ্যে চলে আসে — রিব্যালান্সিং থেমে যায়।
মূল কথা · Key takeaway

মাল্টিপ্রসেসর শিডিউলিং শুধু "কে পরে চলবে" তা নয়, বরং "কোন কোরে চলবে এবং কতবার সরানো হবে" — এই সিদ্ধান্তগুলোও যোগ করে। লোড ব্যালেন্সিং সমতা আনে, কিন্তু প্রতিটি মাইগ্রেশনের একটি প্রকৃত খরচ আছে (ক্যাশ-উষ্ণতা হারানো) — তাই বাস্তব শিডিউলার শুধুমাত্র প্রকৃত বড় অসামঞ্জস্য দেখা দিলেই মাইগ্রেট করে, প্রতিটি ছোট পার্থক্যের জন্য নয়।

ভাবনার প্রশ্ন

প্রতিটি প্রশ্ন নিজে কিছুক্ষণ ভাবুন — তারপর "→ উত্তর" চাপুন।

প্র ০১ লোড ব্যালেন্সিং ও প্রসেসর অ্যাফিনিটি — এই দুটো লক্ষ্য কেন একে অপরের বিপরীতমুখী?

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

প্র ০২ পুশ মাইগ্রেশন ও পুল মাইগ্রেশনের মধ্যে মূল পার্থক্য কী — কে সিদ্ধান্ত নেয় কাজ কখন সরবে?

পুশ মাইগ্রেশনে একটি কেন্দ্রীয়, নিয়মিতভাবে চলা প্রক্রিয়া সিদ্ধান্ত নেয় — সে পর্যবেক্ষণ করে কোন কোর ব্যস্ত, কোনটি অলস, এবং সক্রিয়ভাবে কাজ ঠেলে (push) দেয়। পুল মাইগ্রেশনে কোনো কেন্দ্রীয় সিদ্ধান্ত-গ্রহণকারী নেই — যখন একটি কোর অলস হয়ে যায়, সে নিজেই অন্য কোরের কিউ পরীক্ষা করে কাজ টেনে (pull) নেয়। উপরের কোড সেলের rebalance() ফাংশনটি ধারণাগতভাবে পুল মাইগ্রেশনের মতো — অলস কোর সক্রিয়ভাবে কাজ পাচ্ছে।

প্র ০৩ উপরের কোড সেলে দ্বিতীয় রাউন্ডে কেন core1 (যেটি প্রথম রাউন্ডে কাজ পেয়েছিল) থেকেই আবার একটি কাজ সরে যায়?

প্রথম রাউন্ডের পরে অবস্থা ছিল core0: load ৮, core1: load ১০, core2: load ৪ — অর্থাৎ core1 এখন সবচেয়ে ব্যস্ত কোর হয়ে গেছে (কাজ পাওয়ার কারণেই), আর core2 সবচেয়ে অলস। যেহেতু rebalance() প্রতি রাউন্ডে সেই মুহূর্তের সবচেয়ে ব্যস্ত ও সবচেয়ে অলস কোর বের করে, তাই এবার core1 থেকে core2-তে একটি কাজ সরে যায় — এটি দেখায় লোড ব্যালেন্সিং একটি চলমান, পুনরাবৃত্ত প্রক্রিয়া, একবারের সিদ্ধান্ত নয়।

অনুশীলন

  1. চিন্তা করুন: একটি প্রসেস যদি বারবার বিভিন্ন কোরে মাইগ্রেট হতে থাকে (প্রতিবার লোড ব্যালেন্সিংয়ের কারণে), এটি তার নিজের পারফরম্যান্সে কী প্রভাব ফেলতে পারে?

    প্রতিবার নতুন কোরে যাওয়ার মানে সেই প্রসেসের ডেটা নতুন কোরের ক্যাশে একেবারে নতুন করে লোড হতে হবে (ঠান্ডা ক্যাশ থেকে শুরু) — প্রথম কিছু মেমরি অ্যাক্সেসেই ক্যাশ মিস হবে, যা সময়সাপেক্ষ। ঘন ঘন মাইগ্রেশন তাই লোড ব্যালেন্স করলেও প্রতিটি মাইগ্রেশনেই সাময়িক পারফরম্যান্স খরচ যোগ করে — এই জন্যই থ্রেশহোল্ড ব্যবহার করে শুধু বড় অসামঞ্জস্যেই মাইগ্রেশন ঘটানো হয়, প্রতিটি ছোট ওঠানামায় নয়।

  2. পরীক্ষা করুন: উপরের কোড সেলে threshold-কে 3 থেকে 10 করে Run চেপে দেখুন রিব্যালান্সিং কতবার ঘটে।

    থ্রেশহোল্ড 10 করলে প্রথম চেকেই core0(১৫)-core1(৩) = ১২, যা এখনও ১০-এর চেয়ে বেশি, তাই প্রথম মাইগ্রেশনটি তখনও ঘটবে। কিন্তু তারপর core0(৮)-core1(১০) অবস্থায় পার্থক্য মাত্র ২ (বা core2 বিবেচনায় স্বল্প), যা থ্রেশহোল্ডের নিচে — তাই দ্বিতীয় রাউন্ডের মাইগ্রেশনটি আর ঘটবে না। এটি দেখায় থ্রেশহোল্ড বাড়ালে সিস্টেম "যথেষ্ট ব্যালান্সড" বলে দ্রুত সন্তুষ্ট হয়ে যায়, এমনকি কিছুটা অসামঞ্জস্য থাকলেও।

আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ

পূর্ববর্তী পাঠ
মাল্টিলেভেল কিউ ও ফিডব্যাক কিউ শিডিউলিং