Handle yang ringkas sering direpresentasikan sebagai integer karena murah untuk disimpan, disalin, dibandingkan, dan diteruskan melewati batas API. Pada resource manager berbasis tabel, integer tersebut dapat berupa index ke array slot. Representasi ini bermasalah saat sebuah slot dilepas lalu dipakai kembali. Handle lama dapat menunjuk resource baru yang kebetulan menempati index yang sama.
Kegagalannya bukan akses di luar batas array. Index tersebut bisa sepenuhnya valid. Masalahnya ada pada identitas: handle menyebut lokasi penyimpanan, sementara caller memperlakukannya sebagai identitas resource yang sebelumnya menempati lokasi itu.
Generation counter menambahkan komponen identitas kedua. Slot tetap menyediakan addressing yang cepat, sedangkan generation mencatat okupansi slot yang menjadi tujuan handle.
Reuse slot menghasilkan handle usang yang tampak valid
Bayangkan tabel resource dengan 1.024 slot. Sebuah handle berisi index slot 37, dan slot 37 saat ini menyimpan connection A. Ketika A ditutup, allocator mengembalikan slot itu ke free list. Alokasi berikutnya kemudian menempatkan connection B pada slot 37.
Jika handle lama hanya berupa integer 37, operasi yang terlambat untuk A kini akan mencapai B. Pemeriksaan range lolos. Slot sedang terisi. Pemeriksaan non-null juga lolos. Tidak ada informasi pada index yang membedakan occupant sebelumnya dari occupant saat ini.
Pola ini muncul pada object pool, entity table, descriptor table, job registry, timer table, dan sistem lain yang mendaur ulang identifier dalam jumlah terbatas. API di sekelilingnya dapat berbeda, tetapi kesalahan identitasnya sama ketika nilai yang menyerupai alamat hidup lebih lama daripada instance resource yang semula dirujuk.
Menunda reuse dapat menurunkan peluang collision, tetapi tidak membentuk kontrak identitas. Handle yang cukup lama tetap dapat bertahan melewati masa penundaan tersebut.
Handle dapat membawa index dan generation
Tabel generasional menyimpan counter di samping setiap slot. Alokasi mengembalikan index slot beserta nilai counter saat itu. Akses hanya berhasil ketika generation pada handle sama dengan generation yang tersimpan di slot.
Representasi sederhananya:
Handle {
index: u32
generation: u32
}
Slot {
generation: u32
value: Resource?
}Lookup kemudian memiliki dua pemeriksaan terpisah:
slot = slots[handle.index]
if slot.value is empty:
reject stale handle
if slot.generation != handle.generation:
reject stale handle
return slot.valueKetika resource dilepas, tabel membatalkan okupansi tersebut. Salah satu desain yang umum adalah menaikkan generation sebelum slot tersedia untuk alokasi berikutnya. Handle baru untuk index yang sama dengan demikian membawa generation yang berbeda.
Index menentukan lokasi pencarian. Generation menentukan apakah resource yang ditemukan merupakan okupansi yang sama dengan yang disebut oleh handle.
Perubahan counter berada pada batas reuse
Update generation memerlukan posisi yang tegas dalam lifecycle resource. Jika counter baru dinaikkan setelah caller baru dapat melihat slot hasil reuse, terdapat celah saat handle lama masih dapat cocok. Transisi state perlu membuat okupansi sebelumnya tidak valid sebelum slot dipublikasikan sebagai okupansi baru.
Pada tabel yang dilindungi mutex, transisi dapat diserialisasi dalam critical section yang sama:
release(handle):
lock table
slot = slots[handle.index]
require slot.generation == handle.generation
destroy slot.value
slot.value = empty
slot.generation += 1
free_list.push(handle.index)
unlock tableUrutan detail dapat berbeda sesuai implementasi, tetapi publication tetap penting. Caller tidak boleh menerima handle baru sebelum state slot dan generation yang terkait dengannya terlihat sesuai kontrak sinkronisasi yang dipakai oleh lookup.
Field generation tidak menyediakan sinkronisasi tersebut dengan sendirinya. Fungsinya mendeteksi ketidakcocokan identitas. Lock, atomic, aturan ownership, atau mekanisme concurrency lain tetap mengatur race pada tabel dan resource di dalamnya.
Packing tidak mengubah aturan identitas
Sebagian API mengemas kedua field ke dalam satu machine word. Handle 64-bit, misalnya, dapat memakai 32 bit rendah untuk index dan 32 bit tinggi untuk generation:
raw = (u64(generation) << 32) | u64(index)Bentuk ini menjaga biaya penyalinan handle tetap kecil sambil mempertahankan kedua komponen identitas. Equality comparison pada nilai yang sudah dikemas juga dapat dipakai selama layout bit stabil di dalam API tersebut.
Packing hanya keputusan encoding, bukan mekanisme keselamatan. Proteksi berasal dari perbandingan generation pada handle dengan state generation otoritatif saat lookup. Nilai packed yang di-decode tetapi tidak diperiksa tetap memiliki masalah stale reference yang sama dengan index biasa.
Pembagian bit juga menetapkan batas. Bit index yang lebih sedikit membatasi jumlah slot yang dapat dialamatkan; bit generation yang lebih sedikit mempercepat wraparound. Batas tersebut sebaiknya mengikuti populasi resource dan laju reuse, bukan sekadar pembagian bit yang tampak rapi.
Wraparound menetapkan horizon terbatas untuk handle usang
Generation counter dengan lebar tetap pada akhirnya mengalami wraparound. Dengan counter unsigned selebar n bit, sebuah slot kembali ke nilai generation yang sama setelah 2^n perubahan generation pada slot tersebut.
Pada titik itu, handle yang sangat lama dapat menjadi identik secara numerik dengan handle aktif untuk index yang sama. Karena itu, generation counter dengan representasi terbatas tidak menghasilkan keunikan permanen secara matematis.
Kontrak praktis bergantung pada lebar counter, laju reuse maksimum, dan durasi terpanjang handle usang masih dapat dijangkau. Generation 32-bit memiliki siklus jauh lebih panjang daripada 8-bit, tetapi lebar saja bukan bukti kecukupan. Sistem dengan churn sangat tinggi atau handle yang disimpan secara persisten perlu memperhitungkan model lifetime secara penuh.
Jika handle usang dapat bertahan tanpa batas waktu, counter yang lebih lebar hanya memindahkan batas. Desain yang membutuhkan identitas durable biasanya memerlukan identifier dengan kebijakan reuse yang sesuai kebutuhan tersebut, bukan memperlakukan handle generasional kecil sebagai object ID permanen.
Validasi harus terjadi pada setiap batas dereference
Pemeriksaan generation efektif hanya di titik saat handle berubah menjadi akses terhadap resource. Melakukan pemeriksaan sekali lalu mempertahankan pointer tanpa validasi dapat memindahkan jendela stale reference melewati titik pemeriksaan.
Sebagai contoh, lookup dapat memvalidasi handle di bawah table lock lalu mengembalikan raw pointer. Jika thread lain dapat melepas dan mendaur ulang slot segera setelah lock dilepas, lifetime pointer kini bergantung pada aturan ownership yang terpisah. Pemeriksaan generation menetapkan identitas pada saat lookup; pemeriksaan itu tidak memperpanjang lifetime resource.
Perbedaan ini memisahkan dua kontrak:
- validasi identitas menolak handle ketika slot sudah berpindah ke generation lain;
- sinkronisasi lifetime mencegah resource yang sudah tervalidasi menghilang saat operasi masih menggunakannya.
Reference counting, scoped borrow, hazard-pointer scheme, epoch-based reclamation, lock, atau explicit ownership transfer dapat menyediakan aturan lifetime, tergantung sistemnya. Generation counter dapat digunakan bersama mekanisme tersebut, tetapi bukan penggantinya.
Generational handle membuat reuse dapat diamati
Daur ulang slot hanya merupakan detail implementasi selama caller tidak dapat mencampuradukkan satu okupansi dengan okupansi lain. Begitu caller dapat menyimpan identifier melewati release dan reuse, kebijakan reuse menjadi bagian dari semantik identitas API.
Pasangan index dan generation membuat transisi itu dapat diamati secara terkendali. Tabel tetap dapat memakai storage padat dan alokasi murah sambil menolak reference ke occupant sebelumnya. Biayanya berupa handle yang lebih besar, state generation per slot, satu perbandingan saat akses, serta kebijakan wraparound yang eksplisit.
Trade-off tersebut paling relevan ketika resource memang sengaja didaur ulang dan stale reference mungkin terjadi. Generation tidak membuat resource hidup selamanya dan tidak menserialisasi akses konkuren. Generation memberi batas akses informasi yang cukup untuk membedakan resource saat ini dari occupant sebelumnya pada slot yang sama.