Praktik terbaik multithreading praktis: edisi C — menulis dengan aman cara Win32 API
· Go Komura · Windows, Multithreading, C, Win32 API, Aplikasi bisnis, Investigasi bug, Desain
“Saya menulis proses residen untuk kendali peralatan dalam C.” “Kami akhirnya menambahkan thread ke aplikasi C berumur dua puluh tahun.” “Kami menghentikan thread dengan TerminateThread, tetapi seluruh proses sesekali macet.” — Multithreading di C adalah dunia di mana bahasa memberi bantuan paling sedikit. Tidak ada pengecualian, tidak ada RAII, tidak ada template; kebenaran sinkronisasi seluruhnya bertumpu pada API mana yang Anda pilih dan seberapa disiplin Anda memanggilnya.
Artikel ini adalah edisi C dari seri multithreading praktis. Ditujukan kepada pengembang yang menulis C terhadap Win32 API, artikel ini mengambil prinsip desain multithreading — berhenti memproduksi thread secara massal, kurangi keadaan berubah yang dibagi, jaga disiplin kunci, dan rancang cara berhenti sebelum merancang cara mulai — lalu memetakannya ke perangkat Win32: cara membuat thread (_beginthreadex), cara memilih objek sinkronisasi, cara merancang jalur berhenti tanpa TerminateThread, dan batasan DllMain, semuanya disusun di sekitar sumber primer per Agustus 2026. Ditulis agar dapat dibaca sendiri. Prinsip yang sama, dijabarkan untuk bahasa lain, juga ada di edisi .NET, edisi C++, dan edisi Java.
1. Kesimpulan lebih dulu
- Buat thread dengan
_beginthreadex, bukanCreateThread. Jika thread yang memanggil CRT dibuat dengan CreateThread, CRT dapat mengakhiri proses saat memori menipis.12 - Kunci bawaan di dalam proses adalah kunci SRW; pakai CRITICAL_SECTION hanya ketika perolehan rekursif dibutuhkan. Memakai Mutex untuk pengecualian di dalam proses adalah “kesalahan umum” yang selalu melibatkan transisi ke mode kernel.3
- Perbarui variabel tunggal dengan keluarga fungsi Interlocked.
volatiletidak menjamin atomisitas maupun urutan. Sebagian besar fungsi Interlocked membawa penghalang memori penuh.4 - Tunggu memakai variabel kondisi (keluarga
SleepConditionVariableCS) atau peristiwa plus fungsi tunggu. Loop polling berbasisSleepmembuang CPU dan daya tanggap.5 - Jangan pernah memakai
TerminateThread. Itu fungsi berbahaya yang merusak kunci, heap, dan keadaan DLL, dan menjadi sasaran peringatan analisis kode C6258. Rancang berhenti sebagai shutdown kooperatif: peristiwa berhenti plusWaitForMultipleObjects.67 - Serahkan pekerjaan pendek ke kumpulan thread Windows (
CreateThreadpoolWork) alih-alih memparalelkannya dengan thread Anda sendiri. Jangan pernah mengakhiri thread kumpulan denganExitThread/TerminateThread.89 - Jangan membuat thread, menyinkronkan, atau menunggu thread selesai di dalam DllMain. Ia dipanggil saat loader lock dipegang, sehingga menjadi lahan subur deadlock.10
<threads.h>C11 dapat dipakai dari VS 2022 17.8 ke atas, tetapi<stdatomic.h>masih experimental. Untuk basis kode hanya-Windows, pendekatan Win32 adalah pilihan realistis.11
2. Mengapa multithreading sulit? — Race condition dan deadlock
Rebus masalah yang dibawa multithreading, terlepas dari bahasanya, dan ada dua jenis.
Race condition adalah bug di mana hasil berubah tergantung urutan beberapa thread mencapai potongan kode tertentu. Contoh klasiknya adalah penghitung bersama: ekspresi tunggal count++ terurai di tingkat kode mesin menjadi tiga langkah — baca, tambah, tulis kembali. Jika dua thread masuk ke tiga langkah ini pada saat yang sama, penambahan satu thread tertimpa dan hilang ketika yang lain menulis kembali.4 Hasilnya berubah dari satu jalan ke jalan lain, dan hasil mana yang Anda dapat tidak dapat diprediksi.
sequenceDiagram
participant A as Thread A
participant M as Variabel bersama count
participant B as Thread B
Note over M: count = 10
A->>M: Baca (10)
B->>M: Baca (10)
A->>A: Tambah di lokal (11)
B->>B: Tambah di lokal (11)
A->>M: Tulis kembali (11)
B->>M: Tulis kembali (11)
Note over M: count = 11 meski dua increment<br/>Increment thread A hilang
Gambar 1: Race condition klasik di mana penghitung bersama kehilangan satu increment. Jika thread lain menyelip di dalam tiga langkah count++, tulis-kembali mana pun yang terjadi terakhir menimpa yang lain
Deadlock adalah keadaan di mana dua thread masing-masing menunggu kunci yang dipegang yang lain, dan tidak satu pun dapat maju. Thread A memegang kunci 1 dan menunggu kunci 2; thread B memegang kunci 2 dan menunggu kunci 1 — itu saja cukup agar keduanya berhenti selamanya.
flowchart LR
A["Thread A<br/>memegang kunci 1"] -->|"menunggu kunci 2 dilepas"| B["Thread B<br/>memegang kunci 2"]
B -->|"menunggu kunci 1 dilepas"| A
Gambar 2: Tunggu melingkar pada deadlock. Begitu panah menunggu membentuk cincin, setiap thread di cincin itu berhenti selamanya
Keduanya bergantung waktu: kombinasi urutan eksekusi yang hanya muncul sekali dalam puluhan ribu jalan di mesin pengembangan dapat terjadi setiap hari di mesin pelanggan dengan jumlah inti dan waktu yang berbeda. “Tidak tereproduksi saat debugger terpasang” terjadi karena pengamatan itu sendiri mengubah waktu — perilaku khas bug race. Jadi setiap prinsip di artikel ini mengarah ke satu arah: kurangi tempat yang butuh sinkronisasi, sebelum Anda khawatir menyinkronkan dengan benar.
2.1. Asumsi khusus C — Bahasa tidak melindungi Anda dari apa pun
Di C, karena bahasa tidak punya mekanisme untuk menegakkan prinsip ini, semuanya perlu ditulis secara eksplisit sebagai disiplin.
Pertama, bangun jaminan pelepasan ke dalam struktur. Tanpa padanan RAII C++, melepas kunci dan memanggil CloseHandle pada handle harus dilindungi oleh pola goto cleanup yang menyalurkan setiap keluar fungsi lewat satu tempat, atau oleh konvensi pengodean yang memasangkan setiap perolehan dengan pelepasan. Menambah return dini dan kemudian membocorkan kunci adalah kecelakaan klasik C.
Kedua, perlakukan data race sama seperti C++. Baca atau tulis sederhana pada variabel 32-bit yang disejajarkan dengan benar bersifat atomik di Windows, tetapi tidak lebih dari itu — variabel 64-bit di Windows 32-bit, operasi majemuk, atau konsistensi lintas beberapa variabel — tidak dijamin sama sekali.12 Kode yang “kebetulan bekerja” rusak begitu kompiler atau tingkat optimisasi berubah.
Ketiga, putuskan kepemilikan. Budaya menuliskan, di komentar fungsi, “thread mana yang menulis buffer ini, dan sejak kapan ia milik siapa” membuahkan hasil di multithreading C sama besarnya dengan pilihan primitif sinkronisasi.
3. Cara membuat thread — _beginthreadex, dan tidak yang lain
3.1. Mengapa CreateThread adalah pilihan yang salah
API native Win32 adalah CreateThread, tetapi panduan resmi menyatakan bahwa setiap thread yang memanggil fungsi CRT (C runtime) harus dibuat dengan _beginthreadex. _beginthreadex menginisialisasi data internal per-thread yang dibutuhkan CRT sebelum memulai thread. Jika thread yang dibuat dengan CreateThread memanggil fungsi CRT, CRT dapat mengakhiri proses dalam kondisi memori rendah.12 Karena printf, malloc, dan strtok semuanya fungsi CRT, aturan praktisnya: “thread yang ditulis dalam C selalu memakai _beginthreadex”.
Hindari juga _beginthread (tanpa ex). Ia punya jebakan: jika thread yang dibuatnya selesai lebih dulu, handle yang dikembalikan sudah bisa tidak valid — atau bahkan menunjuk ke thread lain — sedangkan _beginthreadex, yang handlenya dapat diteruskan dengan aman ke API sinkronisasi, adalah pilihan yang lebih aman. Pemanggil menutup handle yang dikembalikan _beginthreadex dengan CloseHandle.13
#include <process.h>
static unsigned __stdcall WorkerMain(void* arg)
{
WorkerContext* ctx = (WorkerContext*)arg;
/* ... loop tunggu dari Bagian 5 dan 6 ... */
return 0;
}
HANDLE hThread = (HANDLE)_beginthreadex(
NULL, 0, WorkerMain, &ctx, 0, NULL);
if (hThread == NULL) { /* penanganan kegagalan */ }
/* ...setelah permintaan berhenti... */
WaitForSingleObject(hThread, INFINITE); /* join */
CloseHandle(hThread);
3.2. Pekerjaan pendek pergi ke kumpulan thread Windows
Jika Anda ingin “melemparkan banyak pekerjaan kecil ke sesuatu” atau mendapati diri “membuat dan menghancurkan thread berumur pendek berulang-ulang”, pakai kumpulan thread Windows (API kumpulan thread yang tersedia dari Vista ke atas) alih-alih thread Anda sendiri. Buat objek work dengan CreateThreadpoolWork dan kirim dengan SubmitThreadpoolWork, dan thread pekerja kumpulan menjalankan callback secara paralel.8 Pengelolaan jumlah thread diserahkan ke OS, dan biaya membuat serta menghancurkan thread hilang. Inilah jawaban C terhadap prinsip “jangan membuat thread sendiri”.
Disiplin memakai kumpulan juga didokumentasikan secara resmi: jangan pernah mengakhiri thread kumpulan dengan TerminateThread / ExitThread; pulihkan setiap keadaan yang Anda ubah di dalam callback (TLS, prioritas thread, dan seterusnya) sebelum kembali; dan jaga handle tunggu tetap hidup sampai kumpulan selesai memakainya.9 Satu catatan praktis lagi: kumpulan hanya membatasi jumlah thread pekerja — jumlah callback yang belum dijalankan yang diantrekan lewat SubmitThreadpoolWork dapat menumpuk tanpa batas. Dalam konfigurasi berjalan lama di mana pengiriman terus mendahului pemrosesan, taruh pembatas masuk seperti semafor, atau antrean berbatas, di sisi aplikasi, agar sisi yang mengirim menunggu atau ditolak begitu penuh (ini back-pressure agar kelebihan beban tidak menjadi masalah memori, dan prinsip yang sama dengan desain antrean di Bagian 5).
4. Minimalkan keadaan berubah yang dibagi — Partisi, hanya-baca, dan serah-terima
Race hanya terjadi ketika “beberapa thread” dan “data berubah yang dibagi” keduanya ada. Sebelum memilih primitif sinkronisasi (bab berikutnya), pikirkan apakah Anda dapat mengurangi pembagian sejak awal. Ada tiga keluarga teknik.
Partisi. Untuk agregasi paralel, alih-alih setiap thread menulis ke penghitung bersama, bangun subtotal di variabel lokal per-thread (atau buffer yang dialokasikan per thread), dan gabungkan sekali saja di akhir dengan sesuatu seperti InterlockedAdd. Tulis ke keadaan bersama turun dari “setiap iterasi” menjadi “sekali per thread”, dan baik biaya sinkronisasi maupun jendela persaingan menyusut berkali lipat. Disiplin kepemilikan dari Bagian 2.1 — “buffer ini milik thread mana” — menjadi cetak biru cara Anda mempartisi.
Jadikan hanya-baca. Konfigurasi dan tabel yang dibangun saat mulai dan tidak pernah diubah setelahnya aman dibaca dari berapa pun banyaknya thread setelah inisialisasi selesai. Entah selesaikan semua inisialisasi sebelum thread mana pun mulai, atau, jika inisialisasi malas diperlukan, pakai inisialisasi sekali Win32 (InitOnceExecuteOnce), dan buat batas — “sejak kapan ini menjadi hanya-baca” — eksplisit di kode.3
Serahkan. Alih-alih kedua sisi menyentuh variabel bersama, alirkan data antar thread lewat antrean produsen/konsumen. Di C, implementasinya tepat pola variabel kondisi dari Bagian 5 (buffer sirkular berbatas plus SleepConditionVariableCS), yang merupakan contoh resmi yang dikerjakan; buffer dengan batas kapasitas juga memberi back-pressure alami — produksi menunggu begitu ia mendahului konsumsi.5
5. Memilih objek sinkronisasi dan disiplin kunci
Win32 punya banyak jenis primitif sinkronisasi, dan memilih yang salah merugikan kinerja maupun kebenaran. Berikut panduan resmi yang diringkas dalam satu gambar.3
flowchart TB
S{"Sinkronisasi lintas<br/>proses dibutuhkan?"} -->|"Ya"| Q2{"Untuk apa?"}
Q2 -->|"Pengecualian bersama"| MTX["Mutex bernama"]
Q2 -->|"Membatasi jumlah akses bersamaan"| SEM["Semafor bernama"]
Q2 -->|"Pemberitahuan peristiwa"| EVT["Peristiwa bernama"]
S -->|"Tidak - internal proses"| Q3{"Perolehan rekursif<br/>oleh thread yang sama dibutuhkan?"}
Q3 -->|"Ya"| CS["CRITICAL_SECTION"]
Q3 -->|"Tidak"| Q4{"Kode C++ portabel<br/>jadi prioritas?"}
Q4 -->|"Ya"| STD["std::mutex /<br/>std::shared_mutex"]
Q4 -->|"Tidak"| SRW["Kunci SRW - pilihan bawaan"]
Gambar 3: Cara memilih primitif sinkronisasi Win32. Cabang pertama adalah “apakah ia menyeberangi proses” — inti soalnya adalah jangan meraih objek kernel (Mutex) ketika ia tidak
| Primitif | Lingkup | Ciri | Tempat memakainya |
|---|---|---|---|
| Kunci SRW | Internal proses | Cepat (biasanya tetap seluruhnya di mode pengguna), seukuran pointer, tidak rekursif | Bawaan untuk kode baru. AcquireSRWLockShared juga memungkinkan akses baca bersama |
| CRITICAL_SECTION | Internal proses | Cepat (berputar, lalu jatuh ke tunggu kernel), rekursif | Ketika thread yang sama butuh perolehan rekursif |
| Mutex | Internal proses / lintas proses | Selalu objek kernel, jadi lebih lambat | Pengecualian lintas proses (bernama), atau digabung dengan WaitForMultipleObjects |
| Semafor | Internal proses / lintas proses | Objek kernel | Membatasi akses bersamaan ke kumpulan sumber daya |
| Peristiwa | Internal proses / lintas proses | Objek kernel | Memberitahu bahwa “sesuatu terjadi” (bukan untuk melindungi data) |
| Fungsi Interlocked | Internal proses (lintas proses juga, lewat memori bersama) | Operasi atomik tanpa kunci | Penghitung, flag, tukar pointer4 |
Satu catatan kaki untuk tabel dan bagan alir. Objek kernel seperti peristiwa, semafor, dan mutex bekerja dengan sempurna untuk sinkronisasi internal proses ketika dibuat tanpa nama (peristiwa berhenti di Bagian 6 tepat sebuah peristiwa tanpa nama). Objek kernel tidak berarti hanya lintas proses. Sebaliknya, “tanpa nama” juga tidak berarti ketat “terkurung di satu proses” — jika Anda membiarkan proses anak mewarisi handle, atau menduplikasinya ke proses lain dengan DuplicateHandle, objek kernel yang sama dapat dipakai dari beberapa proses bahkan tanpa nama. Cara akurat mengungkapkannya adalah bahwa penamaan adalah satu cara representatif agar proses dapat membuka kembali objek yang sama. Cabang di Gambar 3 menangkap inti pilihan sebagai “jangan pilih objek kernel untuk kunci internal proses” — untuk pemberitahuan internal proses (peristiwa) atau pembatasan konkurensi (semafor), objek kernel tanpa nama tetap jawaban yang benar.
Keluarga Interlocked setara dengan kelas Interlocked di edisi .NET dan std::atomic di edisi C++. InterlockedIncrement / InterlockedExchange / InterlockedCompareExchange menjalankan operasi pada satu variabel secara tak terpisahkan, dan karena sebagian besar fungsi ini membawa penghalang memori penuh, Anda juga mendapat jaminan urutan.4 “Tidak apa-apa karena saya menandainya volatile” adalah kesalahpahaman — volatile tidak menjamin atomisitas maupun urutan (lihat FAQ). Ada satu prasyarat lagi: penyejajaran. Variabel yang menjadi sasaran fungsi Interlocked harus disejajarkan pada batas alami (batas 4-byte untuk nilai 32-bit, batas 8-byte untuk nilai 64-bit); jika tidak, perilakunya tidak dapat diprediksi.12 Jangan pernah menyasar medan di dalam struct #pragma pack, atau medan di buffer yang dipetakan langsung ke format kabel, dengan fungsi Interlocked. Batasi penghitung dan flag pada variabel yang dideklarasikan biasa — yang disejajarkan kompiler untuk Anda. Ada juga peringatan khusus seputar menukar pointer dengan sesuatu seperti InterlockedExchangePointer: hanya tukar itu sendiri yang tak terpisahkan, dan tidak ada yang menjamin masa hidup blok lama setelah tukar. Jika pembaca memuat pointer lama tepat saat penulis menukarnya dan memanggil free, itu akses ke memori yang sudah dibebaskan. Desain yang memperbarui data bersama dengan menukar pointer hanya bekerja ketika dipasangkan dengan protokol reklamasi — kunci, hitungan referensi, atau serupa (jika ragu, melindunginya dengan kunci SRW adalah bawaan yang aman).
Untuk menunggu sesuatu, ada variabel kondisi. Buat satu dengan InitializeConditionVariable; konsumen tidur di SleepConditionVariableCS (dipasangkan dengan CRITICAL_SECTION), dan produsen membangunkannya dengan WakeConditionVariable — ini tepat bentuk contoh resmi antrean produsen/konsumen di atas buffer berbatas.5 Disiplin penting: saat bangun, selalu periksa ulang kondisi (apakah antrean tidak kosong) di dalam kunci, dan putar kembali ke menunggu jika salah. Variabel kondisi dapat mengalami spurious wakeup tanpa pemberitahuan sama sekali, dan pada saat Anda bangun konsumen lain mungkin sudah mengambil item lebih dulu — jadi “saya dibangunkan” tidak serta-merta berarti “kondisinya berlaku”. Ini adalah alat C untuk membangun bentuk yang sama dengan channel di edisi .NET Bagian 4.3 dan BlockingQueue di edisi C++ Bagian 4. Ketika memasangkan dengan kunci SRW, pakai SleepConditionVariableSRW.
5.1. Disiplin kunci — Tiga prinsip yang berlaku terlepas dari primitif mana yang Anda pilih
Memilih primitif yang tepat saja tidak cukup — tanpa pemakaian yang disiplin, Anda tetap tidak akan mencegah race.
- Putuskan, satu-ke-satu, kunci mana yang melindungi data mana. Tetapkan tepat satu kunci (kunci SRW atau CRITICAL_SECTION) ke setiap himpunan data berubah yang ingin Anda lindungi, dan ambil kunci yang sama di setiap tempat yang menyentuh data itu. Di C khususnya, membuahkan hasil menuliskannya secara eksplisit di komentar header — “struct ini dilindungi oleh
g_lockFoo”. - Jangan melakukan apa pun yang lambat atau eksternal saat memegang kunci. Satu-satunya yang boleh dilakukan saat memegang kunci adalah membaca atau menulis data yang dilindunginya. I/O berkas, panggilan jaringan, atau pemanggilan callback yang dilakukan masih sambil memegang kunci memperpanjang berapa lama Anda memegangnya dan berisiko callee mencoba mengambil kunci lain, menciptakan tunggu melingkar dari Gambar 2.
- Tetapkan urutan perolehan untuk beberapa kunci. Di mana pun dua atau lebih kunci diambil, jadikan aturan bahwa setiap thread mengambilnya dalam urutan yang sama (hierarki kunci). Dokumentasi praktik terbaik DLL menyatakan secara eksplisit bahwa membalik urutan itu (lock order inversion) menghasilkan deadlock yang sulit di-debug, dan bahwa Anda harus mendefinisikan hierarki dan mengikutinya secara konsisten.10
6. Merancang cara berhenti — Jangan pernah TerminateThread
6.1. Apa yang dirusak TerminateThread
TerminateThread menghapus thread sasaran tanpa membiarkannya menjalankan kode user-mode sama sekali. Akibat yang didaftar dokumentasi resmi parah. Jika thread sasaran sedang memegang critical section, ia tidak pernah dilepas; jika ia sedang di tengah operasi heap, kunci heap tetap dipegang (dan setiap thread berikutnya yang memanggil malloc macet); dan jika ia sedang memanipulasi keadaan global suatu DLL, keadaan itu dibiarkan rusak. Posisi resmi adalah bahwa itu “fungsi berbahaya yang hanya boleh dipakai dalam kasus paling ekstrem”, dan analisis kode menandainya sebagai peringatan C6258.67
Menemukan TerminateThread saat menyelidiki aplikasi yang “sesekali macet seluruhnya” memang pemandangan umum di praktik. Jika Anda menemukannya, perlakukan sebagai sesuatu yang perlu diperbaiki.
6.2. Pola yang benar: peristiwa berhenti plus WaitForMultipleObjects
Pola mapan untuk shutdown kooperatif di C adalah membuat satu peristiwa berhenti reset-manual, dan membiarkan setiap thread pekerja menunggu “sinyal kerja” dan “sinyal berhenti” pada saat yang sama. Dokumentasi peringatan C6258 sendiri menunjuk tepat pola ini — buat peristiwa, biarkan setiap thread mengawasinya dengan WaitForSingleObject, dan biarkan thread mengakhiri dirinya sendiri — sebagai cara yang benar untuk mengakhiri.7
HANDLE hStopEvent; /* CreateEvent(NULL, TRUE, FALSE, NULL): reset-manual */
HANDLE hWorkEvent; /* CreateEvent(NULL, FALSE, FALSE, NULL): reset-otomatis.
Secara otomatis kembali ke non-signaled saat diterima
(peristiwa reset-manual akan membiarkan tunggu lewat
setelah sekali di-signal, menjadi busy loop yang
terus berputar pada antrean kosong) */
static unsigned __stdcall WorkerMain(void* arg)
{
HANDLE waits[2] = { hStopEvent, hWorkEvent };
for (;;) {
DWORD r = WaitForMultipleObjects(2, waits, FALSE, INFINITE);
if (r == WAIT_FAILED) { /* mis. handle tidak valid. Dibiarkan, ini berputar kencang */
LogLastError(); /* Catat GetLastError() lalu keluar */
break;
}
if (r == WAIT_OBJECT_0) /* Berhenti diminta */
break;
if (r == WAIT_OBJECT_0 + 1) { /* Ada kerja */
/* Teruskan juga peristiwa berhenti ke ProcessNextItem: jika ia menunggu lama
secara internal untuk satu item, dan tidak dapat mengamati berhenti di sana juga,
shutdown jadi sandera item itu */
while (ProcessNextItem(hStopEvent)) { /* Proses satu item dari antrean; FALSE jika kosong */
/* Periksa permintaan berhenti selama pengosongan juga. Lewati ini dan Anda
tidak bisa berhenti selama kerja terus menumpuk (stop starvation) */
if (WaitForSingleObject(hStopEvent, 0) == WAIT_OBJECT_0)
break;
}
}
}
Cleanup(); /* Lakukan pembersihan sendiri */
return 0; /* Akhiri sendiri */
}
BOOL StopWorkers(HANDLE* threads, DWORD count)
{
BOOL ok = TRUE;
if (!SetEvent(hStopEvent)) { /* Jika permintaan berhenti tidak sampai, */
LogLastError(); /* jangan lanjut ke join tanpa batas */
return FALSE;
}
/* Semua sekarang punya permintaan berhenti diangkat sekaligus */
for (DWORD i = 0; i < count; i++) {
if (WaitForSingleObject(threads[i], INFINITE) == WAIT_OBJECT_0) {
CloseHandle(threads[i]); /* Hanya tutup handle yang join-nya kita konfirmasi */
} else {
LogLastError(); /* WAIT_FAILED: mis. handle tidak valid */
ok = FALSE; /* Jangan laporkan "semua berhenti" */
}
}
return ok; /* Jika FALSE, jangan lanjut melepaskan sumber daya bersama */
}
Ada alasan pihak yang menghentikan melakukan join satu thread pada satu waktu dengan WaitForSingleObject. WaitForMultipleObjects dapat menunggu paling banyak MAXIMUM_WAIT_OBJECTS (64) handle sekaligus; serahkan array yang lebih besar dan tunggu itu sendiri gagal dengan WAIT_FAILED, meninggalkan Anda menutup handle sambil percaya telah menunggu semua padahal sebenarnya tidak menunggu siapa pun. Jika yang Anda butuhkan hanyalah menunggu semua selesai, loop satu-per-satu tanpa batas atas adalah pilihan yang aman.
flowchart TB
OWNER["Pihak yang menghentikan"] -->|"SetEvent(hStopEvent)"| SE["Peristiwa berhenti<br/>reset-manual - terlihat semua sekaligus"]
SE --> W1["Pekerja 1 - menunggu berhenti dan kerja<br/>bersamaan lewat WaitForMultipleObjects"]
SE --> W2["Pekerja 2 - menunggu berhenti dan kerja<br/>bersamaan lewat WaitForMultipleObjects"]
W1 --> C1["Membersihkan dan return sendiri"]
W2 --> C2["Membersihkan dan return sendiri"]
C1 --> J["Pihak yang menghentikan menunggu handle thread dan join<br/>baru sekarang bisa disebut berhenti"]
C2 --> J
Gambar 4: Pola peristiwa berhenti. Memakai peristiwa reset-manual untuk sinyal berhenti berarti satu SetEvent membangunkan setiap pekerja yang menunggu sekaligus. Setiap thread memutuskan sendiri bagaimana ia selesai, dan berhenti dianggap lengkap hanya setelah join selesai
Ada tiga poin kunci. Jadikan peristiwa berhenti reset-manual (sehingga satu SetEvent terlihat oleh setiap pekerja); taruh peristiwa berhenti di depan dalam array tunggu (sehingga jika keduanya di-signal sekaligus, berhenti mendapat prioritas); dan pihak yang menghentikan harus selalu join handle thread sebelum menutupnya.
Dua peringatan tentang lingkup. Pertama, pola “peristiwa plus kosongkan-semua” ini untuk konfigurasi pekerja tunggal. Berapa pun kali Anda memanggil SetEvent pada peristiwa reset-otomatis, ia hanya dapat menyatakan “ada satu keadaan signaled” (sinyal berurutan menyatu), jadi dengan beberapa pekerja hanya satu yang bangun dan akhirnya mengerjakan seluruh burst secara serial. Jika beberapa pekerja berbagi antrean, ganti sinyal kerja menjadi semafor, menaikkan hitungan dengan ReleaseSemaphore(hSem, 1, NULL) setiap kali item diantrekan. Tunggu semafor yang berhasil mengonsumsi satu hitungan, memberi korespondensi yang benar: tepat sebanyak pekerja yang menunggu bangun, satu per satu, sebanyak item yang diantrekan (pemakaian ini tepat di dalam wilayah semafor, sama dengan “membatasi akses bersamaan ke kumpulan sumber daya” di tabel Gambar 3). Tetapi ketika Anda beralih ke semafor, ubah juga sisi konsumen sehingga satu tunggu berhasil sama dengan memproses tepat satu item dari antrean. Biarkan loop kosongkan-semua dari sampel di atas apa adanya, dan satu tunggu — yang hanya mengonsumsi satu izin — akan mengosongkan seluruh antrean, membuat pembukuan kacau: pekerja lain bangun ke antrean kosong pada izin sisa, dan ReleaseSemaphore produsen mulai gagal karena melebihi hitungan maksimum. Menjaga korespondensi “satu izin sama dengan satu pekerjaan” adalah prasyarat pendekatan semafor. Kedua, alirkan jalur berhenti lewat pemrosesan satu item juga. Jika ProcessNextItem melakukan tunggu pemblokiran panjang di dalamnya, entah teruskan peristiwa berhenti ke sana juga dan tunggu keduanya bersama, atau pasang batas waktu berhingga. Memeriksa hanya di antara item menyisakan lubang di mana “shutdown menunggu selamanya karena satu item tidak pernah selesai”. Ini mengatakan tepat hal yang sama dengan StopAsync di edisi .NET dan jthread plus join di edisi C++.
Thread yang menunggu I/O pemblokiran (pipe, socket, port serial) tidak dapat kembali memeriksa peristiwa, jadi sisi I/O butuh desain sendiri — entah I/O OVERLAPPED digabung dengan peristiwa yang Anda tunggu bersama, atau membangunkan I/O dengan CancelIoEx (untuk contoh komunikasi serial yang konkret, lihat “Serial Communication App Pitfalls - Through Reconnection and Log Design”).
7. DllMain dan loader lock — ladang ranjau bagi penulis DLL
Komponen bersama yang ditulis dalam C sering berakhir sebagai DLL, dan DLL punya batasan sendiri: loader lock. Pemuat OS memanggil DllMain sambil memegang loader lock, jadi melakukan salah satu dari berikut di dalamnya menjadi sumber deadlock atau crash.10
- Menyinkronkan dengan thread lain (memperoleh kunci, menunggu thread selesai)
- Memanggil
LoadLibrary/FreeLibrary, langsung atau tidak langsung - Membuat thread (berbahaya jika melibatkan sinkronisasi), atau memanggil
ExitThread
“Menunggu di dalam DllMain agar thread pekerja selesai ketika DLL dilepas” terlihat masuk akal tetapi adalah deadlock klasik: thread yang sedang berakhir mencoba mengambil loader lock untuk mengantar DLL_THREAD_DETACH, dan kedua sisi berakhir menunggu satu sama lain. DLL yang punya thread sendiri harus mengekspos fungsi inisialisasi dan shutdown eksplisit — semacam MyLib_Init / MyLib_Shutdown — dan melakukan start serta join thread di sana. DllMain yang ideal mendekati stub kosong.10
8. Opsi thread C11 — Di mana kita berdiri
Jika Anda ingin menulis C portabel yang tidak bergantung pada Win32, opsinya adalah <threads.h> C11 (thrd_create / mtx_lock / cnd_wait) dan <stdatomic.h>. Menurut tabel kesesuaian resmi, dukungan MSVC berdiri sebagai berikut: <threads.h> didukung dari Visual Studio 2022 17.8 (memerlukan /std:c11 dan Windows SDK yang cocok), sementara <stdatomic.h> masih experimental, pada tahap memerlukan opsi /experimental:c11atomics.11
Jika berbagi kode dengan Linux adalah persyaratan, thread C11 (atau pembungkus pthread) punya nilai nyata, tetapi untuk basis kode hanya-Windows, pendekatan Win32 yang dibahas artikel ini unggul dalam volume informasi yang tersedia, rekam jejak, dan kemudahan debug. Mana pun yang Anda pilih, prinsip desain yang dibahas sejauh ini — mengurangi pembagian, korespondensi antara kunci dan data, dan shutdown kooperatif — tidak berubah.
9. Verifikasi dan debug — Bersiap untuk “tidak tereproduksi”
Anda tidak dapat mengharapkan pengujian menangkap bug race. Uji biasa menghitung jalan di mana race “kebetulan tidak terpicu” sebagai lulus. Pikirkan pertahanan dalam tiga lapisan.
Garis pertahanan pertama adalah desain. Dalam tinjauan, konfirmasikan dengan tabel: data berubah mana yang dibagi, kunci mana yang melindungi setiap potongan (korespondensi dari Bagian 5.1), apakah urutan perolehan kunci tidak ambigu, dan apakah peristiwa berhenti mencapai setiap pekerja. Desain yang tidak dapat mengisi tabel ini belum selesai, meski saat ini bekerja.
Kedua, buat anomali dapat diamati. Alih-alih menunggu tanpa syarat dengan INFINITE, pasang batas waktu di titik kunci dan catat batas waktu ketika ia terpicu, mengubah macet yang sebaliknya berlangsung selamanya menjadi kegagalan yang dapat dideteksi. Ketika macet terjadi di lapangan, tangkap dump, periksa tumpukan setiap thread, dan cari siklus siapa menunggu kunci siapa. Memeriksa dengan Application Verifier direkomendasikan secara resmi untuk kesalahan seputar DLL.10 Membangun dump dan pencatatan dibahas di “Designing Windows Apps to Leave Logs and Dumps When They Crash”.
Ketiga, goyang dengan beban. Uji stres — berjalan dengan lebih banyak thread daripada inti yang Anda punya, mengacak urutan pemrosesan, menyisipkan tunda buatan — adalah cara praktis membuat lebih mungkin Anda mengenai “jackpot” race di mesin pengembangan. Jangan lupa juga menguji reproduksi pada build rilis yang dioptimalkan di bawah beban berat.
10. Ringkasan — daftar periksa edisi C
- Apakah setiap thread dibuat dengan
_beginthreadex(tanpaCreateThread/_beginthreadtercampur)? - Apakah Anda join handle thread (
WaitForSingleObject) sebelum memanggilCloseHandle? - Apakah Anda memproduksi massal thread sendiri untuk pekerjaan berumur pendek (bisakah mereka diserahkan ke API kumpulan thread)?
- Apakah pengecualian internal proses memakai kunci SRW / CRITICAL_SECTION (bukan menyalahgunakan Mutex)?
- Apakah penghitung dan flag bersama diperbarui dengan fungsi Interlocked alih-alih mengandalkan
volatile? - Apakah polling
Sleepmasih tersisa (sudahkah diganti dengan variabel kondisi atau tunggu peristiwa)? - Apakah
TerminateThread(membunuh paksa thread lain) absen di mana-mana? Apakah pekerja berakhir lewatreturndari fungsi thread alih-alih memanggilExitThread(sehingga pembersihan CRT berjalan dengan benar lewat_endthreadex)? - Apakah setiap pekerja punya jalur berhenti lewat peristiwa berhenti plus
WaitForMultipleObjects, dan dapatkah Anda juga membangunkan thread yang terblokir di I/O? - Apakah pelepasan kunci dan handle dijamin di setiap jalur kembali (disiplin
goto cleanup)? - Apakah
DllMainmenghindari membuat thread, menyinkronkan, atau menunggu thread selesai?
Sebagai imbalan tidak mendapat bantuan dari bahasa, mutu multithreading di C tepat apa yang pilihan API dan disiplin Anda jadikan. Jadikan _beginthreadex, kunci SRW, fungsi Interlocked, dan peristiwa berhenti sebagai set bawaan empat potong, sekalian di C Anda dapat merancang menjauh dari “sesekali macet”.
Artikel terkait
- Praktik terbaik multithreading praktis: edisi .NET — apa yang diputuskan sebelum Anda menambah lebih banyak thread
- Praktik terbaik multithreading praktis: edisi C++ — menghilangkan kecelakaan lewat struktur dengan RAII dan jthread
- Praktik terbaik multithreading praktis: edisi Java — konvensi era virtual thread
- Shared Memory Pitfalls and Practical Best Practices
- Why You Should Prefer Event Waits over Sleep(1) on Windows
- Serial Communication App Pitfalls - Through Reconnection and Log Design
- Designing Windows Apps to Leave Logs and Dumps When They Crash
Area konsultasi terkait
KomuraSoft LLC menangani tinjauan desain multithreading untuk proses residen, aplikasi kendali peralatan, dan DLL yang ditulis dalam C; investigasi akar masalah (analisis dump) hang dan crash yang disebabkan TerminateThread atau kunci yang bocor; dan konsultasi teknis tentang menambahkan thread ke kode C warisan.
- Konsultasi teknis dan tinjauan desain
- Investigasi bug dan akar masalah
- Pengembangan aplikasi Windows
- Hubungi kami
Tautan referensi
-
Microsoft Learn, CreateThread function. Tentang thread di dalam executable yang memanggil CRT perlu dikelola dengan _beginthreadex / _endthreadex alih-alih CreateThread / ExitThread, dan tentang CRT yang dapat mengakhiri proses dalam kondisi memori rendah ketika thread yang dibuat dengan CreateThread memanggil CRT. ↩ ↩2
-
Microsoft Learn, Multithreading with C and Win32. Tentang program yang memanggil pustaka CRT perlu memulai thread dengan _beginthread / _beginthreadex alih-alih CreateThread / ExitThread Win32; tentang keluarga _beginthread yang menginisialisasi variabel per-thread CRT; dan tentang SuspendThread yang dapat menghentikan thread saat ia mengakses struktur data internal CRT, yang dapat berujung deadlock. ↩ ↩2
-
Microsoft Learn, About Synchronization. Tentang panduan memilih primitif sinkronisasi Win32: kunci SRW sebagai bawaan untuk kode baru, seukuran pointer dan biasanya tetap di mode pengguna; CRITICAL_SECTION untuk kasus yang butuh perolehan rekursif; Mutex selalu objek kernel, dipakai untuk sinkronisasi lintas proses bernama dan digabung dengan WaitForMultipleObjects; memakai Mutex untuk sinkronisasi internal proses adalah “kesalahan umum” yang jauh lebih lambat di bawah operasi sering; dan semafor dipakai untuk membatasi akses bersamaan ke kumpulan sumber daya, peristiwa untuk pemberitahuan. ↩ ↩2 ↩3
-
Microsoft Learn, Interlocked Variable Access. Tentang fungsi Interlocked yang menyinkronkan akses ke variabel yang dibagi lintas beberapa thread dan menjalankan operasi secara tak terpisahkan; tentang InterlockedIncrement / Decrement yang menggabungkan baca, tambah, dan tulis-kembali menjadi satu operasi atomik, karena tanpa sinkronisasi increment bersamaan dari dua thread dapat kehilangan salah satu increment; tentang keluarga fungsi InterlockedExchange / InterlockedCompareExchange; tentang dapat dipakai antar thread di proses berbeda ketika variabel ada di memori bersama; dan tentang sebagian besar fungsi Interlocked yang menyediakan penghalang memori penuh, dengan varian Acquire / Release tersedia untuk memilih semantik urutan. ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, Using Condition Variables. Tentang contoh yang dikerjakan dari antrean produsen/konsumen yang diimplementasikan di atas buffer sirkular berbatas yang dilindungi CRITICAL_SECTION; tentang struktur di mana InitializeConditionVariable membuat variabel kondisi, konsumen menunggu dengan SleepConditionVariableCS, dan produsen membangunkannya dengan WakeConditionVariable; dan tentang variabel kondisi didukung dari Windows Vista ke atas. ↩ ↩2 ↩3
-
Microsoft Learn, TerminateThread function. Tentang TerminateThread yang mengakhiri thread sasaran tanpa membiarkannya menjalankan kode user-mode; tentang critical section sasaran tidak dilepas jika ia memegangnya; tentang kunci heap tidak dilepas jika thread sedang mengalokasikan memori dari heap; tentang keadaan kernel32 atau keadaan global suatu DLL yang berpotensi rusak; dan tentang itu “fungsi berbahaya yang hanya boleh dipakai dalam kasus paling ekstrem”, tidak boleh dipanggil kecuali Anda sepenuhnya tahu dan mengendalikan setiap jalur kode yang mungkin sedang dijalankan thread sasaran. ↩ ↩2
-
Microsoft Learn, Warning C6258. Tentang peringatan analisis kode C6258 yang mendeteksi pemakaian TerminateThread; tentang TerminateThread yang tidak dapat melakukan pembersihan thread yang semestinya; dan tentang prosedur pengakhiran yang benar ditunjukkan sebagai membuat peristiwa dengan CreateEvent, membiarkan setiap thread memantau keadaan peristiwa dengan WaitForSingleObject, dan membiarkan thread mengakhiri eksekusinya sendiri begitu peristiwa menjadi signaled. ↩ ↩2 ↩3
-
Microsoft Learn, CreateThreadpoolWork function. Tentang membuat objek work dengan CreateThreadpoolWork dan membiarkan thread pekerja kumpulan menjalankan callback setiap kali SubmitThreadpoolWork dipanggil; tentang dapat menentukan lingkungan eksekusi lewat lingkungan callback (TP_CALLBACK_ENVIRON); dan tentang ketersediaan dari Windows Vista ke atas. ↩ ↩2
-
Microsoft Learn, Thread Pools. Tentang kumpulan thread yang cocok untuk aplikasi yang menjalankan banyak pekerjaan asinkron pendek, atau yang sering membuat thread berumur pendek; tentang komponen API kumpulan thread baru yang didesain ulang di Vista; tentang praktik terbaik tidak pernah mengakhiri thread kumpulan dengan TerminateThread atau memanggil ExitThread dari dalam callback, membersihkan setiap keadaan yang dibuat di callback sebelum kembali, dan menjaga handle tunggu tetap hidup sampai kumpulan selesai memakainya. ↩ ↩2
-
Microsoft Learn, Dynamic-Link Library Best Practices. Tentang DllMain dipanggil saat loader lock dipegang, yang menempatkan batasan serius pada API mana yang aman dipanggil; tentang menyinkronkan dengan thread lain di dalam DllMain yang berujung deadlock; tentang memanggil LoadLibrary ada di daftar tindakan terlarang; tentang pola di mana menunggu thread selesai di dalam DllMain saat pelepasan DLL deadlock terhadap upaya thread itu sendiri memperoleh loader lock untuk mengantar DLL_THREAD_DETACH; tentang DllMain yang ideal mendekati stub kosong, dengan inisialisasi ditunda sebanyak mungkin; dan tentang mendefinisikan hierarki kunci dengan loader lock di puncak. ↩ ↩2 ↩3 ↩4 ↩5
-
Microsoft Learn, Microsoft C/C++ language conformance by Visual Studio version. Tentang tabel kesesuaian pustaka standar C, yang menunjukkan thread C11 (threads.h) didukung dari Visual Studio 2022 17.8; stdatomic.h diperlakukan sebagai experimental (di balik opsi /experimental:c11atomics); dan dukungan kompiler C11 / C17 memerlukan Visual Studio 2019 16.8 atau lebih baru bersama Windows SDK yang cocok. ↩ ↩2
-
Microsoft Learn, Interlocked Variable Access. Tentang baca atau tulis sederhana pada variabel 32-bit yang disejajarkan dengan benar bersifat atomik, tetapi sinkronisasi (urutan) akses tidak dijamin; tentang baca atau tulis sederhana pada variabel 64-bit bersifat atomik di Windows 64-bit tetapi tidak dijamin di Windows 32-bit; dan tentang variabel ukuran lain tidak dijamin atomik di platform mana pun. ↩ ↩2
-
Microsoft Learn, _beginthread, _beginthreadex. Tentang mengapa _beginthreadex lebih aman daripada _beginthread: thread yang dibuat dengan _beginthread dapat meninggalkan handle yang dikembalikan tidak valid (atau menunjuk ke thread lain) jika ia selesai lebih dulu; handle dari _beginthreadex harus ditutup pemanggil dengan CloseHandle dan keabsahannya dijamin; _beginthreadex memungkinkan Anda meneruskan handle ke API sinkronisasi; fungsi thread mengembalikan kode keluar thread di bawah konvensi pemanggilan __stdcall; dan taut ke CRT multithread diperlukan. ↩
Artikel terkait
Artikel terbaru dengan tag yang sama untuk mendalami topik-topik terdekat.
Praktik terbaik multithreading di lapangan: edisi C++ — menghapus kecelakaan secara struktural dengan RAII dan jthread
Di C++, multithreading adalah dunia di mana data race menjadi perilaku tak terdefinisi. Artikel ini menelusuri jebakan destruktor std::th...
Praktik terbaik multithreading di lapangan: edisi Java — konvensi era virtual thread
Di Java, praktik yang mapan untuk multithreading adalah tidak pernah membuat thread secara langsung, melainkan bertumpu pada ExecutorServ...
Win32 Thread Pool API — konkurensi tanpa membuat thread, lewat CreateThreadpoolWork
Apakah Anda menebar panggilan CreateThread di seluruh kode native? Artikel ini menjelaskan Win32 thread pool API yang didesain ulang di V...
Aplikasi yang rusak saat bangun dari tidur — cara kerja event daya Windows dan cara membangun aplikasi bisnis yang bertahan
Anda membuka laptop dan koneksi aplikasi bisnis sudah mati — penyebabnya adalah desain yang tidak pernah memperhitungkan tidur. Artikel i...
DllMain dan loader lock — alasan sebenarnya Anda diminta "jangan lakukan apa pun di inisialisasi DLL"
Mengapa Anda tidak boleh memanggil LoadLibrary atau menyinkronkan dengan thread lain dari DllMain. Berdasarkan sumber primer, artikel ini...
Topik terkait
Halaman-halaman ini menempatkan topik dalam konteks layanan dan keputusan yang lebih luas.
Topik teknis Windows
Portal tentang pengembangan Windows, investigasi bug, dan pemanfaatan aset yang ada.
Layanan yang terkait dengan topik ini
Artikel ini berkaitan langsung dengan layanan berikut.
Pengembangan aplikasi Windows
Aplikasi bisnis, integrasi perangkat, dan alat komunikasi, dari kebutuhan hingga pengembangan.
Pertanyaan yang sering diajukan
Pertanyaan yang sering muncul dalam konsultasi tentang topik artikel ini.
- Haruskah saya memakai CreateThread atau _beginthreadex?
- Pakai _beginthreadex untuk setiap thread yang memanggil fungsi di pustaka runtime C (CRT). _beginthreadex menginisialisasi data internal per-thread yang dibutuhkan CRT sebelum memulai thread. Dokumentasi resmi menyatakan dengan gamblang bahwa jika thread yang dibuat dengan CreateThread memanggil fungsi CRT, CRT dapat mengakhiri proses saat memori menipis. Dalam praktik, thread di aplikasi C hampir selalu memanggil fungsi CRT di suatu tempat (printf, malloc, strtok, dan seterusnya), jadi tidak ada ruginya mengingat aturan sebagai "selalu _beginthreadex". Hindari juga _beginthread (tanpa ex) — ia punya jebakan di mana handle yang dikembalikan bisa menjadi tidak valid jika thread yang dibuat selesai lebih dulu, jadi _beginthreadex, yang handlenya dapat diteruskan ke API sinkronisasi, adalah yang harus dipilih.
- Saya tidak boleh menghentikan thread dengan TerminateThread?
- Tidak, Anda tidak seharusnya. TerminateThread menghapus thread sasaran tanpa membiarkannya menjalankan kode user-mode sama sekali, jadi jika thread itu sedang memegang critical section ia tidak pernah dilepas, jika ia sedang mengalokasikan memori dari heap maka kunci heap tetap dipegang, dan jika ia sedang memanipulasi keadaan global suatu DLL maka keadaan itu rusak. Dokumentasi resmi menyatakan secara eksplisit bahwa itu "fungsi berbahaya yang hanya boleh dipakai dalam kasus paling ekstrem", dan analisis kode juga menandainya sebagai peringatan C6258. Cara berhenti yang benar adalah shutdown kooperatif: buat peristiwa berhenti, biarkan setiap thread mengawasi peristiwa itu dengan WaitForSingleObject / WaitForMultipleObjects, dan biarkan setiap thread membersihkan dirinya sendiri lalu berakhir sendiri.
- Saya memakai Mutex untuk pengecualian di dalam proses. Apa yang salah?
- Itu bekerja, tetapi Anda membayar banyak kinerja. Mutex Win32 selalu objek kernel, jadi setiap perolehan dan pelepasan memicu transisi ke mode kernel. Untuk pengecualian di dalam satu proses, kunci SRW atau CRITICAL_SECTION — yang tetap di mode pengguna dan hanya jatuh ke tunggu kernel saat ada persaingan — jauh lebih cepat, dan dokumentasi resmi secara eksplisit menyebut memakai Mutex untuk sinkronisasi internal proses sebagai "kesalahan umum". Mutex pantas dipakai ketika Anda butuh pengecualian lintas proses sebagai objek bernama, atau ketika Anda ingin menunggunya bersama objek kernel lain dengan WaitForMultipleObjects.
- Bisakah saya memakai threads.h dan stdatomic.h dari C11 di Windows?
- Di MSVC, thread C11 (threads.h) didukung sejak Visual Studio 2022 17.8 (memerlukan /std:c11 dan Windows SDK yang cocok). stdatomic.h, di sisi lain, masih diperlakukan sebagai experimental, memerlukan opsi /experimental:c11atomics (menurut tabel kesesuaian resmi per Agustus 2026). Itu opsi yang layak jika portabilitas jelas prioritas utama, tetapi untuk basis kode hanya-Windows, menulis ke Win32 API (_beginthreadex, kunci SRW, variabel kondisi, fungsi Interlocked) adalah pilihan realistis mengingat rekam jejak dan volume informasi yang tersedia.
- Apakah menambahkan volatile membuat flag bersama aman?
- Tidak. volatile di C hanya menekan optimisasi kompiler seperti menyimpan nilai di register — ia tidak menjamin atomisitas operasi maupun urutan memori antar prosesor. Baca atau tulis sederhana pada variabel 32-bit yang disejajarkan dengan benar memang atomik di Windows, tetapi "baca, tambah, dan tulis kembali" terpecah menjadi langkah terpisah, dan tidak ada jaminan tentang urutannya relatif terhadap operasi memori di sekitarnya. Pakai keluarga fungsi Interlocked untuk memperbarui penghitung atau flag bersama. Sebagian besar fungsi Interlocked membawa penghalang memori penuh, jadi Anda mendapat jaminan urutan sekaligus. Ketika Anda perlu melindungi beberapa variabel bersama-sama, pakai kunci SRW atau CRITICAL_SECTION.
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.