Win32 Thread Pool API — konkurensi tanpa membuat thread, lewat CreateThreadpoolWork

· · Windows, Multithreading, C++, Pengembangan Windows, Win32 API, Peningkatan performa

“Satu CreateThread per klien.” “Satu untuk timer.” “Satu untuk menunggu event.” — Di kode Windows native, thread cenderung berkembang biak seperti ini. Setiap thread mengonsumsi stack dan objek kernel, dan membuat serta menghancurkannya juga punya biaya. Pekerjaannya berbutir halus, tetapi thread-nya berat — thread pool adalah yang OS sediakan untuk menyerap ketidakcocokan itu.

Kenyamanan ThreadPool dan Task.Run di .NET sudah dikenal, tetapi pada kenyataannya Win32 native juga punya thread-pool API standar OS yang dirancang dengan baik. Didesain ulang secara menyeluruh di Windows Vista, API ini adalah fondasi konkurensi native: ia dapat menangani pekerjaan, timer, tunggu, dan I/O asinkron lewat mekanisme callback yang terpadu. Ditujukan kepada pengembang yang menulis aplikasi, layanan, dan DLL Windows dalam C/C++, artikel ini menjelaskan struktur dan pemakaian API ini, serta jebakan yang mudah dilangkahi, berdasarkan sumber primer.

1. Kesimpulan lebih dulu

  • Untuk mengeluarkan banyak pekerjaan berumur pendek, dan untuk mengganti thread yang hanya menunggu, thread pool mengalahkan CreateThread sendiri. Anda menyerahkan manajemen thread kepada OS dan dapat mengurangi jumlah thread serta context switch.1
  • Yang harus Anda pakai adalah API baru (keluarga CreateThreadpoolWork). Desain ulang Vista membuatnya lebih sederhana, lebih andal, dan berperforma lebih tinggi daripada API lama (keluarga QueueUserWorkItem), dan Anda juga dapat membuat beberapa pool independen dalam satu proses.12
  • Ada empat jenis objek. work, tempat Anda mengirim pekerjaan; timer, yang menyala pada waktu atau periode; wait, yang menyala ketika objek kernel diberi sinyal; dan io, yang menyala ketika I/O asinkron selesai. Semuanya menunggangi mekanisme callback yang sama.3
  • Shutdown adalah “tunggu, lalu tutup”. Disiplin untuk tidak meninggalkan callback yang masih berjalan — menunggu penyelesaian dengan keluarga WaitForThreadpoolWorkCallbacks, atau menanganinya secara massal lewat cleanup group — diperlukan.4
  • Di dalam callback: jangan memblokir lama (jika akan, CallbackMayRunLong); jangan menunggu penyelesaian secara sinkron pada pool yang sama; jangan kotorin state thread. Tiga ini adalah aturan besi.56
  • Pemakaian dari DLL: waspadai balapan unload. Tunggu penyelesaian di fungsi shutdown eksplisit, dan kenali API khusus seperti FreeLibraryWhenCallbackReturns.3

2. Mengapa pool, dan kapan pool

Gagasan thread pool sederhana. Alih-alih membuat thread per pekerjaan, Anda melempar pekerjaan (callback) ke sekelompok worker thread yang dikelola OS. Worker mengeksekusi pekerjaan satu per satu, dan OS menyesuaikan jumlahnya dengan beban.

Dokumentasi resmi mencantumkan jenis aplikasi konkret di mana pool menguntungkan.1

  • Aplikasi yang mengeluarkan banyak item kerja kecil secara paralel (pencarian, I/O jaringan, dan sebagainya)
  • Aplikasi yang sering membuat dan merobohkan thread berumur pendek
  • Aplikasi yang memproses pekerjaan independen secara paralel di latar belakang
  • Aplikasi yang menahan thread yang dikhususkan untuk menunggu objek kernel atau event

Butir terakhir mudah terlewat. Jika Anda punya lima thread yang hanya ada untuk tidur agar dapat “berjalan ketika event diberi sinyal”, itu dapat diganti dengan lima objek wait pada pool, dan penungguan diagregasi ke waiter thread milik pool.

Sebaliknya, ada juga pekerjaan yang tidak cocok untuk pool. Pekerjaan yang membutuhkan perubahan prioritas thread, yang mensyaratkan COM STA, yang terus berjalan sepanjang umur proses — pekerjaan yang membutuhkan “kepribadian” pada thread dipegang pada thread khusus. Worker thread adalah sumber daya bersama; ia dipinjam.

Memilih antara thread khusus dan poolPeriksa dulu apakah kepribadian thread seperti prioritas atau STA dibutuhkan, dan apakah ia berjalan lama; hanya pekerjaan berumur pendek, bervolume tinggi, atau bergaya tunggu yang tidak cocok keduanya yang masuk thread poolYaTidakYaTidakPerlu kepribadian seperti prioritas atau STA?Tetap di thread khususBerjalan lama?Masukkan ke thread poolKerja pendek, tunggu, timer, I/O

Gambar 1: Satu-satunya pekerjaan yang boleh Anda masukkan ke pool adalah pekerjaan yang “tidak butuh kepribadian dan selesai singkat”. Selebihnya tetap di thread khusus seperti semula.

Ada juga satu potongan sejarah yang perlu dipatok. API thread pool punya dua generasi. API lama yang berlanjut sejak Windows 2000 (QueueUserWorkItem, RegisterWaitForSingleObject, dan sebagainya), dan API baru yang didesain ulang secara menyeluruh di Vista (keluarga CreateThreadpoolWork). API baru menyatukan jenis worker thread, menyediakan persistent thread khusus, beberapa pool dalam satu proses, cleanup group, dan lainnya, dan dokumentasi resmi menyatakan dengan kata-kata bahwa ia “lebih sederhana, lebih andal, berperforma lebih baik, dan lebih fleksibel”.1 API lama juga punya kendala struktural seperti “Anda tidak dapat membatalkan pekerjaan setelah diantrekan”.2 Mulai dari sini, artikel ini hanya membahas API baru.

Korespondensi API thread pool lama dan API baruQueueUserWorkItem API lama dipetakan ke objek work API baru, antrian timer ke timer, registered wait ke wait, dan BindIoCompletionCallback ke ioQueueUserWorkItemworkAntrian timertimerRegistered waitwaitBindIoCompletionCallbackio

Gambar 2: Target migrasi dari API lama adalah satu-ke-satu. Inventaris kode yang ada dapat dimulai dari korespondensi ini.

3. Empat objek — work, timer, wait, dan io

Di pusat API baru ada empat jenis objek yang syarat penyalaan callback-nya berbeda.3

Objek Fungsi pembuatan Kapan callback menyala
work CreateThreadpoolWork Ketika dikirim dengan SubmitThreadpoolWork
timer CreateThreadpoolTimer Ketika waktu atau periode yang ditentukan tiba
wait CreateThreadpoolWait Ketika objek kernel menjadi signaled
io CreateThreadpoolIo Ketika I/O asinkron pada handle terkait selesai
Empat objek thread pool dan mekanisme callbackwork menyala pada pengiriman eksplisit, timer pada waktu, wait pada sinyal objek kernel, dan io pada penyelesaian I/O asinkron; semuanya berjalan sebagai callback pada kelompok worker thread yang samaObjek yang mana?work(saat kirim)Timer, wait, atau io?timer(waktu / periode)Wait atau io?wait(saat sinyal)io(penyelesaian I/O)Worker jalankan callback

Gambar 3: Syarat penyalaannya berbeda, tetapi keempatnya disatukan dalam mekanisme di mana “worker pada pool yang sama menjalankan callback”.

Penyatuan ini adalah kekuatan praktis. Alih-alih menulis pemrosesan berkala, respons event, dan pemrosesan penyelesaian I/O masing-masing pada thread khusus, Anda dapat menyelaraskannya pada satu gaya callback. Timer dikumpulkan ke satu antrian timer untuk seluruh pool, dan tunggu diagregasi ke sejumlah kecil waiter thread — thread yang “hanya tidur” menghilang dari proses.1

Mengganti thread yang hanya menunggu dengan objek waitThread yang hanya menunggu yang dulu tidur satu per event menjadi objek wait dan diagregasi ke waiter thread milik pool, sehingga callback hanya berjalan saat diberi sinyal5 thread waiter khusus yang tidur sendiri-sendiriMengonsumsi 5 stack dan 5 thread5 objek waitDiagregasi ke waiter thread poolCallback hanya saat diberi sinyal

Gambar 4: Thread yang “hanya tidur dan menunggu” dapat dihilangkan dengan mengubahnya menjadi objek wait. Ini langkah pertama yang jelas untuk migrasi ke pool.

4. Pola dasar — satu bolak-balik dengan objek work

Kita menelusuri tata krama sekali dengan objek work, yang paling sering dipakai.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);

Ada dua poin yang harus dipegang. Pertama, Anda boleh SubmitThreadpoolWork objek work yang sama lebih dari sekali. Setiap pengiriman menjalankan callback (secara paralel).7 Namun, context yang diteruskan ke callback tetap pada saat pembuatan, jadi ketika Anda menulis “N item pekerjaan jenis yang sama” dengan satu objek work, Anda menaruh antrian tersinkronisasi di context seperti pada kode di atas dan mengambil satu item per pengiriman (desain yang membuat objek work per item juga baik-baik saja). Kedua, selalu tunggu penyelesaian sebelum Anda menutup. Menutup objek sementara callback yang berjalan atau terantre masih ada, atau membebaskan memori yang dirujuk callback, adalah use-after-free begitu saja. Agar tunggu ini menjadi penutupan yang aman, menghentikan pengirim lebih dulu adalah prasyarat — dalam struktur di mana thread lain masih dapat Submit secara paralel dengan tunggu, pengiriman setelah tunggu berbalapan dengan Close. Meneruskan TRUE sebagai argumen kedua WaitForThreadpoolWorkCallbacks juga mencoba membatalkan pengiriman yang belum mulai.

Siklus hidup objek workBuat dengan CreateThreadpoolWork; kirim dengan SubmitThreadpoolWork dan callback berjalan paralel. Saat shutdown, hentikan dulu pengiriman baru, tunggu setiap callback selesai dengan WaitForThreadpoolWorkCallbacks, lalu tutup dengan CloseThreadpoolWorkBuat dengan CreateThreadpoolWorkKirim dengan SubmitThreadpoolWork (boleh diulang)Callback berjalan paralelHentikan pengiriman baruTunggu selesai dengan WaitForThreadpoolWorkCallbacksTutup dengan CloseThreadpoolWork

Gambar 5: Urutan shutdown adalah “hentikan pengiriman → tunggu penyelesaian → tutup”. Lewati salah satunya dan Anda mendapat use-after-free atau balapan.

Secara bawaan, callback berjalan pada pool default proses. Untuk banyak penggunaan itu sudah cukup. Bab berikutnya untuk ketika Anda ingin membagi pool.

5. Pool kustom dan cleanup group

Bagi pool. Anda dapat membuat pool independen dengan CreateThreadpool dan mengatur batas atas serta bawah jumlah thread dengan SetThreadpoolThreadMaximum / SetThreadpoolThreadMinimum.8 Penggunaan khasnya adalah isolasi. Agar “pekerjaan mirip batch yang boleh lambat” tidak memakan worker “pekerjaan yang perlu merespons segera”, Anda membagi pool dan memberi masing-masing anggaran thread sendiri.

Ikat mereka dengan lingkungan callback. Di pool mana pekerjaan berjalan ditentukan dengan menginisialisasi TP_CALLBACK_ENVIRON (lingkungan callback), menunjukkannya ke pool dengan SetThreadpoolCallbackPool, dan meneruskannya sebagai argumen ketiga CreateThreadpoolWork dan sejenisnya.9

Lipat mereka dengan cleanup group. Dalam modul yang membuat banyak objek, pemrosesan shutdown cenderung menjadi hafalan “tunggu semuanya, tutup semuanya”. Jika Anda membuat grup dengan CreateThreadpoolCleanupGroup dan menempelkan setiap objek ke dalamnya lewat lingkungan callback, satu CloseThreadpoolCleanupGroupMembers melakukan tunggu penyelesaian dan pelepasan untuk setiap objek anggota bersama-sama.34

Mengikat konfigurasi lewat lingkungan callbackLingkungan callback menunjuk ke pool kustom dan cleanup group; work dan timer yang dibuat dengan lingkungan itu berjalan di pool itu, dan operasi massal pada cleanup group mengumpulkan tunggu penyelesaian serta pelepasanLingkungan callback (TP_CALLBACK_ENVIRON)Pool kustom (kendalikan jumlah thread)Cleanup groupDiteruskan saat membuat work / timer / wait / ioTunggu selesai dan lepas sekaligus

Gambar 6: Lingkungan callback adalah mekanisme yang menyuntikkan “di pool mana ia berjalan, dan siapa yang membersihkan” pada saat pembuatan objek.

6. Jebakan — disiplin di dalam callback

Hampir setiap bug thread pool berasal dari “berbuat sesuka hati pada thread yang dipinjam”.

Memblokir lama. Pool menyesuaikan jumlah thread dengan asumsi callback kembali segera. Mengerjakan pekerjaan lama atau tunggu lama pada default menunda eksekusi callback lain. Callback yang mungkin berjalan lama harus menyatakan “ini akan berjalan lama” dengan CallbackMayRunLong (pool mengambilnya sebagai petunjuk untuk menambah thread), atau dikirim ke thread khusus sejak awal. Perhatikan bahwa CallbackMayRunLong mengembalikan FALSE ketika ia tidak dapat menyiapkan worker untuk callback lain. Jika Anda tetap memblokir tanpa memeriksa nilai kembalian Anda tetap menyumbat pool, jadi ketika FALSE, jatuh ke sisi yang tidak memblokir — belah pekerjaan, kirim ke thread khusus, dan sebagainya.5

Menunggu penyelesaian secara sinkron pada pool yang sama. Bentuk di mana, di dalam callback A, Anda menunggu dengan WaitForThreadpoolWorkCallbacks atau sejenisnya penyelesaian pekerjaan B yang dikirim ke pool yang sama menjadi deadlock kelaparan pool pada saat setiap worker “menunggu worker lain”. Tulis ulang ketergantungan antar pekerjaan bukan sebagai tunggu melainkan sebagai kelanjutan yang “mengirim yang berikutnya dari callback penyelesaian B”.

Struktur deadlock kelaparan poolJika setiap worker thread menunggu secara sinkron penyelesaian pekerjaan lain yang dikirim ke pool yang sama, tidak ada worker kosong yang tersisa untuk menjalankan pekerjaan itu, dan semua menunggu selamanyaWorker 1: menunggu pekerjaan X selesaiPekerjaan X dan Y menunggu dijalankanWorker 2: menunggu pekerjaan Y selesaiTidak ada worker kosong untuk menjalankannyaSemua menunggu selamanya (deadlock kelaparan)

Gambar 7: Jika Anda menunggu worker secara sinkron dari dalam worker, tidak ada yang tersisa untuk menjalankan pekerjaan yang ditunggu.

Mengotori state thread. Worker thread dipakai ulang untuk callback berikutnya. Perubahan prioritas thread, keadaan inisialisasi COM, nilai yang tertinggal di TLS, kunci yang lupa dilepas — salah satu dari ini menjadi pencemaran callback berikutnya (yang tidak terkait). “Fungsi yang Anda lempar ke pool tidak boleh bergantung pada kepribadian thread” sudah menjadi peringatan resmi sejak era API lama.6 Ada mekanisme khusus untuk pembersihan; misalnya, LeaveCriticalSectionWhenCallbackReturns dapat meminta pool “lepas kunci ini ketika callback ini kembali”.3

Balapan dengan unload DLL. Jika DLL yang berisi kode di-unload sementara callback berjalan, Anda mendapat access violation. Bentuk dasarnya adalah menunggu penyelesaian secara tuntas di fungsi shutdown DLL; FreeLibraryWhenCallbackReturns disediakan untuk situasi “callback ini adalah pekerjaan terakhir, dan ketika selesai saya ingin DLL dibebaskan termasuk diri saya”. API ini, namun, hanya “melepas satu referensi ketika callback yang berjalan kembali”; ia tidak mencegah unload sebelum callback mulai. Anda memakainya sebagai pasangan: ambil referensi modul sendiri dengan GetModuleHandleEx sebelum mengirim, dan biarkan callback melepas referensi itu dengan API ini.3 Dan Anda tidak boleh melakukan tunggu penyelesaian ini di dalam DllMain — seperti dinyatakan di “DllMain dan loader lock”, menunggu thread lain di dalam DllMain adalah pola deadlock.

Bersiap menghadapi balapan antara unload DLL dan callbackUnload DLL sementara callback berjalan menjadi access violation, jadi bentuk dasarnya adalah menunggu penyelesaian di fungsi shutdown eksplisit lalu menutup; ketika callback terakhir sendiri membebaskan DLL, pakai FreeLibraryWhenCallbackReturnsUnload selama callbackAccess violationTunggu, lalu tutupUnload amanDi fungsi shutdownFreeLibraryWhenCallbackReturnscallback terakhir bebaskanBukan di dalam DllMain

Gambar 8: Bentuk dasarnya adalah melakukan “tunggu, lalu tutup” di fungsi shutdown eksplisit. Menunggu di dalam DllMain mengundang deadlock yang berbeda.

Pengecualian dan crash di dalam pekerjaan yang dikirim. Pengecualian yang tidak tertangani pada worker thread membawa proses ikut serta. Terapkan kebijakan menangkap pengecualian secara menyeluruh di pintu masuk callback dan mencatatnya, sama seperti yang Anda lakukan untuk fungsi thread milik thread khusus.

7. Bagaimana ini berhubungan dengan pustaka standar dan .NET — di lapisan mana Anda menulis

Akhirnya, kita menata bagaimana ini duduk bersama alat lain.

  • Jika C++ std::async / std::thread mencukupi, mereka kandidat pertama. Mereka portabel, kodenya pendek, dan bahkan semantik future sudah ditetapkan oleh standar.10
  • Alasan memakai Win32 thread pool secara langsung adalah ketika Anda (1) ingin mekanisme callback terpadu yang mencakup timer, wait, dan io, (2) ingin pembagian pool atau pengendalian jumlah thread, atau (3) tidak ingin menahan thread sendiri di dalam DLL atau komponen COM.
  • Di sisi .NET, ThreadPool dan Task memainkan peran yang sama, dan penyelesaian I/O terikat pada IOCP. Struktur basement ini dijelaskan di “IOCP dan thread pool .NET”.
Memutuskan alat lapisan mana yang dipakai menulisJika async atau thread C++ standar mencukupi, pakai itu; pakai Win32 thread pool secara langsung ketika Anda membutuhkan integrasi timer, tunggu, dan penyelesaian I/O, pembagian pool atau pengendalian jumlah thread, atau Anda tidak ingin thread sendiri di dalam DLL atau komponen COMYaTidakIntegrasi timer, wait, dan ioPembagian pool / kendali jumlahMenghindari thread sendiri di DLLAlat C++ standar mencukupi?std::async / std::threadApa yang Anda butuhkan?Win32 thread pool

Gambar 9: Ketika ragu, mulai dengan pustaka standar; giliran API ini datang ketika persyaratan yang tidak dapat diungkapkannya muncul.

Dengan kata lain, API ini adalah fondasi konkurensi pada titik Anda sudah memutuskan untuk “menulis native”. Pembersihan yang realistis adalah bertahap: sebagai target migrasi dari berkembang biaknya CreateThread sendiri, mulai dengan memperkenalkan objek work, lalu ganti thread yang hanya menunggu dengan wait dan thread timer dengan timer.

8. Ringkasan

  • Untuk mengeluarkan banyak pekerjaan berumur pendek, dan untuk merapikan thread yang hanya menunggu serta yang hanya timer, thread pool standar OS daripada thread sendiri. Yang Anda pakai adalah API baru dari Vista ke atas.
  • Di pusat ada empat objek work, timer, wait, dan io. Syarat penyalaannya berbeda; mereka disatukan pada kelompok worker yang sama dan gaya callback yang sama.
  • Tata kramanya adalah “buat → kirim → tunggu penyelesaian → tutup”. Beberapa pengiriman berjalan paralel. Cleanup group dapat meratakan pemrosesan shutdown.
  • Tiga aturan besi callback: jangan memblokir lama (jika akan, CallbackMayRunLong); jangan menunggu secara sinkron pada pool yang sama; jangan kotorin state thread.
  • Pemakaian dari DLL: waspadai balapan unload. Tunggu penyelesaian di fungsi shutdown eksplisit; jangan lakukan di DllMain.
  • Di mana C++ standar atau .NET mencukupi, pakai itu. Giliran API ini adalah ketika Anda membutuhkan integrasi timer/wait/io atau pengendalian pool.

Thread-pool API, di antara Win32 API, berada di sisi yang lebih baru dan lebih baik dirancang. Setelah Anda selesai beralih dari gagasan “membuat thread” ke gagasan “melempar callback”, konkurensi di kode native menjadi jauh lebih jernih untuk ditulis.

Artikel terkait

Area konsultasi terkait

KomuraSoft LLC menangani desain migrasi dari kode native yang thread-nya berkembang biak ke thread pool, tinjauan desain pemrosesan konkuren di aplikasi dan DLL C++, serta investigasi akar hang dan crash yang disebabkan kelaparan pool atau callback. Anda dipersilakan berkonsultasi mulai dari inventaris kode yang ada.

Tautan referensi

  1. Microsoft Learn, Thread Pools. Tentang thread pool sebagai kumpulan worker thread yang mengeksekusi callback asinkron secara efisien atas nama aplikasi; tentang jenis aplikasi yang cocok (mengeluarkan banyak item kerja kecil secara paralel, sering membuat dan merobohkan thread berumur pendek, memproses pekerjaan independen secara paralel, tunggu eksklusif pada objek kernel, dan sebagainya); dan tentang desain ulang Vista yang menyeluruh (penyatuan jenis worker thread, satu antrian timer, persistent thread khusus, cleanup group, beberapa pool dalam satu proses, dan API baru).  2 3 4 5

  2. Microsoft Learn, Thread Pooling. Tentang struktur API thread pool yang lebih lama (QueueUserWorkItem, antrian timer, registered wait, BindIoCompletionCallback); tentang tidak adanya cara membatalkan pekerjaan setelah diantrekan; dan tentang API thread pool baru yang diperkenalkan di Vista dinyatakan lebih sederhana dan unggul dalam keandalan, performa, dan fleksibilitas.  2

  3. Microsoft Learn, threadpoolapiset.h header. Tentang daftar fungsi yang mencakup empat fungsi pembuatan objek CreateThreadpoolWork, CreateThreadpoolTimer, CreateThreadpoolWait, dan CreateThreadpoolIo; cleanup group (CreateThreadpoolCleanupGroup); dan pembersihan yang terikat pada penyelesaian callback (LeaveCriticalSectionWhenCallbackReturns, FreeLibraryWhenCallbackReturns, dan sebagainya).  2 3 4 5 6

  4. Microsoft Learn, Using the Thread Pool Functions. Tentang prosedur dasar membuat dengan CreateThreadpoolWork, mengirim dengan SubmitThreadpoolWork, menunggu penyelesaian dengan WaitForThreadpoolWorkCallbacks, dan menutup dengan CloseThreadpoolWork; serta contoh konfigurasi yang menggabungkan pool kustom dengan lingkungan callback dan cleanup group.  2 3

  5. Microsoft Learn, CallbackMayRunLong function (threadpoolapiset.h). Tentang memberitahu pool bahwa callback saat ini mungkin berjalan lama, sehingga pool dapat memakai itu sebagai bahan memutuskan apakah mengamankan thread untuk callback lain; dan tentang mempertimbangkan thread khusus untuk callback yang berjalan lama bila memungkinkan.  2

  6. Microsoft Learn, Thread Pooling. Tentang item kerja yang dikirim ke thread pool, dan fungsi yang mereka panggil, harus aman untuk thread pool; tentang tidak mengasumsikan bahwa thread yang mengeksekusi adalah thread khusus yang persisten; dan tentang menghindari pemakaian TLS serta panggilan asinkron yang mensyaratkan thread persisten.  2

  7. Microsoft Learn, SubmitThreadpoolWork function (threadpoolapiset.h). Tentang dapat mengirim objek work yang sama lebih dari sekali tanpa menunggu callback sebelumnya selesai, sehingga callback berjalan paralel; dan tentang pool yang dapat menyesuaikan (throttle) jumlah thread demi efisiensi. 

  8. Microsoft Learn, SetThreadpoolThreadMaximum function (threadpoolapiset.h). Tentang dapat menetapkan batas atas jumlah worker thread untuk pool yang dibuat dengan CreateThreadpool (batas bawah adalah SetThreadpoolThreadMinimum). 

  9. Microsoft Learn, CreateThreadpoolWork function (threadpoolapiset.h). Tentang membuat objek work dari fungsi callback dan pointer context; dan tentang argumen ketiga, TP_CALLBACK_ENVIRON, yang dapat menentukan lingkungan eksekusi callback (pool tempat ia berada, dan sebagainya), dengan NULL berarti ia berjalan di lingkungan default. 

  10. Microsoft Learn, <future>. Tentang eksekusi asinkron per tugas lewat std::async dan future yang disediakan sebagai pustaka standar, sehingga Anda dapat menulis konkurensi tanpa mengelola thread secara langsung. 

Artikel terbaru dengan tag yang sama untuk mendalami topik-topik terdekat.

Halaman-halaman ini menempatkan topik dalam konteks layanan dan keputusan yang lebih luas.

Artikel ini berkaitan langsung dengan layanan berikut.

Pertanyaan yang sering diajukan

Pertanyaan yang sering muncul dalam konsultasi tentang topik artikel ini.

Apa kelebihan thread pool dibanding membuat thread sendiri dengan CreateThread?
Efisiensi ketika Anda punya banyak pekerjaan berumur pendek, dan pengurangan kode manajemen thread. Membuat dan menghancurkan thread punya biaya yang tidak bisa diabaikan, jadi aplikasi yang mengulang "CreateThread untuk setiap pekerjaan lalu hancurkan setelah selesai", atau yang menahan banyak thread yang hanya ada untuk tidur menunggu event, dapat mengurangi jumlah thread dan context switch dengan pindah ke pool. Dokumentasi resmi juga mencantumkan sebagai kandidat pool aplikasi yang mengeluarkan banyak item kerja kecil secara paralel, aplikasi yang membuat banyak thread berumur pendek, dan aplikasi yang punya thread yang dikhususkan semata-mata untuk menunggu objek kernel. Sebaliknya, pekerjaan yang "membutuhkan kepribadian sendiri pada thread" — perubahan prioritas, COM STA, pemrosesan khusus yang berjalan lama — tetap harus dipegang pada thread khusus seperti semula.
Apa bedanya dengan fungsi thread pool lama seperti QueueUserWorkItem?
Thread pool didesain ulang secara menyeluruh di Windows Vista. API keluarga threadpoolapiset saat ini (CreateThreadpoolWork dan sebagainya) adalah API baru; QueueUserWorkItem, RegisterWaitForSingleObject, dan sejenisnya adalah API lama (legacy). API baru menyatukan jenis worker thread, membiarkan Anda membuat beberapa pool independen dalam satu proses, dan menyediakan mekanisme seperti pelepasan massal lewat cleanup group serta pelepasan kunci atau unload DLL yang terikat pada penyelesaian callback. Dokumentasi resmi juga mengatakan API baru lebih sederhana dan unggul dalam keandalan, performa, dan fleksibilitas. API lama juga punya kendala struktural seperti "tidak ada cara membatalkan pekerjaan setelah diantrekan", jadi pakai API baru di kode baru.
Apakah ada hal yang tidak boleh dilakukan di dalam callback?
Ada tiga yang besar. Pertama, memblokir lama atau mengerjakan pekerjaan lama pada default. Pool menyesuaikan jumlah thread dengan asumsi callback selesai segera, jadi untuk pekerjaan yang akan memakan waktu lama Anda menyatakan itu dengan CallbackMayRunLong atau memakai thread khusus. Kedua, menunggu secara sinkron penyelesaian pekerjaan lain yang Anda kirim ke pool yang sama. Jika setiap worker berakhir "menunggu worker lain", Anda mendapat deadlock kelaparan pool. Ketiga, bergantung pada kepribadian thread. Worker thread dibagi antar callback, jadi kembali dengan prioritas thread atau keadaan inisialisasi COM yang berubah, atau meninggalkan state di TLS, mencemari callback berikutnya. Untuk pembersihan di akhir (melepas kunci atau unload DLL), mekanisme khusus seperti LeaveCriticalSectionWhenCallbackReturns dan FreeLibraryWhenCallbackReturns disediakan.
Apakah ada poin yang perlu diwaspadai saat memakai thread pool dari DLL?
Bahaya terbesar adalah "DLL di-unload sementara callback masih berjalan". Jika callback berjalan setelah unload, Anda mendapat access violation. Sisi DLL harus, dalam pemrosesan shutdown-nya, menunggu secara andal penyelesaian callback yang dikeluarkannya — dengan fungsi tunggu seperti WaitForThreadpoolWorkCallbacks, atau CloseThreadpoolCleanupGroupMembers pada cleanup group — dan baru kemudian menutup objek. Menunggu ini di dalam DllMain, namun, dapat deadlock lewat interaksi dengan loader lock, jadi aturannya adalah melakukannya di fungsi shutdown eksplisit, bukan di DllMain. Untuk situasi di mana callback sendiri ingin membebaskan DLL karena "pekerjaan ini yang terakhir", API khusus, FreeLibraryWhenCallbackReturns, disediakan.
Sekarang kita punya C++ std::async dan ThreadPool .NET, apakah masih ada kesempatan memakai API ini secara langsung?
Ada. Kriterianya adalah "apakah alat di lapisan itu mencukupi". Jika granularitas konkurensi yang Anda butuhkan di C++ tercakup oleh std::async atau std::thread, pustaka standar adalah kandidat pertama dari sudut portabilitas juga. Di sisi lain, ingin menyatukan timer, tunggu objek kernel, dan penyelesaian I/O asinkron dalam satu mekanisme callback; ingin membagi pool dan mengendalikan jumlah thread per jenis pekerjaan; tidak ingin menahan thread sendiri di dalam DLL atau komponen COM — persyaratan itu yang dicakup Win32 thread pool. Hubungan dengan ThreadPool .NET dan IOCP dibahas di artikel terkait, dan selama Anda menulis native, mengenal mekanisme yang duduk di lapisan bawah ini tidak sia-sia.

Profil penulis

Halaman perkenalan penulis artikel.

Go Komura

Direktur KomuraSoft LLC

Berspesialisasi dalam pengembangan perangkat lunak Windows, konsultasi teknis, dan investigasi bug, terutama pada proyek dengan sistem yang sudah ada dan bug yang sulit direproduksi.

Kembali ke blog