Operasi atomik pada topik sebelumnya mengamankan satu baca-tulis, tetapi keadaan bersama sering memerlukan lebih dari satu langkah. Memotong stok memerlukan membaca sisa, membandingkan, mengurangi, dan mencatat, dan operasi atomik tunggal tidak cukup untuk rangkaian tersebut. Mutex menjawabnya dengan critical section, yaitu rentang kode yang hanya boleh dijalankan satu thread pada satu waktu. Topik ini mempraktikkan mutex Zig pada 0.17.0, yang kini hidup di std.Io.Mutex dan bekerja melalui instance Io.
Konsep#
Pada 0.17.0, mutex pustaka standar adalah std.Io.Mutex, dan metode lock-nya menerima parameter Io. Mutex perlu fasilitas peniduran (futex) ketika ia bertahan lama, dan fasilitas itu disediakan oleh instance Io yang diterima main dari std.process.Init. Implementasi bawaannya thread-safe, sehingga satu nilai io yang sama dapat dikirim ke seluruh thread pekerja dan dipakai untuk menunggu giliran.
Keadaan internal mutex berangkat dari tiga nilai, yaitu tidak terkunci, terkunci sekali, dan sengketa (contended). Pemegang pertama mengubah keadaan dari tidak terkunci ke terkunci sekali melalui satu pertukaran atomik tanpa kontak dengan sistem operasi, sehingga kasus tanpa persaingan berlangsung murah. Barulah thread kedua yang menemukan mutex terkunci menaikkan keadaannya menjadi sengketa dan menidurkan dirinya melalui futex, sedangkan pemegang yang keluar membuka keadaan itu dan membangunkan satu penunggu. Desain dua tingkat inilah yang membuat lockUncancelable cepat pada kasus umum dan hanya mahal saat benar-benar rebutan.
Polanya berurutan tiga langkah. Mutex dideklarasikan sebagai field pada struct keadaan bersama dengan nilai bawaan .init. Rentang kritis dibuka dengan lockUncancelable(io) dan ditutup dengan unlock(io), dan pembukaannya dipasangkan dengan defer agar penutupan tetap berjalan pada jalur keluar mana pun. Seluruh pembacaan dan penulisan field keadaan hanya terjadi di dalam rentang tersebut.
Diagram berikut memperlihatkan tiga thread yang berebut masuk ke satu critical section, dan satu yang menunggu sampai pemegang sebelumnya keluar.
Mutex tidak berdiri sendiri di std.Io. Primitif saudaranya menutup kebutuhan yang berbeda, yaitu std.Io.Condition untuk menunggu sampai suatu keadaan terpenuhi, std.Io.Semaphore untuk membatasi banyaknya pemegang izin, dan std.Io.RwLock untuk membedakan pembaca bersama dari penulis tunggal. Seluruhnya memakai instance Io yang sama dan berbagi kaidah penggunaan dengan mutex, sehingga pola yang dipelajari pada topik ini menjadi fondasi bagi ketiganya.
Contoh Lengkap#
Program berikut mensimulasikan tiga loket yang menjual karcis dari stok bersama. Setiap loket berhak meminta paling banyak sepuluh karcis dengan pembelian maksimal empat per transaksi, dan seluruh perubahan stok berada di dalam critical section.
const std = @import("std");
const Loket = struct {
mutex: std.Io.Mutex = .init,
sisa: u32 = 30,
terjual: u32 = 0,
};
fn jual(io: std.Io, loket: *Loket, kuota: u32) void {
var permintaan = kuota;
while (permintaan > 0) {
loket.mutex.lockUncancelable(io);
defer loket.mutex.unlock(io);
if (loket.sisa == 0) {
return;
}
const ambil = @min(permintaan, 4);
loket.sisa -= ambil;
loket.terjual += ambil;
permintaan -= ambil;
}
}
pub fn main(init: std.process.Init) !void {
const io = init.io;
var loket = Loket{};
var benang: [3]std.Thread = undefined;
for (0..benang.len) |i| {
benang[i] = try std.Thread.spawn(.{}, jual, .{ io, &loket, 10 });
}
for (0..benang.len) |i| {
benang[i].join();
}
std.debug.print("terjual: {d}\n", .{loket.terjual});
std.debug.print("sisa: {d}\n", .{loket.sisa});
}Ketiga thread membagi satu struct Loket melalui pointer, dan setiap pembacaan sisa serta penulisan sisa dan terjual berada di antara lockUncancelable dan unlock. Pemeriksaan stok habis juga berada di dalam rentang yang sama, karena keputusan berhenti harus diambil atas stok yang tidak berubah di tengah pemeriksaan.
zig run loket-karcis.zigterjual: 30
sisa: 0Total permintaan tiga loket adalah tiga puluh karcis dan stoknya juga tiga puluh, sehingga hasil terjual: 30 dan sisa: 0 muncul pada setiap eksekusi. Konsistensi itu berasal dari critical section, karena dua loket tidak mungkin memotong stok yang sama pada saat bersamaan. Tanpa mutex, kedua field dapat berakhir pada angka yang tidak cocok, misalnya terjual lebih dari stok.
Kesalahan umum#
Kesalahan kedua berupa membuka critical section tanpa defer untuk menutupnya. Jalur return di tengah rentang, termasuk jalur stok habis pada contoh, melompati panggilan unlock yang ditulis manual, sehingga thread lain menunggu tanpa batas. Pola defer unlock menutup rentang pada setiap jalur keluar, dan kesalahan ini tidak terlihat dari hasil satu eksekusi yang berhasil.
Kesalahan ketiga berupa struct keadaan yang dideklarasikan const. Pemanggilan spawn dengan argumen &loket atas variabel const ditolak dengan pesan expected type '*Loket', found '*const Loket', karena thread pekerja menerima pointer yang dapat menulis. Deklarasinya berupa var, sedangkan perlindungan penulisan bersamaan sudah menjadi tugas mutex, bukan tugas const.
Ringkasan#
- Mutex menjaga rentang operasi, bukan sekadar satu baca-tulis, dan menjadi pelengkap operasi atomik.
- Pada 0.17.0 mutexnya adalah
std.Io.Mutexyang memakai instanceIountuk menidurkan thread yang menunggu. - Pasangan
lockUncancelable(io)dandefer unlock(io)membentuk critical section yang aman terhadap jalur keluar mana pun. - Konsistensi antar field keadaan bersama hanya terjamin bila seluruh aksesnya berada di dalam rentang yang sama.
Modul berikutnya memperluas pandangan ke luar kode, yaitu ekosistem package Zig beserta berkas manifest build.zig.zon.
Referensi#
- std.Thread.Mutex — Dokumentasi pustaka standar Zig — Kontrak lock dan unlock pada mutex.