Concurrency dengan LMAX Disruptor - An Introduction

1. Ikhtisar

Artikel ini memperkenalkan LMAX Disruptor dan membahas tentang bagaimana hal itu membantu mencapai konkurensi perangkat lunak dengan latensi rendah. Kami juga akan melihat penggunaan dasar pustaka Disruptor.

2. Apa Itu Pengganggu?

Disruptor adalah pustaka Java open source yang ditulis oleh LMAX. Ini adalah kerangka kerja pemrograman konkuren untuk memproses sejumlah besar transaksi, dengan latensi rendah (dan tanpa kerumitan kode serentak). Optimalisasi kinerja dicapai dengan desain perangkat lunak yang memanfaatkan efisiensi perangkat keras yang mendasarinya.

2.1. Simpati Mekanis

Mari kita mulai dengan konsep inti simpati mekanis - itu semua tentang memahami bagaimana perangkat keras yang mendasarinya beroperasi dan pemrograman dengan cara yang paling sesuai dengan perangkat keras itu.

Misalnya, mari kita lihat bagaimana CPU dan organisasi memori dapat memengaruhi kinerja perangkat lunak. CPU memiliki beberapa lapisan cache antara itu dan memori utama. Saat CPU menjalankan operasi, pertama-tama CPU mencari data di L1, lalu L2, lalu L3, dan terakhir, memori utama. Semakin jauh ia harus pergi, semakin lama waktu yang dibutuhkan untuk operasi tersebut.

Jika operasi yang sama dilakukan pada sepotong data beberapa kali (misalnya, penghitung loop), masuk akal untuk memuat data itu ke tempat yang sangat dekat dengan CPU.

Beberapa angka indikatif untuk biaya cache miss:

Latensi dari CPU ke Siklus CPU Waktu
Memori utama Banyak ~ 60-80 ns
Cache L3 ~ 40-45 siklus ~ 15 ns
Cache L2 ~ 10 siklus ~ 3 ns
Cache L1 ~ 3-4 siklus ~ 1 ns
Daftar 1 siklus Sangat sangat cepat

2.2. Mengapa Tidak Antrian

Implementasi antrian cenderung memiliki pertentangan penulisan pada variabel head, tail, dan size. Antrian biasanya selalu hampir penuh atau hampir kosong karena perbedaan kecepatan antara konsumen dan produsen. Mereka sangat jarang beroperasi di jalan tengah yang seimbang di mana tingkat produksi dan konsumsi seimbang.

Untuk menangani perselisihan tulis, antrian sering menggunakan kunci, yang dapat menyebabkan peralihan konteks ke kernel. Jika ini terjadi, prosesor yang terlibat kemungkinan besar akan kehilangan data dalam cache-nya.

Untuk mendapatkan perilaku caching terbaik, desain hanya boleh memiliki satu tulisan inti ke lokasi memori mana pun (beberapa pembaca baik-baik saja, karena prosesor sering menggunakan tautan berkecepatan tinggi khusus di antara cache mereka). Antrian gagal dengan prinsip satu penulis.

Jika dua utas terpisah menulis ke dua nilai yang berbeda, masing-masing inti membatalkan baris cache yang lain (data ditransfer antara memori utama dan cache dalam blok ukuran tetap, disebut garis cache). Itu adalah perselisihan antara dua utas meskipun mereka menulis ke dua variabel berbeda. Ini disebut berbagi palsu, karena setiap kali kepala diakses, ekor juga diakses, dan sebaliknya.

2.3. Bagaimana Pengganggu Bekerja

Disruptor memiliki struktur data melingkar berbasis larik (buffer cincin). Ini adalah larik yang memiliki penunjuk ke slot berikutnya yang tersedia. Itu diisi dengan objek transfer yang telah dialokasikan sebelumnya. Produsen dan konsumen melakukan penulisan dan pembacaan data ke ring tanpa penguncian atau perselisihan.

Dalam Disruptor, semua kejadian dipublikasikan ke semua konsumen (multicast), untuk konsumsi paralel melalui antrian hilir yang terpisah. Karena pemrosesan paralel oleh konsumen, perlu untuk mengoordinasikan ketergantungan antara konsumen (grafik ketergantungan).

Produsen dan konsumen memiliki penghitung urutan untuk menunjukkan slot mana di buffer yang saat ini sedang dikerjakan. Setiap produsen / konsumen dapat menulis penghitung urutannya sendiri tetapi dapat membaca penghitung urutan lainnya. Produsen dan konsumen membaca penghitung untuk memastikan slot yang ingin ditulisnya tersedia tanpa kunci apa pun.

3. Menggunakan Disruptor Library

3.1. Ketergantungan Maven

Mari kita mulai dengan menambahkan dependensi library Disruptor di pom.xml :

 com.lmax disruptor 3.3.6 

Versi terbaru dari ketergantungan tersebut dapat diperiksa di sini.

3.2. Mendefinisikan Acara

Mari tentukan acara yang membawa data:

public static class ValueEvent { private int value; public final static EventFactory EVENT_FACTORY = () -> new ValueEvent(); // standard getters and setters } 

EventFactory memungkinkan Disruptor melakukan pra - alokasi acara.

3.3. Konsumen

Konsumen membaca data dari ring buffer. Mari tentukan konsumen yang akan menangani acara:

public class SingleEventPrintConsumer { ... public EventHandler[] getEventHandler() { EventHandler eventHandler = (event, sequence, endOfBatch) -> print(event.getValue(), sequence); return new EventHandler[] { eventHandler }; } private void print(int id, long sequenceId) { logger.info("Id is " + id + " sequence id that was used is " + sequenceId); } }

Dalam contoh kami, konsumen hanya mencetak ke log.

3.4. Membangun Pengganggu

Bangun Disruptor:

ThreadFactory threadFactory = DaemonThreadFactory.INSTANCE; WaitStrategy waitStrategy = new BusySpinWaitStrategy(); Disruptor disruptor = new Disruptor( ValueEvent.EVENT_FACTORY, 16, threadFactory, ProducerType.SINGLE, waitStrategy); 

Dalam konstruktor Disruptor, yang berikut ini didefinisikan:

  • Pabrik Acara - Bertanggung jawab untuk menghasilkan objek yang akan disimpan dalam buffer cincin selama inisialisasi
  • Ukuran Buffer Cincin - Kami telah mendefinisikan 16 sebagai ukuran buffer cincin. Itu harus menjadi kekuatan 2 kalau tidak itu akan membuat pengecualian saat inisialisasi. Ini penting karena mudah untuk melakukan sebagian besar operasi menggunakan operator biner logis, misalnya operasi mod
  • Thread Factory - Pabrik untuk membuat thread untuk prosesor acara
  • Producer Type - Menentukan apakah kita akan memiliki satu atau beberapa produsen
  • Strategi menunggu - Menentukan bagaimana kami ingin menangani pelanggan lambat yang tidak mengikuti kecepatan produsen

Hubungkan penangan konsumen:

disruptor.handleEventsWith(getEventHandler()); 

Dimungkinkan untuk memasok banyak konsumen dengan Disruptor untuk menangani data yang dihasilkan oleh produsen. Dalam contoh di atas, kita hanya memiliki satu konsumen alias pengendali peristiwa.

3.5. Memulai Disruptor

Untuk memulai Disruptor:

RingBuffer ringBuffer = disruptor.start();

3.6. Memproduksi dan Menerbitkan Acara

Produsen menempatkan data di buffer cincin secara berurutan. Produsen harus mengetahui slot berikutnya yang tersedia sehingga mereka tidak menimpa data yang belum digunakan.

Gunakan RingBuffer dari Disruptor untuk menerbitkan:

for (int eventCount = 0; eventCount < 32; eventCount++) { long sequenceId = ringBuffer.next(); ValueEvent valueEvent = ringBuffer.get(sequenceId); valueEvent.setValue(eventCount); ringBuffer.publish(sequenceId); } 

Di sini, produser memproduksi dan menerbitkan item secara berurutan. Penting untuk dicatat di sini bahwa Disruptor bekerja mirip dengan protokol komit 2 fase. Itu membaca sequenceId baru dan menerbitkan. Lain kali itu harus mendapatkan sequenceId + 1 sebagai sequenceId berikutnya .

4. Kesimpulan

Dalam tutorial ini, kita telah melihat apa itu Disruptor dan bagaimana itu mencapai konkurensi dengan latensi rendah. Kami telah melihat konsep simpati mekanis dan bagaimana hal itu dapat dieksploitasi untuk mencapai latensi rendah. Kami kemudian telah melihat contoh menggunakan perpustakaan Disruptor.

Kode contoh dapat ditemukan di proyek GitHub - ini adalah proyek berbasis Maven, jadi semestinya mudah untuk mengimpor dan menjalankan apa adanya.