Berikut ini penjelasan dan contoh bagaimana hal ini dilakukan. Beri tahu saya jika ada bagian yang tidak jelas.
Intinya dengan sumber
Universal
Inisialisasi:
Indeks thread diterapkan dengan cara yang bertambah secara atom. Ini dikelola menggunakan AtomicInteger
nama nextIndex
. Indeks-indeks ini ditugaskan untuk utas melalui sebuah ThreadLocal
instance yang menginisialisasi dirinya dengan mendapatkan indeks berikutnya dari nextIndex
dan menambahnya. Ini terjadi saat pertama kali setiap indeks utas diambil pertama kali. A ThreadLocal
dibuat untuk melacak urutan terakhir yang dibuat utas ini. Ini diinisialisasi 0. Referensi objek pabrik berurutan dilewatkan dan disimpan. Dua AtomicReferenceArray
contoh dibuat dari ukuran n
. Objek ekor ditugaskan untuk setiap referensi, yang telah diinisialisasi dengan keadaan awal yang disediakan oleh Sequential
pabrik. n
adalah jumlah maksimum utas yang diizinkan. Setiap elemen dalam array ini 'milik' indeks utas terkait.
Terapkan metode:
Ini adalah metode yang melakukan pekerjaan yang menarik. Ini melakukan hal berikut:
- Buat simpul baru untuk doa ini: milikku
- Setel simpul baru ini di array yang diumumkan di indeks utas saat ini
Kemudian loop pengurutan dimulai. Itu akan berlanjut sampai doa saat ini telah diurutkan:
- temukan sebuah simpul dalam array yang diumumkan menggunakan urutan simpul terakhir yang dibuat oleh utas ini. Lebih lanjut tentang ini nanti.
- jika sebuah node ditemukan pada langkah 2 itu belum diurutkan, lanjutkan dengan itu, jika tidak, hanya fokus pada doa saat ini. Ini hanya akan mencoba membantu satu simpul lainnya per doa.
- Apa pun simpul yang dipilih pada langkah 3, teruslah mencoba untuk mengurutkannya setelah simpul berurutan terakhir (utas lainnya dapat mengganggu.) Terlepas dari keberhasilan, setel referensi kepala utas saat ini ke urutan yang dikembalikan oleh
decideNext()
Kunci untuk loop bersarang yang dijelaskan di atas adalah decideNext()
metode. Untuk memahami itu, kita perlu melihat kelas Node.
Kelas simpul
Kelas ini menentukan node dalam daftar yang ditautkan dua kali lipat. Tidak banyak aksi di kelas ini. Sebagian besar metode adalah metode pengambilan sederhana yang harus cukup jelas.
metode ekor
ini mengembalikan instance simpul khusus dengan urutan 0. Ini hanya bertindak sebagai tempat-pemegang sampai doa menggantikannya.
Properti dan inisialisasi
seq
: nomor urut, diinisialisasi ke -1 (artinya tidak diikuti)
invocation
: nilai doa apply()
. Ditetapkan pada konstruksi.
next
: AtomicReference
untuk tautan penerusan. sekali ditugaskan, ini tidak akan pernah berubah
previous
: AtomicReference
untuk tautan terbelakang yang ditugaskan pada pengurutan dan dihapus olehtruncate()
Putuskan Selanjutnya
Metode ini hanya satu di Node dengan logika non-trivial. Singkatnya, sebuah simpul ditawarkan sebagai kandidat untuk menjadi simpul berikutnya dalam daftar tertaut. The compareAndSet()
Metode akan memeriksa apakah referensi itu adalah null dan jika demikian, mengatur referensi untuk calon. Jika referensi sudah diset, itu tidak melakukan apa-apa. Operasi ini bersifat atomik sehingga jika dua kandidat ditawarkan pada saat yang sama, hanya satu yang akan dipilih. Ini menjamin hanya satu simpul yang akan dipilih sebagai yang berikutnya. Jika kandidat node dipilih, urutannya diatur ke nilai berikutnya, dan tautan sebelumnya diatur ke simpul ini.
Melompat Kembali ke metode berlaku kelas Universal ...
Setelah memanggil decideNext()
simpul sekuens terakhir (saat dicentang) dengan simpul kami atau simpul dari announce
array, ada dua kemungkinan kejadian: 1. Node berhasil diurutkan 2. Beberapa utas lain mencegah utas ini.
Langkah selanjutnya adalah memeriksa apakah simpul dibuat untuk permohonan ini. Ini bisa terjadi karena utas ini berhasil mengurutkannya atau beberapa utas lainnya mengambilnya dari announce
array dan mengurutkannya untuk kami. Jika belum diurutkan, prosesnya diulang. Kalau tidak, panggilan selesai dengan menghapus array yang diumumkan di indeks utas ini dan mengembalikan nilai hasil dari doa. Array announce dihapus untuk menjamin tidak ada referensi ke node yang tersisa yang akan mencegah node dari pengumpulan sampah dan oleh karena itu menjaga semua node dalam daftar tertaut mulai saat itu tetap hidup di heap.
Evaluasi metode
Sekarang setelah simpul doa berhasil diurutkan, doa perlu dievaluasi. Untuk melakukan itu, langkah pertama adalah memastikan bahwa doa sebelum ini telah dievaluasi. Jika mereka tidak memiliki utas ini tidak akan menunggu tetapi akan segera bekerja.
Metode EnsurePrior
The ensurePrior()
Metode ini bekerja dengan memeriksa node sebelumnya dalam linked list. Jika statusnya tidak disetel, simpul sebelumnya akan dievaluasi. Node yang ini bersifat rekursif. Jika node sebelum node sebelumnya belum dievaluasi, itu akan memanggil evaluasi untuk node itu dan seterusnya.
Sekarang node sebelumnya diketahui memiliki status, kita dapat mengevaluasi node ini. Node terakhir diambil dan ditugaskan ke variabel lokal. Jika referensi ini adalah nol, itu berarti bahwa beberapa utas lain telah lebih dulu dari yang ini dan sudah mengevaluasi simpul ini; pengaturan negara itu. Jika tidak, keadaan simpul sebelumnya diteruskan ke metode Sequential
objek yang diterapkan bersama dengan pemanggilan simpul ini. Keadaan dikembalikan diatur pada node dan truncate()
metode ini disebut, membersihkan tautan mundur dari node karena tidak lagi diperlukan.
Metode MoveForward
Metode maju akan mencoba untuk memindahkan semua referensi kepala ke simpul ini jika mereka belum menunjuk ke sesuatu yang lebih jauh. Ini untuk memastikan bahwa jika utas berhenti memanggil, kepalanya tidak akan mempertahankan referensi ke simpul yang tidak lagi diperlukan. The compareAndSet()
metode akan memastikan bahwa kami hanya memperbarui node jika beberapa thread lain tidak berubah sejak itu diambil.
Umumkan susunan dan bantuan
Kunci untuk membuat pendekatan ini bebas-tunggu dan bukan hanya bebas-penguncian adalah bahwa kita tidak dapat berasumsi bahwa penjadwal thread akan memberikan prioritas setiap thread saat dibutuhkan. Jika setiap utas hanya mencoba mengurutkan simpul-simpulnya sendiri, ada kemungkinan bahwa utas dapat terus-menerus dipersiapkan sebelumnya di bawah beban. Untuk memperhitungkan kemungkinan ini, setiap utas pertama-tama akan mencoba 'membantu' utas lainnya yang mungkin tidak dapat diurutkan.
Ide dasarnya adalah bahwa setiap thread berhasil membuat node, urutan yang diberikan meningkat secara monoton. Jika utas atau utas terus-menerus mencegah utas lainnya, indeks penggunaan untuk menemukan simpul yang tidak diikuti dalam announce
larik akan bergerak maju. Bahkan jika setiap utas yang saat ini mencoba untuk mengurutkan simpul yang diberikan secara terus-menerus diawali oleh utas lain, akhirnya semua utas akan mencoba mengurutkan simpul itu. Sebagai ilustrasi, kami akan membuat contoh dengan tiga utas.
Pada titik awal, kepala ketiga elemen dan mengumumkan elemen diarahkan ke tail
node. Untuk lastSequence
setiap utas adalah 0.
Pada titik ini, Thread 1 dijalankan dengan doa. Ia memeriksa array yang mengumumkan untuk urutan terakhir (nol) yang merupakan simpul yang saat ini dijadwalkan untuk diindeks. Ini mengurutkan node dan lastSequence
diatur ke 1.
Thread 2 sekarang dijalankan dengan doa, itu memeriksa array yang mengumumkan di urutan terakhirnya (nol) dan melihat bahwa itu tidak membutuhkan bantuan dan upaya untuk mengurutkan doa itu. Berhasil dan sekarang lastSequence
diatur ke 2.
Thread 3 sekarang dieksekusi dan ia juga melihat bahwa simpul di announce[0]
sudah diurutkan dan urutan doa itu sendiri. Ini lastSequence
sekarang sudah siap untuk 3.
Sekarang Thread 1 dipanggil lagi. Itu memeriksa array mengumumkan di indeks 1 dan menemukan bahwa itu sudah diurutkan. Bersamaan, Thread 2 dipanggil. Itu memeriksa array mengumumkan di indeks 2 dan menemukan bahwa itu sudah diurutkan. Baik Thread 1 dan Thread 2 sekarang mencoba untuk mengurutkan node mereka sendiri. Thread 2 menang dan mengurutkan permohonannya. Sudah lastSequence
diatur ke 4. Sementara itu, utas tiga telah dipanggil. Ia memeriksa indeksnya lastSequence
(mod 3) dan menemukan bahwa simpul di announce[0]
belum diurutkan. Utas 2 kembali dipanggil pada saat yang sama dengan Utas 1 pada upaya kedua. Utas 1menemukan permohonan announce[1]
yang tidak diikuti di mana merupakan simpul yang baru saja dibuat oleh Thread 2 . Ia mencoba untuk mengurutkan permintaan Thread 2 dan berhasil. Thread 2 menemukan simpul itu sendiri di announce[1]
dan telah diurutkan. Ini diatur lastSequence
ke 5. Thread 3 kemudian dipanggil dan menemukan bahwa simpul yang ditempatkan 1 thread announce[0]
masih belum diurutkan dan upaya untuk melakukannya. Sementara itu Thread 2 juga telah dipanggil dan lebih dulu Thread 3. Urutan itu simpul dan set lastSequence
ke 6.
Thread Buruk 1 . Meskipun Thread 3 sedang mencoba untuk mengurutkannya, kedua utas telah terus-menerus digagalkan oleh penjadwal. Tetapi pada titik ini. Thread 2 juga sekarang menunjuk ke announce[0]
(6 mod 3). Ketiga utas diatur untuk mencoba mengurutkan permohonan yang sama. Tidak peduli thread mana yang berhasil, simpul berikutnya yang akan diurutkan akan menjadi doa menunggu Thread 1 yaitu simpul yang dirujuk oleh announce[0]
.
Ini tidak bisa dihindari. Agar utas menjadi pre-empted, utas lainnya harus mengurutkan node dan saat mereka melakukannya, mereka akan terus bergerak lastSequence
maju. Jika simpul utas yang diberikan terus-menerus tidak diurutkan, akhirnya semua utas akan menunjuk ke indeksnya dalam array yang diumumkan. Tidak ada utas yang akan melakukan hal lain sampai simpul yang ia coba bantu telah diurutkan, skenario kasus terburuk adalah bahwa semua utas menunjuk ke simpul yang tidak diikuti yang sama. Oleh karena itu, waktu yang diperlukan untuk mengurutkan setiap doa adalah fungsi dari jumlah utas dan bukan ukuran input.