"Konstruksi universal" adalah kelas pembungkus untuk objek berurutan yang memungkinkannya untuk linierisasi (kondisi konsistensi yang kuat untuk objek bersamaan). Misalnya, berikut ini adalah konstruksi bebas-tunggu yang disesuaikan, di Jawa, dari [1], yang mengasumsikan keberadaan antrian tunggu-bebas yang memenuhi antarmuka WFQ
(yang hanya membutuhkan konsensus satu kali antar thread) dan mengasumsikan Sequential
antarmuka:
public interface WFQ<T> // "FIFO" iteration
{
int enqueue(T t); // returns the sequence number of t
Iterable<T> iterateUntil(int max); // iterates until sequence max
}
public interface Sequential
{
// Apply an invocation (method + arguments)
// and get a response (return value + state)
Response apply(Invocation i);
}
public interface Factory<T> { T generate(); } // generate new default object
public interface Universal extends Sequential {}
public class SlowUniversal implements Universal
{
Factory<? extends Sequential> generator;
WFQ<Invocation> wfq = new WFQ<Invocation>();
Universal(Factory<? extends Sequential> g) { generator = g; }
public Response apply(Invocation i)
{
int max = wfq.enqueue(i);
Sequential s = generator.generate();
for(Invocation invoc : wfq.iterateUntil(max))
s.apply(invoc);
return s.apply(i);
}
}
Implementasi ini tidak terlalu memuaskan karena sangat lambat (Anda ingat setiap permintaan, dan harus mengulangnya di setiap penerapan - kami memiliki runtime linier dalam ukuran histori). Apakah ada cara yang kita bisa memperpanjang WFQ
dan Sequential
antarmuka (dengan cara yang masuk akal) untuk memungkinkan kita untuk menyimpan beberapa langkah ketika menerapkan doa baru?
Bisakah kita menjadikan ini lebih efisien (bukan runtime linier dalam ukuran histori, lebih disukai penggunaan memori juga turun) tanpa kehilangan properti bebas-tunggu?
Klarifikasi
"Konstruksi universal" adalah istilah yang saya cukup yakin dibuat oleh [1] yang menerima objek yang tidak aman tetapi kompatibel dengan utas, yang digeneralisasikan oleh Sequential
antarmuka. Menggunakan antrian tunggu-bebas, konstruksi pertama menawarkan thread-safe, linearizable dan juga bebas-tunggu (ini mengasumsikan determinisme dan menghentikan apply
operasi).
Ini tidak efisien, karena metode ini efektif untuk membuat setiap utas lokal mulai dari yang bersih dan menerapkan setiap operasi yang pernah direkam. Bagaimanapun, ini bekerja karena ini mencapai sinkronisasi secara efektif dengan menggunakan WFQ
untuk menentukan urutan di mana semua operasi harus diterapkan: setiap panggilan thread apply
akan melihat lokal yang samaSequential
objek , dengan urutan Invocation
s yang sama diterapkan untuk itu.
Pertanyaan saya adalah apakah kita dapat (misalnya) memperkenalkan proses pembersihan latar belakang yang memperbarui "keadaan awal" sehingga kita tidak perlu memulai ulang dari awal. Ini tidak sesederhana memiliki penunjuk atomik dengan penunjuk awal - pendekatan semacam ini dengan mudah kehilangan jaminan tunggu-bebas. Kecurigaan saya adalah bahwa beberapa pendekatan berbasis antrian mungkin bekerja di sini.
Jargon:
- tunggu-bebas - terlepas dari jumlah utas atau pengambilan keputusan penjadwal,
apply
akan berakhir dalam jumlah instruksi yang terbukti terbatas yang dieksekusi untuk utas itu. - kunci-bebas - sama seperti di atas, tetapi mengakui kemungkinan waktu eksekusi tanpa batas, hanya dalam kasus bahwa sejumlah
apply
operasi tanpa batas dilakukan di utas lainnya. Biasanya, skema sinkronisasi optimis termasuk dalam kategori ini. - pemblokiran - efisiensi karena belas kasihan penjadwal.
Contoh yang berfungsi, seperti yang diminta (sekarang pada halaman yang tidak akan kedaluwarsa)
[1] Herlihy dan Shavit, Seni Pemrograman Multi-Prosesor .
CopyableSequential
harus valid - linierabilitas kemudian harus mengikuti dari fakta bahwa ituSequential
.Jawaban:
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
namanextIndex
. Indeks-indeks ini ditugaskan untuk utas melalui sebuahThreadLocal
instance yang menginisialisasi dirinya dengan mendapatkan indeks berikutnya darinextIndex
dan menambahnya. Ini terjadi saat pertama kali setiap indeks utas diambil pertama kali. AThreadLocal
dibuat untuk melacak urutan terakhir yang dibuat utas ini. Ini diinisialisasi 0. Referensi objek pabrik berurutan dilewatkan dan disimpan. DuaAtomicReferenceArray
contoh dibuat dari ukurann
. Objek ekor ditugaskan untuk setiap referensi, yang telah diinisialisasi dengan keadaan awal yang disediakan olehSequential
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:
Kemudian loop pengurutan dimulai. Itu akan berlanjut sampai doa saat ini telah diurutkan:
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 doaapply()
. Ditetapkan pada konstruksi.next
:AtomicReference
untuk tautan penerusan. sekali ditugaskan, ini tidak akan pernah berubahprevious
: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 dariannounce
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 dantruncate()
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. UntuklastSequence
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. InilastSequence
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 indeksnyalastSequence
(mod 3) dan menemukan bahwa simpul diannounce[0]
belum diurutkan. Utas 2 kembali dipanggil pada saat yang sama dengan Utas 1 pada upaya kedua. Utas 1menemukan permohonanannounce[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 diannounce[1]
dan telah diurutkan. Ini diaturlastSequence
ke 5. Thread 3 kemudian dipanggil dan menemukan bahwa simpul yang ditempatkan 1 threadannounce[0]
masih belum diurutkan dan upaya untuk melakukannya. Sementara itu Thread 2 juga telah dipanggil dan lebih dulu Thread 3. Urutan itu simpul dan setlastSequence
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 olehannounce[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.sumber
previous
dannext
penunjuk menjadi valid. Mempertahankan dan membuat riwayat yang valid dengan cara bebas-tunggu tampaknya sulit.Jawaban saya sebelumnya tidak benar-benar menjawab pertanyaan dengan benar tetapi karena OP melihatnya berguna, saya akan membiarkannya apa adanya. Berdasarkan kode di tautan dalam pertanyaan, inilah upaya saya. Saya hanya melakukan pengujian yang sangat mendasar untuk hal ini tetapi tampaknya menghitung rata-rata dengan benar. Umpan balik disambut baik apakah ini benar bebas menunggu.
CATATAN : Saya menghapus antarmuka Universal dan menjadikannya kelas. Memiliki Universal terdiri dari Sequentials dan juga menjadi salah satu sepertinya komplikasi yang tidak perlu tapi saya mungkin kehilangan sesuatu. Di kelas rata-rata, saya telah menandai variabel keadaan menjadi
volatile
. Ini tidak perlu untuk membuat kode berfungsi. Menjadi konservatif (ide bagus dengan threading) dan mencegah setiap utas melakukan semua perhitungan (satu kali).Berurutan & Pabrik
Universal
Rata-rata
Kode demo
Saya melakukan beberapa pengeditan pada kode ketika saya memposting di sini. Seharusnya tidak masalah, tetapi beri tahu saya jika Anda memiliki masalah dengannya.
sumber
wfq
, jadi Anda masih harus menelusuri seluruh riwayat - runtime tidak membaik kecuali oleh faktor konstan.