Win32 থ্রেড পুল API — CreateThreadpoolWork দিয়ে থ্রেড তৈরি না করে কনকারেন্সি

· · Windows, মাল্টিথ্রেডিং, C++, Windows ডেভেলপমেন্ট, Win32 API, পারফরম্যান্স উন্নতি

“প্রতি ক্লায়েন্টে একটি CreateThread।” “টাইমারের জন্য একটি।” “ইভেন্টের অপেক্ষার জন্য একটি।” — নেটিভ Windows কোডে থ্রেড এভাবেই বেড়ে যায়। প্রতিটি থ্রেড স্ট্যাক ও কার্নেল অবজেক্ট খায়, তৈরি ও ধ্বংসেরও খরচ আছে। কাজ সূক্ষ্ম, অথচ থ্রেড ভারী — সেই অমিল শোষণ করতে OS যা দেয় তাই থ্রেড পুল

.NET-এর ThreadPoolTask.Run-এর সুবিধা সুপরিচিত, কিন্তু আসলে Win32 নেটিভেও সুপরিকল্পিত, OS-মানক থ্রেড-পুল API আছে। Windows Vista-তে সামগ্রিকভাবে নতুন করে সাজানো এই API নেটিভ কনকারেন্সির ভিত্তি: work, টাইমার, ওয়েট ও অ্যাসিঙ্ক্রোনাস I/O একীভূত কলব্যাক যন্ত্রে সামলাতে পারে। C/C++-এ Windows অ্যাপ, সার্ভিস ও DLL লেখা ডেভেলপারদের লক্ষ্য করে এই নিবন্ধ এই API-এর কাঠামো ও ব্যবহার, আর সহজে পড়ে যাওয়া ফাঁদ, প্রাথমিক উৎসের ভিত্তিতে ব্যাখ্যা করে।

১. আগে উপসংহার

  • অনেক সংখ্যক স্বল্পজীবী কাজ ইস্যু করতে, আর শুধু-ওয়েটার থ্রেড বদলাতে, থ্রেড পুল নিজের CreateThread-এর চেয়ে ভালো। থ্রেড ব্যবস্থাপনা OS-এর হাতে রেখে থ্রেড সংখ্যা ও কনটেক্সট সুইচ কমানো যায়।1
  • যা ব্যবহার করবেন তা নতুন API (CreateThreadpoolWork পরিবার)। Vista-র নতুন সাজ পুরোনো API (QueueUserWorkItem পরিবার)-এর চেয়ে সরল, নির্ভরযোগ্য ও উচ্চ-পারফরম্যান্স করেছে, আর এক প্রক্রিয়ায় কয়েকটি স্বাধীন পুলও তৈরি করা যায়।12
  • চার ধরনের অবজেক্ট আছে। কাজ জমা দেওয়া work; সময় বা পর্যায়ে ফায়ার করা timer; কার্নেল অবজেক্ট সিগন্যাল হলে ফায়ার করা wait; আর অ্যাসিঙ্ক্রোনাস I/O সম্পূর্ণ হলে ফায়ার করা io। সবগুলো একই কলব্যাক যন্ত্রে চড়ে।3
  • শাটডাউন হলো “অপেক্ষা, তারপর বন্ধ”। চলমান কলব্যাক রেখে না যাওয়ার শৃঙ্খলা — WaitForThreadpoolWorkCallbacks পরিবার দিয়ে সম্পূর্ণির অপেক্ষা, অথবা ক্লিনআপ গ্রুপ দিয়ে বাল্কে সামলানো — দরকার।4
  • কলব্যাকের ভেতরে: দীর্ঘ সময় ব্লক করবেন না (করলে CallbackMayRunLong); একই পুলে সিঙ্ক্রোনাস সম্পূর্ণি-অপেক্ষা করবেন না; থ্রেডের স্টেট নোংরা করবেন না। এই তিনটি লোহার নিয়ম।56
  • DLL থেকে ব্যবহার: আনলোড রেস সতর্ক হোন। স্পষ্ট শাটডাউন ফাংশনে সম্পূর্ণির অপেক্ষা করুন, আর FreeLibraryWhenCallbackReturns-এর মতো নিবেদিত API জানুন।3

২. কেন পুল, আর কখন পুল

থ্রেড পুলের ধারণা সরল। প্রতি কাজে একটি থ্রেড তৈরি না করে, OS-পরিচালিত ওয়ার্কার থ্রেডের দলে কাজ (কলব্যাক) ছুড়ে দেন। ওয়ার্কাররা একের পর এক কাজ চালায়, আর OS লোড অনুযায়ী সংখ্যা সাজায়।

অফিসিয়াল ডকুমেন্টেশন পুল লাভজনক এমন অ্যাপের কংক্রিট ধরন তালিকা করে।1

  • সমান্তরালে অনেক ছোট ওয়ার্ক আইটেম ইস্যু করা অ্যাপ (সার্চ, নেটওয়ার্ক I/O ইত্যাদি)
  • স্বল্পজীবী থ্রেড ঘন ঘন তৈরি ও ভেঙে ফেলা অ্যাপ
  • ব্যাকগ্রাউন্ডে স্বাধীন কাজ সমান্তরালে প্রক্রিয়া করা অ্যাপ
  • কার্নেল অবজেক্ট বা ইভেন্টের অপেক্ষায় নিবেদিত থ্রেড ধরে রাখা অ্যাপ

শেষ বিষয়টি সহজে মিস হয়। “ইভেন্ট সিগন্যাল হলে চালাব” বলে শুধু ঘুমানোর জন্য পাঁচটি থ্রেড থাকলে সেগুলো পুলের পাঁচটি wait অবজেক্ট দিয়ে বদলানো যায়, আর অপেক্ষা পুলের ওয়েটার থ্রেডে একত্র হয়।

উল্টোদিকে পুল মানায় না এমন কাজও আছে। থ্রেড অগ্রাধিকার বদল দরকার, COM STA দরকার, প্রক্রিয়ার পুরো জীবনকাল চলে — থ্রেডে “ব্যক্তিত্ব” দরকার এমন কাজ নিবেদিত থ্রেডে রাখা হয়। ওয়ার্কার থ্রেড ভাগ করা সম্পদ; ধার করা হয়।

নিবেদিত থ্রেড ও পুলের মধ্যে পছন্দপ্রথমে অগ্রাধিকার বা STA-এর মতো থ্রেড ব্যক্তিত্ব দরকার কি না, আর দীর্ঘ সময় চলে কি না দেখুন; কোনোটাই না মিললে শুধু স্বল্পজীবী, উচ্চ-আয়তন বা ওয়েট-ধরনের কাজ থ্রেড পুলে যায়হ্যাঁনাহ্যাঁনাঅগ্রাধিকার বা STA-এর মতো ব্যক্তিত্ব দরকার?নিবেদিত থ্রেডে রাখুনদীর্ঘ সময় চলে?থ্রেড পুলে দিনস্বল্পজীবী কাজ, ওয়েট, টাইমার, I/O সম্পূর্ণি

চিত্র ১: পুলে দেওয়া যায় শুধু সেই কাজ যা “ব্যক্তিত্ব চায় না এবং সংক্ষেপে শেষ হয়”। বাকি সব আগের মতো নিবেদিত থ্রেডেই থাকে।

ইতিহাসের একটি টুকরোও পেরেক গেড়ে রাখুন। থ্রেড-পুল API-এর দুই প্রজন্ম আছে। Windows 2000 থেকে চলা পুরোনো API (QueueUserWorkItem, RegisterWaitForSingleObject ইত্যাদি), আর Vista-তে সামগ্রিকভাবে নতুন করে সাজানো নতুন API (CreateThreadpoolWork পরিবার)। নতুন API ওয়ার্কার থ্রেডের ধরন একীভূত করে, নিবেদিত স্থায়ী থ্রেড, এক প্রক্রিয়ায় একাধিক পুল, ক্লিনআপ গ্রুপ ইত্যাদি দেয়, আর অফিসিয়াল ডকুমেন্টেশন স্পষ্ট বলে এটি “সরলতর, নির্ভরযোগ্যতর, ভালো পারফরম্যান্স ও নমনীয়তর”।1 পুরোনো API-এর কাঠামোগত সীমাও আছে যেমন “কিউ হয়ে যাওয়া কাজ বাতিল করা যায় না”।2 এখান থেকে এই নিবন্ধ শুধু নতুন API নিয়ে কথা বলে।

পুরোনো থ্রেড-পুল API ও নতুন API-এর মিলপুরোনো API-এর QueueUserWorkItem নতুন API-এর work অবজেক্টে, টাইমার কিউ timer-এ, রেজিস্টার্ড ওয়েট wait-এ, আর BindIoCompletionCallback io-তে ম্যাপ হয়QueueUserWorkItemworkটাইমার কিউtimerরেজিস্টার্ড ওয়েটwaitBindIoCompletionCallbackio

চিত্র ২: পুরোনো API থেকে মাইগ্রেশনের লক্ষ্য এক-এক। বিদ্যমান কোডের তালিকা এই মিল থেকে শুরু করা যায়।

৩. চারটি অবজেক্ট — work, timer, wait ও io

নতুন API-এর কেন্দ্রে চার ধরনের অবজেক্ট যাদের কলব্যাক ফায়ার হওয়ার শর্ত আলাদা।3

অবজেক্ট তৈরির ফাংশন কলব্যাক কখন ফায়ার হয়
work CreateThreadpoolWork SubmitThreadpoolWork দিয়ে জমা দিলে
timer CreateThreadpoolTimer নির্দিষ্ট সময় বা পর্যায় এলে
wait CreateThreadpoolWait কার্নেল অবজেক্ট সিগন্যাল হলে
io CreateThreadpoolIo সংশ্লিষ্ট হ্যান্ডেলের অ্যাসিঙ্ক্রোনাস I/O সম্পূর্ণ হলে
থ্রেড পুলের চারটি অবজেক্ট ও কলব্যাক যন্ত্রwork স্পষ্ট সাবমিটে ফায়ার হয়, timer সময়ে, wait কার্নেল-অবজেক্ট সিগন্যালে, io অ্যাসিঙ্ক্রোনাস I/O সম্পূর্ণিতে; সবগুলো একই ওয়ার্কার থ্রেডের দলে কলব্যাক হিসেবে চলেকোন অবজেক্ট?work(সাবমিটে)টাইমার, ওয়েট, নাকি io?timer(সময় / পর্যায়)ওয়েট নাকি io?wait(সিগন্যালে)io(I/O সম্পূর্ণি)ওয়ার্কাররা কলব্যাক চালায়

চিত্র ৩: ফায়ার হওয়ার শর্ত আলাদা, কিন্তু চারটিই একীভূত যন্ত্রে যেখানে “একই পুলের ওয়ার্কাররা কলব্যাক চালায়”।

এই একীকরণ ব্যবহারিক শক্তি। পর্যায়ক্রমিক প্রক্রিয়া, ইভেন্ট সাড়া ও I/O-সম্পূর্ণি প্রক্রিয়া প্রত্যেকটি নিবেদিত থ্রেডে না লিখে এক কলব্যাক স্টাইলে মেলানো যায়। টাইমারগুলো পুলজুড়ে একটি টাইমার কিউতে জোড়া হয়, ওয়েট অল্প কয়েকটি ওয়েটার থ্রেডে একত্র হয় — “শুধু ঘুমায়” এমন থ্রেড প্রক্রিয়া থেকে মিলিয়ে যায়।1

শুধু-ওয়েটার থ্রেডকে wait অবজেক্টে বদলানোআগে প্রতি ইভেন্টে আলাদা ঘুমানো শুধু-ওয়েটার থ্রেড wait অবজেক্ট হয়ে পুলের ওয়েটার থ্রেডে একত্র হয়, তাই কলব্যাক শুধু সিগন্যাল হলেই চলে৫টি নিবেদিত ওয়েটার থ্রেড আলাদা ঘুমায়৫ স্ট্যাক ও ৫ থ্রেড খায়৫টি wait অবজেক্টপুলের ওয়েটার থ্রেডে একত্রকলব্যাক শুধু সিগন্যালে চলে

চিত্র ৪: “শুধু ঘুমিয়ে অপেক্ষা করে” এমন থ্রেড wait অবজেক্ট করে সরিয়ে ফেলা যায়। পুল মাইগ্রেশনের এটি স্পষ্ট প্রথম পদক্ষেপ।

৪. মৌলিক প্যাটার্ন — work অবজেক্ট দিয়ে এক রাউন্ড ট্রিপ

সবচেয়ে ঘন ব্যবহৃত work অবজেক্ট দিয়ে শিষ্টাচার একবার ঘুরে দেখি।4

VOID CALLBACK WorkCallback(PTP_CALLBACK_INSTANCE instance,
                           PVOID context, PTP_WORK work)
{
    // The context is fixed at creation time. Per-item data is passed through a synchronised queue
    WORK_QUEUE* queue = (WORK_QUEUE*)context;
    ITEM* item = Dequeue(queue);        // Take one item under exclusive control
    ProcessItem(item);
}

// 1) Create (bind the callback to the shared context = the queue)
PTP_WORK work = CreateThreadpoolWork(WorkCallback, &queue, NULL);
if (!work) { /* Failure handling with GetLastError */ }

// 2) Submit once for each item you enqueue (keep the item count and the submit count in step)
Enqueue(&queue, item);
SubmitThreadpoolWork(work);

// 3) Stop the submitter, then wait for completion (TRUE also attempts to cancel work that has not yet started)
WaitForThreadpoolWorkCallbacks(work, FALSE);

// 4) Close
CloseThreadpoolWork(work);

দুটি বিষয় ধরে রাখুন। প্রথম, একই work অবজেক্টে SubmitThreadpoolWork একাধিকবার করা যায়। প্রতিটি সাবমিট কলব্যাক চালায় (সমান্তরালে)।7 তবে কলব্যাকে পাস করা কনটেক্সট তৈরির সময়ই স্থির, তাই এক work অবজেক্টে “একই ধরনের কাজের N আইটেম” লিখলে উপরের কোডের মতো কনটেক্সটে সিঙ্ক্রোনাইজড কিউ রাখুন এবং প্রতি সাবমিটে একটি আইটেম নিন (প্রতি আইটেমে একটি work অবজেক্ট তৈরির ডিজাইনও চলে)। দ্বিতীয়, বন্ধ করার আগে সবসময় সম্পূর্ণির অপেক্ষা করুন। চলমান বা কিউ করা কলব্যাক থেকে যাওয়া অবস্থায় অবজেক্ট বন্ধ করা, অথবা কলব্যাক যে মেমরি দেখে তা মুক্ত করা, হুবহু use-after-free। এই অপেক্ষাকে নিরাপদ বন্ধ করতে আগে সাবমিটার থামানো পূর্বশর্ত — ওয়েটের সমান্তরালে অন্য থ্রেড এখনও Submit করতে পারলে, ওয়েটের পরের সাবমিট Close-এর সাথে রেসে পড়ে। WaitForThreadpoolWorkCallbacks-এর দ্বিতীয় আর্গুমেন্টে TRUE দিলে এখনও শুরু হয়নি এমন সাবমিট বাতিল করার চেষ্টাও হয়।

work অবজেক্টের জীবনচক্রCreateThreadpoolWork দিয়ে তৈরি; SubmitThreadpoolWork দিয়ে সাবমিট করলে কলব্যাক সমান্তরালে চলে। শাটডাউনে আগে নতুন সাবমিট থামান, WaitForThreadpoolWorkCallbacks দিয়ে সব কলব্যাকের সম্পূর্ণির অপেক্ষা করুন, তারপর CloseThreadpoolWork দিয়ে বন্ধ করুনCreateThreadpoolWork দিয়ে তৈরিSubmitThreadpoolWork দিয়ে সাবমিট(বারবার চলে)কলব্যাক সমান্তরালে চলেনতুন সাবমিট থামানWaitForThreadpoolWorkCallbacks-এ অপেক্ষাCloseThreadpoolWork দিয়ে বন্ধ

চিত্র ৫: শাটডাউন ক্রম “সাবমিট থামান → সম্পূর্ণির অপেক্ষা → বন্ধ”। কোনোটা বাদ দিলে use-after-free বা রেস হয়।

ডিফল্টে কলব্যাক প্রক্রিয়া-ডিফল্ট পুল-এ চলে। অনেক ব্যবহারে তাই যথেষ্ট। পুল ভাগ করতে চাইলে পরের অধ্যায়।

৫. কাস্টম পুল ও ক্লিনআপ গ্রুপ

পুল ভাগ করুন। CreateThreadpool দিয়ে স্বাধীন পুল তৈরি করে SetThreadpoolThreadMaximum / SetThreadpoolThreadMinimum দিয়ে থ্রেড সংখ্যার উর্ধ্ব ও নিম্ন সীমা সেট করা যায়।8 সাধারণ ব্যবহার আইসোলেশন। “ধীর হতে পারে এমন ব্যাচ-ধরনের কাজ” যেন “সঙ্গে সঙ্গে সাড়া দিতে হয় এমন কাজের” ওয়ার্কার না খায়, সেজন্য পুল ভাগ করে প্রত্যেককে নিজস্ব থ্রেড বাজেট দিন।

কলব্যাক এনভায়রনমেন্ট দিয়ে বাঁধুন। কাজ কোন পুলে চলবে তা TP_CALLBACK_ENVIRON (কলব্যাক এনভায়রনমেন্ট) ইনিশিয়ালাইজ করে, SetThreadpoolCallbackPool দিয়ে পুলের দিকে নির্দেশ করে, আর সেটা CreateThreadpoolWork ইত্যাদির তৃতীয় আর্গুমেন্ট হিসেবে পাস করে নির্দিষ্ট করা হয়।9

ক্লিনআপ গ্রুপ দিয়ে ভাঁজ করুন। অনেক অবজেক্ট তৈরি করা মডিউলে শাটডাউন প্রক্রিয়া “সবগুলোর অপেক্ষা, সবগুলো বন্ধ” আবৃত্তি হয়ে যায়। CreateThreadpoolCleanupGroup দিয়ে গ্রুপ তৈরি করে কলব্যাক এনভায়রনমেন্ট দিয়ে প্রতিটি অবজেক্ট জোড়ালে, একটি CloseThreadpoolCleanupGroupMembers প্রতিটি সদস্য অবজেক্টের সম্পূর্ণি-অপেক্ষা ও রিলিজ একসঙ্গে করে।34

কলব্যাক এনভায়রনমেন্ট দিয়ে কনফিগারেশন বাঁধাকলব্যাক এনভায়রনমেন্ট কাস্টম পুল ও ক্লিনআপ গ্রুপের দিকে নির্দেশ করে; সেই এনভায়রনমেন্ট দিয়ে তৈরি work ও timer সেই পুলে চলে, আর ক্লিনআপ গ্রুপের বাল্ক অপারেশন সম্পূর্ণি-অপেক্ষা ও রিলিজ জড়ো করেকলব্যাক এনভায়রনমেন্ট(TP_CALLBACK_ENVIRON)কাস্টম পুল(থ্রেড সংখ্যা নিয়ন্ত্রণ)ক্লিনআপ গ্রুপwork / timer / wait / io তৈরির সময় পাসএকবারে সম্পূর্ণির অপেক্ষা ও রিলিজ

চিত্র ৬: কলব্যাক এনভায়রনমেন্ট সেই যন্ত্র যা অবজেক্ট-তৈরির সময় “কোন পুলে চলবে, কে ক্লিনআপ করবে” ঢোকায়।

৬. ফাঁদ — কলব্যাকের ভেতরের শৃঙ্খলা

প্রায় প্রতিটি থ্রেড-পুল বাগ আসে “ধার করা থ্রেডে ইচ্ছেমতো করা” থেকে।

দীর্ঘ সময় ব্লক করা। পুল ধরে নেয় কলব্যাক দ্রুত ফেরে বলে থ্রেড সংখ্যা সাজায়। ডিফল্টে দীর্ঘ কাজ বা দীর্ঘ ওয়েট করলে অন্য কলব্যাকের চালনা দেরি হয়। দীর্ঘ চলতে পারে এমন কলব্যাক CallbackMayRunLong দিয়ে “এটা দীর্ঘ চলবে” ঘোষণা করুক (পুল থ্রেড যোগ করার ইঙ্গিত হিসেবে নেয়), অথবা শুরুতেই নিবেদিত থ্রেডে পাঠানো হোক। লক্ষ করুন, অন্য কলব্যাকের জন্য ওয়ার্কার প্রস্তুত করতে না পারলে CallbackMayRunLong FALSE ফেরায়। রিটার্ন মান না দেখে ব্লক করতে থাকলে পুল তবু জমে, তাই FALSE হলে যে পাশ ব্লক করে না সেদিকে পড়ুন — কাজ ভাগ করুন, নিবেদিত থ্রেডে পাঠান ইত্যাদি।5

একই পুলে সিঙ্ক্রোনাস সম্পূর্ণি-অপেক্ষা। কলব্যাক A-এর ভেতরে একই পুলে জমা দেওয়া কাজ B-এর সম্পূর্ণির জন্য WaitForThreadpoolWorkCallbacks ইত্যাদি দিয়ে অপেক্ষা করার আকৃতি সব ওয়ার্কার “আরেক ওয়ার্কারের অপেক্ষায়” হয়ে গেলেই পুল-স্টারভেশন ডেডলক হয়ে যায়। কাজের মধ্যে নির্ভরতা ওয়েট হিসেবে নয়, “B-এর সম্পূর্ণি কলব্যাক থেকে পরেরটা সাবমিট” কন্টিনিউয়েশন হিসেবে লিখুন।

পুল-স্টারভেশন ডেডলকের কাঠামোপ্রতিটি ওয়ার্কার থ্রেড একই পুলে জমা দেওয়া অন্য কাজের সম্পূর্ণির জন্য সিঙ্ক্রোনাস অপেক্ষা করলে সেই কাজ চালানোর মুক্ত ওয়ার্কার থাকে না, আর সবাই চিরকাল অপেক্ষা করেওয়ার্কার ১: কাজ X-এর অপেক্ষায়কাজ X ও Y চলার অপেক্ষায়ওয়ার্কার ২: কাজ Y-এর অপেক্ষায়চালানোর মুক্ত ওয়ার্কার নেইসবাই চিরকাল অপেক্ষা(স্টারভেশন ডেডলক)

চিত্র ৭: ওয়ার্কারের ভেতর থেকে ওয়ার্কারের জন্য সিঙ্ক্রোনাস অপেক্ষা করলে যে কাজের অপেক্ষা করা হচ্ছে তা চালানোর কেউ থাকে না।

থ্রেডের স্টেট নোংরা করা। ওয়ার্কার থ্রেড পরের কলব্যাকে পুনর্ব্যবহৃত হয়। থ্রেড অগ্রাধিকার বদল, COM ইনিশিয়ালাইজেশন স্টেট, TLS-এ রেখে যাওয়া মান, ছাড়তে ভুলে যাওয়া লক — এগুলোর যেকোনোটি পরের (অসম্পর্কিত) কলব্যাকের দূষণ হয়ে ওঠে। “পুলে ছোঁড়া ফাংশন থ্রেডের ব্যক্তিত্বের উপর নির্ভর করবে না” পুরোনো-API যুগ থেকেই অফিসিয়াল সতর্কতা।6 ক্লিনআপের নিবেদিত যন্ত্র আছে; যেমন LeaveCriticalSectionWhenCallbackReturns পুলকে “এই কলব্যাক ফেরার সময় এই লক ছাড়ো” বলতে পারে।3

DLL আনলোডের সাথে রেস। কলব্যাক চলতে থাকতে যে DLL-এ কোড আছে তা আনলোড হলে অ্যাক্সেস ভায়োলেশন হয়। মৌলিক রূপ: DLL-এর শাটডাউন ফাংশনে সম্পূর্ণির অপেক্ষা নিখুঁতভাবে করা; “এই কলব্যাকই শেষ কাজ, শেষ হলে নিজেকেসহ DLL মুক্ত করতে চাই” পরিস্থিতির জন্য FreeLibraryWhenCallbackReturns দেওয়া আছে। তবে এই API শুধু “চলমান কলব্যাক ফেরার সময় একটি রেফারেন্স ছেড়ে দেয়”; কলব্যাক শুরুর আগে আনলোড ঠেকায় না। জোড়ায় ব্যবহার করুন: সাবমিটের আগে GetModuleHandleEx দিয়ে নিজের মডিউল রেফারেন্স নিন, আর কলব্যাক এই API দিয়ে সেই রেফারেন্স ছাড়ুক।3 আর এই সম্পূর্ণি-অপেক্ষা DllMain-এর ভেতরে করবেন না — “DllMain ও লোডার লক“-এ বলা হয়েছে, DllMain-এর ভেতরে অন্য থ্রেডের অপেক্ষা ডেডলক প্যাটার্ন।

DLL আনলোড ও কলব্যাকের রেসের প্রস্তুতিকলব্যাক চলতে থাকতে DLL আনলোড অ্যাক্সেস ভায়োলেশন হয়ে যায়, তাই মৌলিক রূপ স্পষ্ট শাটডাউন ফাংশনে সম্পূর্ণির অপেক্ষা করে তারপর বন্ধ করা; শেষ কলব্যাক নিজেই DLL মুক্ত করলে FreeLibraryWhenCallbackReturns ব্যবহার করুনকলব্যাক চলতে আনলোডঅ্যাক্সেস ভায়োলেশনঅপেক্ষা, তারপর বন্ধনিরাপদ আনলোডশাটডাউন ফাংশনেFreeLibraryWhenCallbackReturnsশেষ কলব্যাক DLL মুক্ত করেDllMain-এর ভেতরে নয়

চিত্র ৮: মৌলিক রূপ স্পষ্ট শাটডাউন ফাংশনে “অপেক্ষা, তারপর বন্ধ” করা। DllMain-এর ভেতরে অপেক্ষা আরেক ডেডলক ডাকে।

জমা দেওয়া কাজের ভেতরে এক্সসেপশন ও ক্র্যাশ। ওয়ার্কার থ্রেডে আনহ্যান্ডেলড এক্সসেপশন প্রক্রিয়াকে সাথে নিয়ে যায়। নিবেদিত থ্রেডের থ্রেড ফাংশনের মতোই কলব্যাকের প্রবেশে এক্সসেপশন ব্যাপকভাবে ধরে লগ করার নীতি প্রয়োগ করুন।

৭. স্ট্যান্ডার্ড লাইব্রেরি ও .NET-এর সাথে সম্পর্ক — কোন স্তরে লিখবেন

শেষে অন্য হাতিয়ারের সাথে এটি কোথায় বসে তা সাজাই।

  • C++ std::async / std::thread যথেষ্ট হলে সেগুলোই প্রথম প্রার্থী। পোর্টেবল, কোড ছোট, আর future-এর অর্থও স্ট্যান্ডার্ডে স্থির।10
  • Win32 থ্রেড পুল সরাসরি ব্যবহারের কারণ হলো যখন আপনি (১) timer, wait ও ioসহ একীভূত কলব্যাক যন্ত্র চান, (২) পুল ভাগ বা থ্রেড-সংখ্যা নিয়ন্ত্রণ চান, অথবা (৩) DLL বা COM কম্পোনেন্টের ভেতরে নিজের থ্রেড ধরে রাখতে চান না।
  • .NET পাশে ThreadPoolTask একই ভূমিকা পালন করে, আর I/O সম্পূর্ণি IOCP-এর সাথে বাঁধা। এই তলার কাঠামো “IOCP ও .NET থ্রেড পুল“-এ ব্যাখ্যা করা আছে।
কোন স্তরের হাতিয়ার দিয়ে লিখবেন তা ঠিক করাস্ট্যান্ডার্ড C++ async বা thread যথেষ্ট হলে সেগুলো ব্যবহার করুন; টাইমার, ওয়েট ও I/O সম্পূর্ণির একীকরণ, পুল ভাগ বা থ্রেড-সংখ্যা নিয়ন্ত্রণ, অথবা DLL বা COM কম্পোনেন্টের ভেতরে নিজের থ্রেড না চাইলে Win32 থ্রেড পুল সরাসরি ব্যবহার করুনহ্যাঁনাtimer, wait ও io-এর একীকরণপুল ভাগ / সংখ্যা নিয়ন্ত্রণDLL-এ নিজের থ্রেড এড়ানোস্ট্যান্ডার্ড C++ হাতিয়ার যথেষ্ট?std::async / std::threadকী দরকার?Win32 থ্রেড পুল

চিত্র ৯: সন্দেহ হলে স্ট্যান্ডার্ড লাইব্রেরি দিয়ে শুরু করুন; এই API-এর পালা তখনই আসে যখন সে যা প্রকাশ করতে পারে না এমন চাহিদা দেখা দেয়।

অর্থাৎ এই API সেই বিন্দুতে কনকারেন্সির ভিত্তি যেখানে আপনি “নেটিভ লিখব” ঠিক করেছেন। বাস্তবসম্মত পরিষ্কারকরণ ধাপে ধাপে: নিজের CreateThread-এর বিস্তার থেকে মাইগ্রেশনের লক্ষ্য হিসেবে আগে work অবজেক্ট আনুন, তারপর শুধু-ওয়েটার থ্রেডকে wait আর টাইমার থ্রেডকে timer দিয়ে বদলান।

৮. সারসংক্ষেপ

  • অনেক সংখ্যক স্বল্পজীবী কাজ ইস্যু করতে, আর শুধু-ওয়েটার ও শুধু-টাইমার থ্রেড গোছাতে, নিজের থ্রেডের বদলে OS-মানক থ্রেড পুল। যা ব্যবহার করবেন তা Vista থেকে নতুন API।
  • কেন্দ্রে চারটি অবজেক্ট work, timer, wait ও io। ফায়ার হওয়ার শর্ত আলাদা; একই ওয়ার্কার দল ও একই কলব্যাক স্টাইলে একীভূত।
  • শিষ্টাচার “তৈরি → সাবমিট → সম্পূর্ণির অপেক্ষা → বন্ধ”। একাধিক সাবমিট সমান্তরালে চলে। ক্লিনআপ গ্রুপ শাটডাউন প্রক্রিয়া বাল্ক করতে পারে।
  • কলব্যাকের তিনটি লোহার নিয়ম: দীর্ঘ সময় ব্লক করবেন না (করলে CallbackMayRunLong); একই পুলে সিঙ্ক্রোনাস অপেক্ষা করবেন না; থ্রেডের স্টেট নোংরা করবেন না।
  • DLL থেকে ব্যবহার: আনলোড রেস সতর্ক হোন। স্পষ্ট শাটডাউন ফাংশনে সম্পূর্ণির অপেক্ষা করুন; DllMain-এ করবেন না।
  • স্ট্যান্ডার্ড C++ বা .NET যথেষ্ট হলে সেগুলোই ব্যবহার করুন। এই API-এর পালা timer/wait/io একীকরণ বা পুল নিয়ন্ত্রণ দরকার হলে।

থ্রেড-পুল API Win32 API-এর মধ্যে নতুনতর ও ভালো-ডিজাইন করা দিকের। “থ্রেড তৈরি” ধারণা থেকে “কলব্যাক ছোঁড়া” ধারণায় স্থানান্তর শেষ হলে নেটিভ কোডে কনকারেন্সি অনেক পরিষ্কারভাবে লেখা যায়।

সম্পর্কিত নিবন্ধ

সম্পর্কিত পরামর্শ ক্ষেত্র

KomuraSoft LLC থ্রেড বেড়ে যাওয়া নেটিভ কোড থেকে থ্রেড পুলে মাইগ্রেশন ডিজাইন, C++ অ্যাপ ও DLL-এর কনকারেন্ট প্রক্রিয়ার ডিজাইন রিভিউ, আর পুল স্টারভেশন বা কলব্যাকের কারণে হ্যাং ও ক্র্যাশের মূল কারণ অনুসন্ধান সামলায়। বিদ্যমান কোডের তালিকা থেকে শুরু করেও পরামর্শ স্বাগত।

তথ্যসূত্র

  1. Microsoft Learn, Thread Pools। থ্রেড পুল অ্যাপের পক্ষে অ্যাসিঙ্ক্রোনাস কলব্যাক দক্ষভাবে চালানো ওয়ার্কার থ্রেডের সংগ্রহ হওয়া; কোন ধরনের অ্যাপে মানায় (সমান্তরালে অনেক ছোট ওয়ার্ক আইটেম ইস্যু, স্বল্পজীবী থ্রেড ঘন ঘন তৈরি ও ভাঙা, স্বাধীন কাজ সমান্তরালে প্রক্রিয়া, কার্নেল অবজেক্টে একচেটিয়া ওয়েট ইত্যাদি); এবং Vista-র সামগ্রিক নতুন সাজ (ওয়ার্কার-থ্রেড ধরনের একীকরণ, একটি টাইমার কিউ, নিবেদিত স্থায়ী থ্রেড, ক্লিনআপ গ্রুপ, এক প্রক্রিয়ায় একাধিক পুল, ও নতুন API) সম্পর্কে।  2 3 4 5

  2. Microsoft Learn, Thread Pooling। পুরোনো থ্রেড-পুল API-এর কাঠামো (QueueUserWorkItem, টাইমার কিউ, রেজিস্টার্ড ওয়েট, BindIoCompletionCallback); কিউ হয়ে যাওয়া কাজ বাতিল করার উপায় না থাকা; এবং Vista-তে আনা নতুন থ্রেড-পুল API সরলতর ও নির্ভরযোগ্যতা, পারফরম্যান্স ও নমনীয়তায় উন্নত বলা সম্পর্কে।  2

  3. Microsoft Learn, threadpoolapiset.h header। চারটি অবজেক্ট-তৈরির ফাংশন CreateThreadpoolWork, CreateThreadpoolTimer, CreateThreadpoolWait ও CreateThreadpoolIoসহ ফাংশন তালিকা; ক্লিনআপ গ্রুপ (CreateThreadpoolCleanupGroup); এবং কলব্যাক সম্পূর্ণির সাথে বাঁধা ক্লিনআপ (LeaveCriticalSectionWhenCallbackReturns, FreeLibraryWhenCallbackReturns ইত্যাদি) সম্পর্কে।  2 3 4 5 6

  4. Microsoft Learn, Using the Thread Pool Functions। CreateThreadpoolWork দিয়ে তৈরি, SubmitThreadpoolWork দিয়ে সাবমিট, WaitForThreadpoolWorkCallbacks দিয়ে সম্পূর্ণির অপেক্ষা, CloseThreadpoolWork দিয়ে বন্ধ করার মৌলিক পদ্ধতি; এবং কাস্টম পুলকে কলব্যাক এনভায়রনমেন্ট ও ক্লিনআপ গ্রুপের সাথে মেলানো কনফিগারেশন উদাহরণ সম্পর্কে।  2 3

  5. Microsoft Learn, CallbackMayRunLong function (threadpoolapiset.h)। বর্তমান কলব্যাক দীর্ঘ সময় চলতে পারে বলে পুলকে জানানো, যাতে পুল অন্য কলব্যাকের জন্য থ্রেড সুরক্ষিত করবে কি না তার উপাদান হিসেবে ব্যবহার করতে পারে; এবং সম্ভব হলে দীর্ঘ-চলমান কলব্যাকের জন্য নিবেদিত থ্রেড ভাবা সম্পর্কে।  2

  6. Microsoft Learn, Thread Pooling। থ্রেড পুলে জমা দেওয়া ওয়ার্ক আইটেম ও তারা যে ফাংশন ডাকে সেগুলো থ্রেড-পুল নিরাপদ হতে হয়; চালনাকারী থ্রেড নিবেদিত স্থায়ী থ্রেড ধরে নেওয়া যায় না; এবং TLS ও স্থায়ী থ্রেড দরকার এমন অ্যাসিঙ্ক্রোনাস কল এড়ানো সম্পর্কে।  2

  7. Microsoft Learn, SubmitThreadpoolWork function (threadpoolapiset.h)। আগের কলব্যাক সম্পূর্ণির অপেক্ষা না করে একই work অবজেক্ট একাধিকবার সাবমিট করা যায় বলে কলব্যাক সমান্তরালে চলা; এবং পুল দক্ষতার জন্য থ্রেড সংখ্যা সাজাতে (থ্রটল করতে) পারা সম্পর্কে। 

  8. Microsoft Learn, SetThreadpoolThreadMaximum function (threadpoolapiset.h)। CreateThreadpool দিয়ে তৈরি পুলের ওয়ার্কার-থ্রেড সংখ্যার উর্ধ্বসীমা সেট করা যায় (নিম্নসীমা SetThreadpoolThreadMinimum) সম্পর্কে। 

  9. Microsoft Learn, CreateThreadpoolWork function (threadpoolapiset.h)। কলব্যাক ফাংশন ও কনটেক্সট পয়েন্টার থেকে work অবজেক্ট তৈরি; এবং তৃতীয় আর্গুমেন্ট TP_CALLBACK_ENVIRON কলব্যাকের চালনার পরিবেশ (কোন পুল ইত্যাদি) নির্দিষ্ট করতে পারা, NULL মানে ডিফল্ট পরিবেশে চলা সম্পর্কে। 

  10. Microsoft Learn, <future>। std::async ও future দিয়ে প্রতি কাজে অ্যাসিঙ্ক্রোনাস চালনা স্ট্যান্ডার্ড লাইব্রেরি হিসেবে দেওয়া, তাই থ্রেড সরাসরি ব্যবস্থাপনা না করেই কনকারেন্সি লেখা যায় সম্পর্কে। 

কাছাকাছি বিষয়ে গভীরে যেতে একই ট্যাগযুক্ত সাম্প্রতিক নিবন্ধ।

নেমড পাইপ ব্যবহারিকভাবে — ডিজাইন থেকে নিরাপত্তা পর্যন্ত Windows-এর মানক IPC

Windows-এর মানক আন্তঃপ্রক্রিয়া যোগাযোগ নেমড পাইপের ব্যবহারিক গাইড। প্রাথমিক উৎস থেকে এই নিবন্ধ বাইট ও মেসেজ মোডের পছন্দ, একাধিক ক্লায়েন...

Windows ভার্চুয়ালাইজেশনের গভীরতা (পর্ব ৩) — সেকেন্ডে বুট হওয়া ভার্চুয়াল মেশিন: WSL2, Windows Sandbox ও কন্টেইনার এত হালকা কেন

WSL2 ও Windows Sandbox সেকেন্ডে শুরু হয়ে এত হালকা মনে হয় কেন? এই নিবন্ধ ডায়নামিক বেস ইমেজ ও ডাইরেক্ট ম্যাপ থেকে ডায়নামিক মেমরি বরাদ্দ...

Windows ভার্চুয়ালাইজেশনের গভীরতা (পর্ব ২) — কার্নেলও দেখতে পায় না এমন মেমরি: VBS, HVCI ও Credential Guard কীভাবে কাজ করে

সামঞ্জস্যপূর্ণ হার্ডওয়্যারে ক্লিন ইনস্টলে VBS ডিফল্টে চালু থাকে এবং হাইপারভাইজার ও SLAT দিয়ে কার্নেলের চেয়ে শক্তিশালী আইসোলেশন তৈরি কর...

Windows ভার্চুয়ালাইজেশনের গভীরতা (পর্ব ১) — আপনার Windows আসলে কোথায় চলছে? হাইপারভাইজার ও পার্টিশন

Hyper-V চালু করলে হোস্ট Windows নিজেই রুট পার্টিশন হিসেবে হাইপারভাইজারের উপর চলে। এই নিবন্ধ VT-x, SLAT ও VMBus-এর ভূমিকা দিয়ে ভার্চুয়াল...

এলাকার নাম দিয়ে অনুসন্ধানে দৃশ্যমান হোন — ছোট ও মাঝারি ব্যবসার জন্য লোকাল SEO-এর ব্যবহারিক গাইড (এলাকা পৃষ্ঠা ও Google Business Profile)

সেসব ছোট ও মাঝারি ব্যবসার জন্য যাদের সাইট "এলাকার নাম + খাত" খুঁজলে দেখা যায় না। এই নিবন্ধ লোকাল SEO ঠিক করার ক্রম সাজায়: Google Busine...

এই পৃষ্ঠাগুলো বিষয়টিকে সেবা ও সিদ্ধান্তের বৃহত্তর প্রেক্ষাপটে স্থাপন করে।

নিবন্ধটি নিচের সেবাগুলোর সাথে সরাসরি সম্পর্কিত।

প্রায়শ জিজ্ঞাসিত প্রশ্ন

এই নিবন্ধের বিষয়ে পরামর্শে প্রায়ই আসা প্রশ্ন।

CreateThread দিয়ে নিজের থ্রেড তৈরি করার তুলনায় থ্রেড পুল কী ভালো?
অনেক সংখ্যক স্বল্পজীবী কাজ থাকলে দক্ষতা, আর থ্রেড-ব্যবস্থাপনার কোড কমে যাওয়া। থ্রেড তৈরি ও ধ্বংসের খরচ উপেক্ষা করা যায় না, তাই "প্রতি কাজে CreateThread করে শেষে ধ্বংস" বারবার করা অ্যাপ, অথবা শুধু ইভেন্টের অপেক্ষায় ঘুমানো অনেক থ্রেড ধরে রাখা অ্যাপ, পুলে গেলে থ্রেড সংখ্যা ও কনটেক্সট সুইচ কমাতে পারে। অফিসিয়াল ডকুমেন্টেশন পুল-প্রার্থী হিসেবে তালিকা করে: সমান্তরালে অনেক ছোট ওয়ার্ক আইটেম ইস্যু করা অ্যাপ, অনেক স্বল্পজীবী থ্রেড তৈরি করা অ্যাপ, আর শুধু কার্নেল অবজেক্টের অপেক্ষায় নিবেদিত থ্রেড রাখা অ্যাপ। উল্টোদিকে "থ্রেডে নিজস্ব ব্যক্তিত্ব দরকার" এমন কাজ — অগ্রাধিকার বদল, COM STA, দীর্ঘস্থায়ী নিবেদিত প্রক্রিয়া — আগের মতোই নিবেদিত থ্রেডে রাখুন।
QueueUserWorkItem-এর মতো পুরোনো থ্রেড-পুল ফাংশন থেকে এটি কীভাবে আলাদা?
Windows Vista-তে থ্রেড পুল সামগ্রিকভাবে নতুন করে সাজানো হয়েছিল। বর্তমান threadpoolapiset-পরিবারের API (CreateThreadpoolWork ইত্যাদি) নতুন API; QueueUserWorkItem, RegisterWaitForSingleObject ইত্যাদি পুরোনো (লেগাসি) API। নতুন API ওয়ার্কার থ্রেডের ধরন একীভূত করে, এক প্রক্রিয়ায় কয়েকটি স্বাধীন পুল তৈরি করতে দেয়, আর ক্লিনআপ গ্রুপ দিয়ে বাল্ক রিলিজ এবং কলব্যাক সম্পূর্ণির সাথে লক রিলিজ বা DLL আনলোডের মতো যন্ত্র দেয়। অফিসিয়াল ডকুমেন্টেশনও বলে নতুন API সরলতর এবং নির্ভরযোগ্যতা, পারফরম্যান্স ও নমনীয়তায় উন্নত। পুরোনো API-এর কাঠামোগত সীমাও আছে, যেমন "কিউ হয়ে যাওয়া কাজ বাতিল করার উপায় নেই", তাই নতুন কোডে নতুন API ব্যবহার করুন।
কলব্যাকের ভেতরে এমন কিছু আছে যা করা যাবে না?
বড় তিনটি। প্রথম, ডিফল্টে দীর্ঘ সময় ব্লক করা বা দীর্ঘ কাজ করা। পুল ধরে নেয় কলব্যাক দ্রুত শেষ হয় বলে থ্রেড সংখ্যা সাজায়, তাই দীর্ঘ সময় লাগবে এমন কাজে CallbackMayRunLong দিয়ে ঘোষণা করুন অথবা নিবেদিত থ্রেড ব্যবহার করুন। দ্বিতীয়, একই পুলে জমা দেওয়া অন্য কাজের সম্পূর্ণির জন্য সিঙ্ক্রোনাস অপেক্ষা। সব ওয়ার্কার "আরেক ওয়ার্কারের অপেক্ষায়" হয়ে গেলে পুল-স্টারভেশন ডেডলক হয়। তৃতীয়, থ্রেডের ব্যক্তিত্বের উপর নির্ভর করা। ওয়ার্কার থ্রেড কলব্যাকজুড়ে ভাগ করা, তাই বদলানো থ্রেড অগ্রাধিকার বা COM ইনিশিয়ালাইজেশন স্টেট নিয়ে ফেরা, অথবা TLS-এ স্টেট রেখে যাওয়া, পরের কলব্যাক দূষিত করে। শেষে ক্লিনআপের জন্য (লক ছাড়া বা DLL আনলোড) LeaveCriticalSectionWhenCallbackReturns ও FreeLibraryWhenCallbackReturns-এর মতো নিবেদিত যন্ত্র দেওয়া আছে।
DLL থেকে থ্রেড পুল ব্যবহারের সতর্কতা কী?
সবচেয়ে বড় বিপদ "কলব্যাক এখনও চলতে থাকতে DLL আনলোড হয়ে যাওয়া"। আনলোডের পর কলব্যাক চললে অ্যাক্সেস ভায়োলেশন হয়। DLL পাশে শাটডাউন প্রক্রিয়ায় নিজের ইস্যু করা কলব্যাকের সম্পূর্ণির জন্য নির্ভরযোগ্যভাবে অপেক্ষা করতে হয় — WaitForThreadpoolWorkCallbacks-এর মতো ওয়েট ফাংশন, অথবা ক্লিনআপ গ্রুপে CloseThreadpoolCleanupGroupMembers — তারপরই অবজেক্ট বন্ধ করুন। তবে DllMain-এর ভেতরে এই অপেক্ষা লোডার লকের সাথে মিলে ডেডলক করতে পারে, তাই নিয়ম: DllMain-এ নয়, স্পষ্ট শাটডাউন ফাংশনে করুন। যে পরিস্থিতিতে কলব্যাক নিজেই DLL মুক্ত করতে চায় কারণ "এই কাজই শেষ", সেখানে নিবেদিত API FreeLibraryWhenCallbackReturns দেওয়া আছে।
C++ std::async ও .NET-এর ThreadPool থাকতে এই API সরাসরি ব্যবহারের উপলক্ষ এখনও আছে?
আছে। মাপকাঠি "সেই স্তরের হাতিয়ার কি যথেষ্ট"। C++-এ যে কনকারেন্সির দানাদারতা দরকার তা std::async বা std::thread দিয়ে ঢাকা থাকলে পোর্টেবিলিটির দিক থেকেও স্ট্যান্ডার্ড লাইব্রেরি প্রথম প্রার্থী। অন্যদিকে টাইমার, কার্নেল-অবজেক্ট ওয়েট ও অ্যাসিঙ্ক্রোনাস I/O সম্পূর্ণি এক কলব্যাক যন্ত্রে একীভূত করতে চাওয়া; কাজের ধরন অনুযায়ী পুল ভাগ করে থ্রেড সংখ্যা নিয়ন্ত্রণ করতে চাওয়া; DLL বা COM কম্পোনেন্টের ভেতরে নিজের থ্রেড ধরে রাখতে না চাওয়া — সেই চাহিদাই Win32 থ্রেড পুল ঢাকে। .NET-এর ThreadPool ও IOCP-এর সম্পর্ক সম্পর্কিত নিবন্ধে আছে, আর নেটিভ লিখতে থাকলে নিচের স্তরে বসা এই যন্ত্র জানা বৃথা যায় না।

লেখকের প্রোফাইল

নিবন্ধের লেখকের পরিচিতি পৃষ্ঠা।

Go Komura

KomuraSoft LLC-এর প্রতিনিধি

Windows সফটওয়্যার ডেভেলপমেন্ট, প্রযুক্তিগত পরামর্শ ও বাগ তদন্তে বিশেষজ্ঞ, বিশেষ করে বিদ্যমান সিস্টেমযুক্ত প্রকল্প ও পুনরুৎপাদন করা কঠিন বাগে।

পাবলিক লিঙ্ক

ব্লগে ফিরে যান