Sampai modul ini, setiap program berjalan sebagai satu alur instruksi. Thread sistem operasi membuka alur tambahan di dalam proses yang sama, membagi memori yang sama, dan dijadwalkan oleh sistem operasi secara bebas. Zig membuka kemampuan ini melalui std.Thread tanpa runtime khusus, sehingga thread yang dibuat adalah thread sistem operasi sungguhan. Topik ini membahas pembuatan thread, pengiriman argumennya, dan penggabungan kembali hasilnya.
Konsep#
std.Thread.spawn meminta dua masukan utama, yaitu opsi dan tuple argumen, lalu mengembalikan penangan thread yang baru dibuat. Fungsi yang diberikan mulai berjalan seketika pada alur sendiri, dan pemanggilnya melanjutkan eksekusi tanpa menunggu. Penangan tersebut wajib ditutup dengan join, yaitu menunggu thread menuntaskan pekerjaannya sebelum sumber daya thread dibersihkan. Pasangan spawn dan join adalah kontrak dasarnya, dan program yang membuat thread tanpa pernah melakukan join meninggalkan sumber daya yang tidak terkelola.
Pembuat thread juga dapat menyetel SpawnConfig untuk menyesuaikan ukuran tumpukan setiap thread, yang bawaannya berukuran enam belas megabita dan mencukupi sebagian besar kebutuhan. Alternatif penutupan penangan adalah detach, yang melepaskan penangan dan membiarkan thread berjalan sendiri tanpa ada yang menunggunya. Mode lepas ini cocok untuk pekerjaan latar yang hasilnya tidak dipedulikan, tetapi program harus menjamin threadnya selesai sebelum proses berakhir, karena tidak ada mekanisme pemeriksaan yang tersisa setelah penangan dilepas.
Argumen dikirim sebagai tuple pada parameter ketiga, dan setiap nilai disalin pada saat spawn. Untuk mengirim hasil kembali, program membagi alamat memori secara eksplisit, misalnya dengan mengirim pointer ke elemen array yang disiapkan pemanggil. Pola ini selaras dengan keeksplisitan Zig, karena aliran data antar thread terbaca dari tanda tangan fungsi pekerjanya.
Hasil gabungan dihitung setelah seluruh join selesai, sehingga keluaran program tidak bergantung pada urutan penjadwalan. Diagram berikut merangkum alurnya.
Banyaknya thread yang dibuat adalah keputusan desain, bukan angka bawaan. Thread lebih banyak daripada inti prosesor hanya menambah biaya peralihan konteks tanpa mempercepat pekerjaan murni komputasi, sehingga program seperti contoh menyetel banyaknya thread mengikuti banyaknya inti yang dipakainya. Keputusan yang sama berbeda untuk pekerjaan yang banyak menunggu, misalnya baca-tulis berkas, karena thread yang menunggu tidak membebani inti prosesor dan jumlahnya boleh melebihi banyaknya inti.
Contoh Lengkap#
Program berikut membagi delapan angka menjadi empat bagian, menjalankan satu thread penghitung per bagian, dan menjumlahkan hasilnya setelah seluruh thread digabung.
const std = @import("std");
fn jumlah_kuadrat(data: []const u64, tujuan: *u64) void {
var total: u64 = 0;
for (data) |n| {
total += n * n;
}
tujuan.* = total;
}
pub fn main() !void {
const data = [_]u64{ 1, 2, 3, 4, 5, 6, 7, 8 };
var hasil: [4]u64 = undefined;
var benang: [4]std.Thread = undefined;
const bagian = data.len / benang.len;
for (0..benang.len) |i| {
benang[i] = try std.Thread.spawn(.{}, jumlah_kuadrat, .{ data[i * bagian .. (i + 1) * bagian], &hasil[i] });
}
for (0..benang.len) |i| {
benang[i].join();
}
var total: u64 = 0;
for (0..benang.len) |i| {
std.debug.print("benang {d}: {d}\n", .{ i, hasil[i] });
total += hasil[i];
}
std.debug.print("total: {d}\n", .{total});
}Setiap pemanggilan spawn mengirim dua argumen berupa slice bagian data dan pointer ke slot hasil, dan keduanya masuk ke tuple .{} pada panggilan spawn. Setelah keempat join selesai, array hasil sudah terisi penuh, sehingga pencetakan dan penjumlahan berjalan atas nilai final.
zig run paralel-kuadrat.zigbenang 0: 5
benang 1: 25
benang 2: 61
benang 3: 113
total: 204Bacaan angkanya dapat diverifikasi per bagian, misalnya bagian pertama memuat 1 dan 2 sehingga kuadratnya menjumlah menjadi 5, sedangkan bagian terakhir memuat 7 dan 8 sehingga hasilnya 113. Keluaran selalu identik pada setiap eksekusi karena tidak ada satu pun pencetakan yang terjadi sebelum join, dan itulah pola yang dijaga sepanjang modul ini.
Spawn yang berhasil belum berarti aman, dan dua pemeriksaan ringkas memastikan keadaannya. Pertama, seluruh data yang dipegang thread diperiksa asal-usulnya, yaitu stack milik pemanggil tetap hidup sampai join kembali, sedangkan data tumpukan dialokasikan dengan allocator yang dipatuhi kedua sisi. Kedua, urutan pencetakan antar thread tidak diandalkan untuk menilai urutan eksekusi, karena penjadwal bebas memilih urutannya, dan satu-satunya titik temu yang pasti adalah kembali dari join.
Kesalahan umum#
Kesalahan kedua berupa mengirim pointer ke variabel lokal pekerja yang sudah keluar dari lingkup. Argumen tuple disalin saat spawn, tetapi pointer yang tersimpan di dalamnya tetap menunjuk alamat lama, sehingga variabel yang berada di dalam badan perulangan pembuat thread menghasilkan penunjuk tak sah. Slot hasil dideklarasikan pada lingkup yang hidup sampai seluruh join selesai, seperti array hasil pada contoh.
Kesalahan ketiga berupa pencetakan dari dalam thread tanpa pengaturan apa pun. Pencetakan yang terjadi bersamaan menghasilkan baris yang saling menimpa dan urutan yang berubah-ubah setiap eksekusi. Selama modul ini, thread hanya menulis ke slot memori yang disediakan, dan seluruh pencetakan dilakukan oleh main setelah join.
Ringkasan#
std.Thread.spawnmembuat thread sistem operasi sungguhan dengan fungsi pekerja dan tuple argumen.joinmenunggu pekerjaan tuntas dan wajib dipanggil untuk setiap thread yang dibuat.- Komunikasi hasil berlangsung melalui memori bersama yang alamatnya dikirim eksplisit.
- Keluaran yang deterministik diperoleh dengan menunda seluruh pencetakan hingga seluruh join selesai.
Topik berikutnya menangani kasus ketika beberapa thread memperbarui satu variabel yang sama, dengan operasi atomik sebagai pondasinya.
Referensi#
- std.Thread — Dokumentasi pustaka standar Zig — Pembuatan thread, pengiriman argumen, dan penggabungannya.