Pemrosesan konkuren yang aman di Ada ── panduan praktis task dan protected object
· Diperbarui pada: · Go Komura · Ada, Concurrency, Tasking, ProtectedObjects, Rendezvous, RealTime, ParallelProgramming, ProgrammingLanguage, Pemrosesan konkuren, Keandalan tinggi
1. Pendahuluan ── pemrosesan konkuren yang tertanam di bahasa
Pemrosesan konkuren adalah tema yang tidak terelakkan dalam pengembangan perangkat lunak modern. Namun di banyak bahasa, pemrosesan konkuren adalah «tambahan belakangan» yang bergantung pada pustaka atau fitur OS, sehingga pemakaian yang benar menuntut pengetahuan mendalam dan perancangan yang hati-hati.
Ada punya jawaban tersendiri untuk masalah itu. Pemrosesan konkuren tertanam di spesifikasi bahasa itu sendiri.
Model pemrosesan konkuren Ada:
- Task ── satuan konkuren yang dieksekusi secara independen
- Rendezvous ── komunikasi sinkron antar-task
- Protected object ── mutual exclusion yang dikelola bahasa
- Prioritas real-time ── fitur real-time Annex D
Task dan rendezvous sudah ada sejak Ada 83 (1983); protected object dan fitur real-time Annex D ditambahkan di Ada 95, lalu terus berkembang di Ada 2005 dan 2012. Bukan primitif sinkronisasi tingkat rendah seperti mutex atau semaphore, melainkan «niat rancangan dapat dinyatakan langsung di kode» — itulah ciri utama pemrosesan konkuren Ada.
Artikel ini menjelaskan pemrosesan konkuren Ada secara bertahap melalui delapan contoh kode praktis. Setiap contoh adalah cuplikan mandiri yang dapat dikompilasi dan dijalankan, dan dapat dicoba di mesin Anda.
Cuplikan kode yang muncul di artikel ini dipublikasikan di GitHub sebagai kumpulan kode rujukan yang diorganisasikan per bab ke dalam berkas.
ada-task-concurrency - komurasoft-blog-samples (GitHub)
Menjalankannya di mesin Anda ── build dan eksekusi
Karena sudah ditulis «dapat dikompilasi dan dijalankan», prosedurnya ditunjukkan lebih dulu.
Menyiapkan GNAT
GNAT adalah compiler Ada dari GCC. Di Linux dipasang dengan apt install gnat-13, di Windows dengan paket MSYS2 mingw-w64-x86_64-gcc-ada, atau lewat Alire (manajer paket Ada / SPARK).
Build dan jalankan
Setiap cuplikan berisi beberapa unit kompilasi dalam satu berkas (spesifikasi task, body task, prosedur utama), sehingga dipecah dulu dengan gnatchop lalu dibangun dengan gnatmake. gnatchop adalah alat yang membagi berkas menurut konvensi penamaan GNAT «nama unit = nama berkas».
mkdir work && cd work
gnatchop ../src/snippets/01_hello_task.ada
gnatmake hello_task_demo
./hello_task_demo
Nama prosedur utama setelah pemecahan menjadi nama berkas eksekusi apa adanya.
Korespondensi bab dan berkas
Nomor bab dan nomor berkas bergeser satu (bab 3 adalah 01_). Korespondensinya sebagai berikut.
| Bab | Berkas | Berkas eksekusi | Isi |
|---|---|---|---|
| Bab 3 | 01_hello_task.ada |
hello_task_demo |
Bentuk dasar task |
| Bab 4 | 02_rendezvous_intro.ada |
rendezvous_demo |
Transfer data dua arah lewat rendezvous |
| Bab 5 | 03_selective_accept.ada |
selective_accept_demo |
Accept selektif dan task server |
| Bab 6 | 04_producer_consumer.ada |
producer_consumer_demo |
Produsen–konsumen |
| Bab 7 | 05_protected_counter.ada |
protected_counter_demo |
Mutual exclusion lewat protected object |
| Bab 8 | 06_bounded_buffer.ada |
bounded_buffer_demo |
Entry terproteksi ber-barrier (bounded buffer) |
| Bab 9 | 07_timed_entry.ada |
timed_entry_demo |
Pemanggilan select ber-timeout |
| Bab 10 | 08_task_priorities.ada |
task_priorities_demo |
Prioritas task dan penjadwalan real-time |
Cuplikan di tubuh artikel dipotong hanya pada bagian yang diperlukan untuk penjelasan. Apa adanya, cuplikan itu tidak berjalan; ketika ingin mencobanya, gunakan berkas di atas.
Peta pengetahuan artikel ini
Task Ada berkomunikasi secara sinkron dengan pihak luar lewat rendezvous yang memakai entry dan accept; accept selektif (pernyataan select) menunggu beberapa entry dengan kondisi guard, dan dengan or terminate dapat menghindari task server yang menunggu selamanya, penyebab deadlock. Protected object adalah mutual exclusion yang dikelola bahasa: pemanggil ditahan sampai kondisi barrier pada entry menjadi true, sehingga akses eksklusif ke data bersama dijamin dan data race dicegah; di sisi lain, jika di dalam operasi terproteksi dilakukan pemrosesan terlarang seperti delay, itu termasuk bounded error, dan pada sebagian implementasi dapat berujung pada deadlock. Priority Ceiling Protocol menetapkan ceiling priority pada protected object untuk mencegah inversi prioritas, dan sifat real-time ditopang di atas latar teoretis Rate Monotonic Scheduling. Profil Ravenscar membatasi model task sehingga analisis deadlock secara statis menjadi mungkin.
flowchart LR
accTitle: Peta pengetahuan task dan protected object Ada
accDescr: Diagram yang menunjukkan task Ada yang berkomunikasi secara sinkron lewat rendezvous, accept selektif yang menunggu beberapa entry sambil menghindari deadlock, protected object yang melakukan mutual exclusion dengan barrier dan mencegah data race, serta Priority Ceiling Protocol dan profil Ravenscar yang menopang sifat real-time
ada_task["task Ada (pemrosesan concurrent)"]
protected_object["objek terlindung (protected object)"]
ada["Ada (bahasa pemrograman)"]
rendezvous["rendezvous"]
selective_accept["selective accept (pernyataan select)"]
deadlock["deadlock"]
barrier["barrier (kondisi when pada protected entry)"]
data_race["data race"]
erroneous_execution["eksekusi salah (erroneous execution)"]
bounded_error["kesalahan terbatas (bounded error)"]
priority_ceiling_protocol["protokol plafon prioritas (Priority Ceiling Protocol)"]
priority_inversion["pembalikan prioritas (priority inversion)"]
task_priority["prioritas task (pragma Priority)"]
rate_monotonic_scheduling["Rate Monotonic Scheduling (RMS)"]
ravenscar_profile["profil Ravenscar"]
ada_task -->|"mensyaratkan"| ada
protected_object -->|"mensyaratkan"| ada
ada_task -->|"menggunakan"| rendezvous
selective_accept -->|"menggunakan"| rendezvous
selective_accept -.->|"mencegah"| deadlock
protected_object -.->|"menggunakan"| barrier
protected_object -->|"mencegah"| data_race
data_race -->|"dapat menyebabkan"| erroneous_execution
bounded_error -.->|"dapat menyebabkan"| deadlock
protected_object -.->|"dapat menyebabkan"| bounded_error
priority_ceiling_protocol -->|"mencegah"| priority_inversion
priority_ceiling_protocol -->|"mensyaratkan"| protected_object
priority_ceiling_protocol -->|"menggunakan"| task_priority
rate_monotonic_scheduling -->|"menggunakan"| task_priority
rate_monotonic_scheduling -.->|"disarankan untuk"| ada_task
ravenscar_profile -.->|"mengurangi"| deadlock
ravenscar_profile -->|"mensyaratkan"| ada_task
ada_task -.->|"dapat menyebabkan"| priority_inversion
ada_task -.->|"dikonfigurasi dengan"| task_priority
rendezvous -.->|"mengurangi"| deadlock
Pada diagram, garis utuh menunjukkan relasi yang selalu berlaku dan garis putus-putus menunjukkan relasi bersyarat (syaratnya ada pada penjelasan masing-masing relasi di halaman rincian). Daftar lengkap relasi (total 20, beserta bukti dan tingkat kepastian) serta definisi konsep utama dikumpulkan di halaman rincian peta pengetahuan (dalam bahasa Jepang). Data: JSON-LD / Turtle
2. Mengingat kembali «bahaya» pemrosesan konkuren
Sebelum masuk ke Ada, mari tinjau singkat mengapa pemrosesan konkuren yang «aman» itu penting.
Bug khas pemrosesan konkuren mencakup hal-hal berikut.
- Data race: beberapa thread mengakses lokasi memori yang sama secara bersamaan, dan setidaknya satu di antaranya menulis. Hasilnya tidak terdefinisi.
- Deadlock: beberapa task saling menunggu penyelesaian satu sama lain, dan tidak ada yang maju selamanya.
- Inversi prioritas (priority inversion): task prioritas tinggi menunggu resource yang dipegang task prioritas rendah, lalu task prioritas menengah mem-preempt task prioritas rendah itu (menghentikan task yang sedang berjalan dan beralih ke task lain).
- Starvation: suatu task tidak pernah berhasil memperoleh resource yang dibutuhkannya.
Model pemrosesan konkuren Ada menyediakan pertahanan di tingkat bahasa terhadap masalah-masalah itu.
Data race → protected object menjamin akses eksklusif
Deadlock → model rendezvous menyediakan sinkronisasi terstruktur
Inversi prioritas → Priority Ceiling Protocol tersedia sebagai fitur bahasa
Starvation → barrier entry dan kebijakan antrean memberi kendali
3. Dasar task ── satuan eksekusi independen
Satuan dasar pemrosesan konkuren di Ada adalah task. Task mirip thread, tetapi tidak selalu berpasangan satu-satu dengan thread OS; runtime Ada yang mengelola penjadwalannya.
task Greeter is
entry Start;
end Greeter;
task body Greeter is
begin
accept Start;
Put_Line ("Hello from a task!");
end Greeter;
Kode ini (01_hello_task.ada) punya beberapa poin penting.
Task mulai dieksekusi secara otomatis begitu dideklarasikan. Task Greeter diaktifkan pada saat begin prosedur yang memuatnya tercapai, lalu menunggu permintaan rendezvous dari pemanggil di accept Start;.
Entry adalah antarmuka yang task buka ke dunia luar. Ketika pemanggil memanggil Greeter.Start;, ia tersinkronisasi dengan accept Start; pada task. Inilah yang disebut rendezvous.
Penyelesaian task ditunggu secara otomatis. Ketika prosedur utama berakhir, jika masih ada task yang berjalan, penyelesaiannya ditunggu secara implisit. Ini kontras dengan crash karena lupa memanggil std::thread::join di C++.
Contoh eksekusi
gnatchop ../src/snippets/01_hello_task.ada
gnatmake hello_task_demo
./hello_task_demo
Main: starting task...
Hello from a task!
Main: task has completed.
Ada satu hal yang perlu diperhatikan. accept Start; tidak punya blok do ... end. Artinya, begitu rendezvous terbentuk keduanya dilepas, dan setelah itu keduanya maju secara konkuren. Karena itu urutan dua baris terakhir tidak dijamin. Bergantung pada lingkungan, Main: task has completed. dapat muncul lebih dulu. Jika urutan juga ingin dikunci, letakkan pemrosesan yang urutannya harus dijaga di dalam do ... end pada accept Start do ... end Start;. Hanya baris pertama yang pasti di depan: Greeter berhenti di accept sampai Greeter.Start; dipanggil.
4. Rendezvous ── komunikasi sinkron yang mentransfer data
Rendezvous bukan sekadar sinkronisasi; data juga dapat dipertukarkan dua arah.
task Worker is
entry Compute (X, Y : Integer; Result : out Integer);
end Worker;
task body Worker is
A, B : Integer;
Output : Integer;
begin
accept Compute (X, Y : Integer; Result : out Integer) do
A := X;
B := Y;
Output := A * A + B * B;
Result := Output;
end Compute;
end Worker;
Pemanggil memakainya sebagai berikut (02_rendezvous_intro.ada).
Worker.Compute (3, 4, Answer);
Put_Line ("Main: result = " & Integer'Image (Answer));
Poin rancangan yang penting di sini adalah bahwa mode parameter dinyatakan secara eksplisit.
- Mode
in: meneruskan nilai dari pemanggil ke task - Mode
out: mengembalikan hasil dari task ke pemanggil - Mode
in out: dua arah
Blok do ... end di dalam body accept menjadi critical section. Selama itu pemanggil diblokir, dan task tidak menerima entry lain. Ketika pemrosesan selesai, keduanya dilanjutkan.
Jika dilihat dalam urutan waktu, penantiannya tampak sebagai berikut.
sequenceDiagram
accTitle: Urutan rendezvous Ada
accDescr: Pemanggil dan task Worker tersinkronisasi pada Compute, menjalankan body accept, lalu keduanya dilanjutkan bersama.
participant Main as Pemanggil
participant W as Task Worker
Main->>W: Memanggil Worker.Compute
Note over Main: Diblokir sampai mencapai accept
W->>W: Mencapai accept Compute ... do
Note over Main,W: Rendezvous terbentuk / body accept dijalankan
W-->>Main: Menulis hasil ke parameter out Result
Note over Main,W: Pada end Compute keduanya dilanjutkan bersama
Main->>Main: Pemrosesan lanjutan
W->>W: Pemrosesan lanjutan
Siapa yang tiba lebih dulu menunggu. Jika pemanggil lebih dulu, ia berhenti sampai accept tercapai; jika sisi task lebih dulu, ia berhenti sampai seseorang memanggil. Siapa pun yang lebih dulu, isi do ... end selalu dijalankan dalam keadaan kedua belah pihak sudah hadir.
Ciri rendezvous diringkas sebagai berikut:
| Ciri | Penjelasan |
|---|---|
| Sinkron | Pemanggil dan task menunggu sampai keduanya mencapai titik rendezvous |
| Transfer data | Parameter in / out / in out dapat meneruskan nilai dua arah |
| Mutual exclusion | Selama body accept dieksekusi, entry lain pada task itu diblokir |
| Terstruktur | Entry mana yang diterima kapan dinyatakan secara eksplisit di body task |
Contoh eksekusi
gnatchop ../src/snippets/02_rendezvous_intro.ada
gnatmake rendezvous_demo
./rendezvous_demo
Main: calling Worker.Compute...
Main: result = 25
3 * 3 + 4 * 4 = 25 kembali lewat parameter out. Ada satu spasi di kanan = karena spesifikasi 'Image pada tipe integer menempatkan satu karakter spasi di depan nilai yang tidak negatif. Berbeda dengan bab 3, di sini urutan dua baris dijamin: pemanggilan Worker.Compute tidak kembali sebelum end Compute;.
5. Accept selektif ── menunggu beberapa layanan
Pada task server yang nyata, beberapa jenis permintaan perlu ditunggu. Pernyataan select Ada mewujudkan ini di tingkat bahasa.
task Server is
entry Deposit (Amount : Integer);
entry Withdraw (Amount : Integer; Success : out Boolean);
entry Balance (Value : out Integer);
end Server;
task body Server is
Current : Integer := 0;
begin
loop
select
accept Deposit (Amount : Integer) do
Current := Current + Amount;
end Deposit;
or
accept Withdraw (Amount : Integer; Success : out Boolean) do
if Current >= Amount then
Current := Current - Amount;
Success := True;
else
Success := False;
end if;
end Withdraw;
or
accept Balance (Value : out Integer) do
Value := Current;
end Balance;
or
terminate;
end select;
end loop;
end Server;
Pada pernyataan select kode ini (03_selective_accept.ada) ada beberapa cabang or, dan salah satu entry yang memiliki panggilan dipilih (pilihannya ditentukan oleh implementasi). Jika tidak ada entry yang dipanggil, task menunggu sampai salah satunya dipanggil.
or terminate; adalah cabang khusus yang mengakhiri task dengan aman ketika «prosedur utama telah selesai dan tidak ada lagi yang dapat memanggil entry pada task ini». Ini mekanisme khas Ada yang menyelesaikan masalah «task server yang menunggu selamanya», penyebab deadlock.
Kekuatan accept selektif adalah kondisi guard juga dapat ditulis.
Berikut adalah task yang di dalamnya memuat ring buffer. Jika hanya bagian select yang dipotong, tidak jelas dari mana Count atau Head berasal, jadi ditampilkan utuh mulai dari bagian deklarasi. Bentuk yang sama, ditulis ulang dengan protected object, dibahas di bab 8.
task Buffer_Task is
entry Put_Item (Item : Integer);
entry Get_Item (Item : out Integer);
end Buffer_Task;
task body Buffer_Task is
Max : constant := 8;
Data : array (0 .. Max - 1) of Integer;
Head : Integer := 0; -- posisi yang akan diambil berikutnya
Tail : Integer := 0; -- posisi yang akan ditulis berikutnya
Count : Integer := 0; -- jumlah elemen saat ini
begin
loop
select
when Count > 0 =>
accept Get_Item (Item : out Integer) do
Item := Data (Head);
Head := (Head + 1) mod Max;
Count := Count - 1;
end Get_Item;
or
when Count < Max =>
accept Put_Item (Item : Integer) do
Data (Tail) := Item;
Tail := (Tail + 1) mod Max;
Count := Count + 1;
end Put_Item;
or
terminate;
end select;
end loop;
end Buffer_Task;
Cabang yang kondisi guard-nya false, pada saat itu, dikeluarkan dari kandidat pemilihan. Dengan demikian, pengendalian seperti «jika buffer kosong, Get harus menunggu; jika penuh, Put harus menunggu» dapat ditulis secara deklaratif. Memajukan Head dan Tail dengan mod Max adalah inti ring buffer, dan kondisi guard juga berperan menjamin indeks itu tetap berada dalam rentang yang valid.
6. Produsen–konsumen ── sinkronisasi lewat rendezvous
Sebagai pola khas yang memakai rendezvous, mari lihat produsen–konsumen.
task Consumer is
entry Deliver (Item : Integer);
end Consumer;
task Producer;
task body Consumer is
Sum : Integer := 0;
begin
for I in 1 .. 5 loop
accept Deliver (Item : Integer) do
Sum := Sum + Item;
end Deliver;
end loop;
end Consumer;
task body Producer is
begin
for I in 1 .. 5 loop
Consumer.Deliver (I);
end loop;
end Producer;
Pada pola ini (04_producer_consumer.ada), setiap kali produsen memanggil Deliver ia tersinkronisasi dengan konsumen. Jika produsen terlalu cepat, ia menunggu sampai konsumen melakukan accept; jika konsumen terlalu cepat, ia menunggu pemanggilan berikutnya dari produsen. Backpressure alami pun terjadi (ketika penerima tidak sanggup mengikuti, kecepatan pengirim ditekan secara otomatis). Pada rendezvous tanpa antrean di antaranya, ini terwujud tanpa kekhawatiran overflow buffer.
7. Protected object ── mutual exclusion tanpa lock
Jika task adalah «pelaku aktif», protected object adalah mekanisme untuk «data bersama yang pasif».
protected Counter is
procedure Increment;
function Value return Integer;
private
Count : Integer := 0;
end Counter;
protected body Counter is
procedure Increment is
begin
Count := Count + 1;
end Increment;
function Value return Integer is
begin
return Count;
end Value;
end Counter;
Aturan penting protected object adalah sebagai berikut.
- Function bersifat baca-saja. Beberapa task dapat memanggil function secara bersamaan.
- Procedure bersifat baca-tulis. Selama procedure dieksekusi, procedure lain maupun function diblokir.
- Entry ber-barrier. Pemanggil menunggu di antrean sampai kondisi barrier menjadi true.
Pada kode ini (05_protected_counter.ada), tiga task worker masing-masing memanggil Increment 1.000 kali. Karena protected object menjamin mutual exclusion, nilai counter akhir selalu 3.000. Tidak perlu menulis lock dan unlock mutex secara manual.
task type Worker (Id : Integer; Rounds : Integer);
task body Worker is
begin
for I in 1 .. Rounds loop
Counter.Increment; -- protected object menjamin mutual exclusion
end loop;
end Worker;
W1 : Worker (1, 1_000);
W2 : Worker (2, 1_000);
W3 : Worker (3, 1_000);
Contoh eksekusi
Di sini ada satu masalah. Jika prosedur utama langsung membaca Counter.Value, ia akan membaca nilai di tengah-tengah putaran worker. Karena itu versi lengkap (05_protected_counter.ada) menambahkan procedure yang menghitung penyelesaian, dan entry yang menunggu sampai semua selesai.
protected Counter is
procedure Increment;
procedure Mark_Done;
entry All_Done;
function Value return Integer;
private
Count : Integer := 0;
Done_Count : Integer := 0;
end Counter;
Barrier entry All_Done when Done_Count = Num_Workers dipasang, dan setiap worker memanggil Counter.Mark_Done; setelah keluar dari loop. Prosedur utama menunggu penyelesaian semua pihak dengan Counter.All_Done; baru kemudian membaca nilai. Baik variabel flag terpisah untuk menunggu maupun sleep tidak diperlukan.
gnatchop ../src/snippets/05_protected_counter.ada
gnatmake protected_counter_demo
./protected_counter_demo
Final counter value = 3000
Berapa pun kali dijalankan, hasilnya 3000. Tiga task memanggil Increment total 3.000 kali, dan protected object mengeksekusi setiap panggilan secara eksklusif.
Apa yang terjadi tanpa protected object
Untuk memahami nilai protected object, mari lihat kode berbahaya jika tidak dilindungi.
-- ⚠ Berbahaya: memanipulasi variabel bersama secara langsung
Shared_Counter : Integer := 0;
task body Bad_Worker is
begin
for I in 1 .. 10_000 loop
Shared_Counter := Shared_Counter + 1; -- data race!
end loop;
end Bad_Worker;
Shared_Counter := Shared_Counter + 1 pada tingkat CPU adalah tiga langkah: «baca → tambah → tulis kembali». Jika beberapa task menjalankannya secara bersamaan, hasil penambahan suatu task tidak sempat terlihat pada pembacaan task lain, dan increment hilang. Lebih dari itu, ini termasuk erroneous execution menurut Ada RM 9.10. «Erroneous execution» adalah istilah standar yang lebih kuat daripada «nilainya bergeser»: artinya standar tidak lagi menjamin apa pun tentang perilaku program itu. Baca-tulis bersamaan pada variabel bersama yang tidak disinkronkan tidak berhenti pada ketidakakuratan nilai hitung akhir; perilaku program secara keseluruhan dapat menjadi sebarang. Meski dua task masing-masing dijalankan 10.000 kali, tidak ada jaminan sama sekali bahwa nilai akhir menjadi 20.000.
Jika ingin melihat sendiri «tidak adanya jaminan» itu, buat versi Bad_Worker di atas, jalankan berulang-ulang, dan catat nilai akhir setiap kali. Artikel ini tidak memuat nilai terukur. Hasil data race berubah menurut CPU, opsi optimisasi, dan timing runtime; menampilkan satu angka dari lingkungan tertentu sebagai «beginilah yang terjadi» justru memberi patokan keliru semacam «gesernya kira-kira sebesar ini». Yang perlu dipastikan bukan «muncul nilai tertentu yang lebih kecil dari 20.000», melainkan bahwa hasil berubah setiap eksekusi, dan bahkan jika sekali saja nilai yang benar muncul, itu tidak berarti apa-apa.
Protected object adalah mekanisme yang «mencegah masalah ini lewat sintaksis». Cukup memanggil Counter.Increment;, dan compiler serta runtime menjamin mutual exclusion.
8. Entry terproteksi dan barrier ── bounded buffer
Dengan menambahkan entry pada protected object, sinkronisasi bersyarat menjadi mungkin. Mari lihat pada bounded buffer klasik.
type Buffer_Array is array (0 .. Buffer_Size - 1) of Integer;
protected Buf is
entry Put (Item : Integer);
entry Get (Item : out Integer);
private
Data : Buffer_Array;
Head : Integer := 0;
Tail : Integer := 0;
Count : Integer := 0;
end Buf;
protected body Buf is
entry Put (Item : Integer) when Count < Buffer_Size is
begin
Data (Tail) := Item;
Tail := (Tail + 1) mod Buffer_Size;
Count := Count + 1;
end Put;
entry Get (Item : out Integer) when Count > 0 is
begin
Item := Data (Head);
Head := (Head + 1) mod Buffer_Size;
Count := Count - 1;
end Get;
end Buf;
when Count < Buffer_Size adalah barrier. Barrier dievaluasi setiap kali entry dipanggil; jika true, eksekusi dilanjutkan, jika false task pemanggil menunggu di antrean. Setiap kali state buffer berubah (task lain menjalankan Put atau Get), barrier task yang menunggu dievaluasi ulang.
Kapan evaluasi ulang terjadi, sulit diikuti hanya dari teks. Jika Get tiba lebih dulu pada buffer kosong, urutan waktunya sebagai berikut.
sequenceDiagram
accTitle: Evaluasi ulang barrier pada protected object
accDescr: Get pada buffer kosong menunggu di antrean; setelah Put, barrier dievaluasi ulang dan Get dijalankan.
participant C as Task Consumer
participant B as Protected object Buf
participant P as Task Producer
C->>B: Memanggil Get
B->>B: Mengevaluasi apakah barrier Count lebih besar dari 0 → false
Note over C: Menunggu di antrean entry Get
P->>B: Memanggil Put
B->>B: Mengevaluasi apakah barrier Count kurang dari Buffer_Size → true
B->>B: Menjalankan body Put / Count menjadi 1
Note over B: Pada akhir operasi terproteksi, barrier entry yang menunggu dievaluasi ulang
B->>B: Barrier Get menjadi true
B-->>C: Menjalankan body Get dan melepaskan Consumer
Poinnya: evaluasi ulang barrier dilakukan sekaligus pada penghujung operasi terproteksi. Di antara selesainya body Put dan dilepaskannya lock Buf, barrier entry yang menunggu dievaluasi, dan yang menjadi true dijalankan apa adanya. Tidak ada pola kegagalan seperti condition variable di C, di mana «jika seseorang lupa memanggil signal, tidak pernah bangun lagi».
Pola ini (06_bounded_buffer.ada) adalah salah satu momen ketika protected object Ada paling bersinar. Bandingkan dengan penulisan di C memakai mutex + condition variable pthread.
// C + pthread (untuk perbandingan dengan Ada)
pthread_mutex_lock(&mutex);
while (count >= BUFFER_SIZE) { // setara dengan when di Ada
pthread_cond_wait(¬_full, &mutex); // menunggu barrier
}
data[tail] = item;
tail = (tail + 1) % BUFFER_SIZE;
count++;
pthread_cond_signal(¬_empty); // memberitahu task yang menunggu
pthread_mutex_unlock(&mutex);
Di Ada, semuanya terangkum dalam satu baris when Count < Buffer_Size. Kondisi while, pengiriman sinyal, kesalahan timing pelepasan lock — semua kesempatan bug itu hilang.
9. Pemanggilan ber-timeout ── tidak menunggu selamanya
Dalam sistem real-time, «menunggu selamanya» tidak diizinkan. Ada mendukung timeout dengan sintaksis select ... or delay.
select
Slow_Worker.Do_Work (Result);
Put_Line ("Main: work completed");
or
delay until Ada.Real_Time.Clock + Milliseconds (500);
Put_Line ("Main: timeout after 500ms!");
end select;
Pada kode ini (07_timed_entry.ada), Slow_Worker sedang menjalankan delay 2.0 dan belum mencapai accept, sehingga pemanggilan entry yang masuk antrean timeout setelah 500 ms. (Timeout berlaku pada waktu menunggu di antrean sebelum rendezvous diterima; ia tidak memotong eksekusi body rendezvous itu sendiri.) delay until adalah penentuan waktu absolut, dan merupakan teknik dasar pemrograman real-time untuk mencegah drift kumulatif.
Ada juga mendukung pemanggilan entry bersyarat (conditional entry call).
select
Server.Process (Item);
else
Put_Line ("Server is busy, will retry later");
end select;
Berkat cabang else, jika rendezvous tidak dapat terjadi segera, pemrosesan alternatif dilanjutkan seketika. Polling tidak perlu ditulis secara manual.
Jangan lupa rancangan setelah timeout
Timeout itu nyaman, tetapi esensi rancangannya adalah «apa yang dilakukan setelah tidak berhasil menunggu». Apakah nilainya benar-benar boleh dibuang? Haruskah dicoba lagi? Haruskah dilaporkan sebagai error ke tingkat atas? Jika ini dibiarkan samar, di lingkungan produksi ia berubah menjadi kehilangan data atau berhentinya layanan. Ketika menulis timeout, rancang juga tanggung jawab setelah timeout di tempat yang sama.
Task periodik dan delay until
delay until tidak hanya untuk timeout; ia juga dipakai untuk eksekusi periodik. Dengan delay 0.1 sederhana, periodenya menjadi «waktu pemrosesan + 0,1 detik»; sebaliknya delay until menentukan titik aktivasi berikutnya pada waktu absolut, sehingga periode yang stabil, tidak bergantung pada waktu pemrosesan, dapat dipertahankan.
loop
Next := Next + Period;
Do_Work;
delay until Next;
end loop;
Pola ini efektif di setiap situasi yang menuntut pemrosesan berperiode tetap — pemantauan sensor, loop kendali, dan sebagainya.
10. Prioritas task dan penjadwalan real-time
Fitur real-time Ada didefinisikan di Annex D (Real-Time Systems). Jika implementasi Ada mendukung Annex D, prioritas task dan kebijakan penjadwalan dapat ditentukan.
Memastikan apakah dapat dipakai di lingkungan Anda
Annex D adalah salah satu Specialized Needs Annex (lampiran untuk bidang tertentu), dan dukungan bergantung pada implementasi serta lingkungan eksekusi. Apakah dapat dipakai di mesin Anda dipilah dalam tiga tahap berikut.
1. Melihat rentang prioritas
with Ada.Text_IO; use Ada.Text_IO;
with System;
procedure Check_Priority is
begin
Put_Line ("Priority range :"
& Integer'Image (System.Priority'First)
& " .."
& Integer'Image (System.Priority'Last));
Put_Line ("Default_Priority :"
& Integer'Image (System.Default_Priority));
end Check_Priority;
Rentang dan nilai default System.Priority bergantung pada implementasi, jadi angka konkret tidak ditunjukkan di sini. Jika rentang yang ditampilkan cukup luas, lingkungan itu membuat spesifikasi seperti pragma Priority (System.Default_Priority + 5) bermakna.
2. Melihat apakah spesifikasi kebijakan lolos kompilasi
pragma Task_Dispatching_Policy (FIFO_Within_Priorities);
pragma Locking_Policy (Ceiling_Locking);
Jika build dengan ini lolos, setidaknya secara sintaksis spesifikasi itu diterima.
3. Melihat apakah prioritas benar-benar berlaku
Inilah jebakan terbesar. Lolos kompilasi dan scheduler OS benar-benar menjalankan sesuai prioritas adalah dua hal yang berbeda. Pada OS general-purpose seperti Linux atau Windows, agar prioritas real-time benar-benar tercermin ke scheduler, pengaturan hak di sisi OS kadang diperlukan. README kumpulan sampel artikel ini juga mencatat bahwa di lingkungan yang Annex D-nya tidak didukung sepenuhnya, 08_task_priorities.ada berjalan sebagai task biasa.
Dengan kata lain, meski prioritas tidak berlaku, program tetap berjalan. Untuk keperluan yang menuntut hard real-time, selain rancangan prioritas di atas kertas, tahap mengukur dan memastikan urutan yang sebenarnya di lingkungan target menjadi wajib.
task High_Task is
pragma Priority (System.Default_Priority + 5);
end High_Task;
task Low_Task is
pragma Priority (System.Default_Priority);
end Low_Task;
Sebagai pengaturan yang lebih lanjut, kebijakan penjadwalan dan Priority Ceiling Protocol juga dapat ditentukan.
pragma Task_Dispatching_Policy (FIFO_Within_Priorities);
pragma Locking_Policy (Ceiling_Locking);
Priority Ceiling Protocol adalah protokol untuk mencegah inversi prioritas. Pada setiap protected object, ceiling priority ditetapkan secara eksplisit dengan pragma Priority (atau aspect Priority). Jika prioritas aktif task pemanggil melebihi ceiling itu, Program_Error terjadi. Selama objek terkunci, eksekusi berjalan pada ceiling priority, sehingga preemption oleh task prioritas menengah dicegah.
protected Shared_Data is
pragma Priority (15); -- ceiling priority
procedure Update (Val : Integer);
function Read return Integer;
private
Data : Integer := 0;
end Shared_Data;
Fitur-fitur ini berpijak pada latar teoretis Rate Monotonic Scheduling (RMS), dan punya rekam jejak pada sistem hard real-time seperti kendali penerbangan pesawat dan perangkat medis. RMS adalah skema prioritas tetap «task dengan periode lebih pendek diberi prioritas lebih tinggi»; bahwa himpunan task periodik dapat dianalisis sebelum eksekusi apakah akan menepati tenggat, itulah yang dihargai pada keperluan hard real-time.
11. Pedoman rancangan untuk praktik
Sampai di sini kita telah melihat sintaksis dasar task dan protected object. Terakhir, pedoman rancangan yang perlu diingat ketika memakai pemrosesan konkuren Ada di pekerjaan nyata disusun di sini.
Apa yang tidak boleh dilakukan di dalam protected object
Aturan emas di dalam protected object: perbarui state secara singkat, dan jalankan pemrosesan berat di luar. Operasi pada protected object secara internal berada dalam mutual exclusion, jadi pemblokiran lama di dalamnya menghentikan semua task lain yang memakai protected object yang sama.
Pemrosesan yang secara konkret harus dihindari:
delayatau I/O yang memakan waktu- Pemanggilan rumit ke protected object lain
- Pemanggilan pustaka eksternal yang berat
Selain itu, delay atau I/O tertentu di dalam operasi terproteksi bukan sekadar masalah kinerja: menurut standar Ada itu bounded error. Bounded error adalah jenis error «rentang hasil yang mungkin ditentukan standar, tetapi mana di antaranya yang terjadi tidak ditentukan». Tidak tanpa batas seperti erroneous execution, tetapi jaminan berjalan benar pun tidak ada. Pada praktiknya, bergantung pada implementasi, Program_Error dapat terjadi atau deadlock dapat timbul, sehingga yang diperlukan bukan «menahannya» melainkan menghilangkannya sama sekali.
Rancangan yang baik mengikuti pola «ambil nilai yang diperlukan dari protected object dalam waktu singkat → lakukan perhitungan berat atau I/O di luar → tulis kembali hanya hasilnya ke protected object dalam waktu singkat».
Jaga kondisi barrier tetap sederhana
Barrier entry ... when <condition> itu kuat, tetapi jika terlalu rumit menjadi sulit dibaca, dan sulit menelusuri mengapa suatu task tidak dilepas.
Idealnya pada tingkat yang makna state-nya langsung terbaca, seperti when Count < Buffer_Size atau when Used > 0. Jika beberapa kondisi memang diperlukan, pertimbangkan merepresentasikan state sebagai tipe enumerasi, dan mendekatkan barrier ke bentuk yang dapat dibaca lewat nama state, seperti when State = Running.
Exception pada task dan penghentian
Kebijakan ketika exception terjadi di dalam task perlu diputuskan secara eksplisit. Paling tidak, tangkap exception di tingkat terluar body task dan catat apa yang terjadi.
Yang lebih penting adalah rancangan setelah exception. Jika task itu berhenti, apakah sistem dapat berlanjut? Bolehkah di-restart? Bagaimana memberitahu task lain? Bagaimana mengembalikan state bersama ke kondisi yang aman? Pertanyaan-pertanyaan itu perlu dapat dijawab. Ada memiliki mekanisme exception sebagai fitur bahasa, tetapi keamanan setelah exception adalah tanggung jawab rancangan aplikasi.
Mini checklist rancangan
| Aspek | Yang dicek |
|---|---|
| State bersama | Apakah terkungkung di protected object? Tidak disentuh langsung dari luar? |
| Operasi terproteksi | Apakah singkat? Tidak memblokir di dalamnya? |
| Entry | Apakah barrier sederhana? Adakah kemungkinan menunggu selamanya? Adakah kebijakan timeout? |
| Masa hidup task | Apakah kondisi berakhir jelas? Adakah kebijakan saat exception? |
| Pemrosesan periodik | Apakah delay until sudah dipertimbangkan, bukan delay? |
Dalam pemrosesan konkuren, «mungkin tidak apa-apa» adalah frasa paling berbahaya. Menyatakan state bersama, kondisi menunggu, kondisi berakhir, dan kebijakan exception secara eksplisit di kode adalah langkah pertama menuju pemrosesan konkuren yang aman.
12. Ringkasan ── bahasa yang menjadikan pemrosesan konkuren «tata bahasa»
Yang membedakan model pemrosesan konkuren Ada dari bahasa lain adalah bahwa pemrosesan konkuren yang aman bukan «best practice tambahan belakangan», melainkan tertanam sebagai «tata bahasa».
| Yang ingin dilakukan | Tata bahasa Ada |
|---|---|
| Satuan eksekusi independen | task / task body |
| Komunikasi sinkron | entry / accept |
| Menunggu beberapa permintaan | select / or / else |
| Mutual exclusion | protected / function / procedure |
| Sinkronisasi bersyarat | entry ... when <barrier> |
| Timeout | or delay until <time> |
| Pengendalian prioritas | pragma Priority |
Konstruksi ini menjadi sasaran verifikasi compiler. Misalnya, mencoba menulis ulang komponen private protected object itu sendiri di dalam function-nya menghasilkan error kompilasi. Ketika operasi terproteksi selesai, barrier entry yang menunggu dievaluasi ulang secara otomatis — pengiriman sinyal manual tidak diperlukan.
«Sebagaimana type system menjamin keamanan memori,
sintaksis pemrosesan konkuren Ada menjamin keamanan sinkronisasi»
Delapan contoh kode yang dibahas artikel ini adalah pengantar praktis ke task, rendezvous, protected object, dan fitur real-time. Cobalah menjalankannya di mesin Anda, lalu tantang juga topik lanjutan berikut.
- Profil Ravenscar: profil pembatasan task untuk sistem real-time keandalan tinggi. Model task yang dibatasi memungkinkan analisis deadlock statis.
- Blok paralel Ada 2022: pemrosesan data-parallel dengan sintaksis
parallel ... do. - Integrasi dengan SPARK: memverifikasi secara formal perilaku program konkuren (didukung GNATprove di bawah profil Ravenscar).
Meskipun demikian, «memakai Ada» tidak membuatnya aman
Catatan penting di penutup. Sintaksis pemrosesan konkuren Ada itu kuat, tetapi memakai Ada tidak secara otomatis membuat program aman. Menyentuh data bersama tanpa memasukkannya ke protected object, memblokir lama di dalam protected object, membuat beberapa protected object saling memanggil secara rumit — kesalahan rancangan semacam itu dapat terjadi juga di Ada.
Fitur bahasa dirancang agar «untuk menulis secara berbahaya diperlukan upaya eksplisit», tetapi tidak menggantikan rancangan yang benar itu sendiri. Nilai sesungguhnya Ada adalah dapat membawa diskusi keamanan ke tempat yang dekat dengan kode — pertanyaan seperti «apakah state ini terlindungi?», «kapan task ini berakhir?», «pada kondisi apa entry ini menunggu?» dapat ditinggalkan sebagai sintaksis di kode.
Gagasan Ada yang berbicara tentang rancangan lewat tipe tetap konsisten juga pada pemrosesan konkuren. Pemrosesan konkuren yang aman tidak dimulai dari menangani lock dengan hati-hati, melainkan dari tidak membiarkan state bersama yang berbahaya ada secara telanjang.
Terhadap anggapan umum «pemrosesan konkuren itu sulit», Ada menjawab: «jika sintaksis dipilih dengan benar, keamanan dijamin compiler». Filsafat rancangan itu beririsan dengan bahasa modern seperti Rust dan Pony, tetapi Ada telah memegangnya sebagai spesifikasi bahasa sejak lebih dari 40 tahun lalu.
Artikel terkait
Artikel terbaru dengan tag yang sama untuk mendalami topik-topik terdekat.
Pemrograman sistem real-time dengan Ada — praktik pengendalian prioritas, periode, dan waktu eksekusi
Artikel ini membahas Annex D (sistem real-time) pada Ada melalui delapan contoh kode praktis. Prioritas task, Ceiling_Locking, eksekusi p...
Pemrograman generik di Ada ── menulis kontrak dengan tipe, dan mewujudkan reuse tanpa biaya runtime
Penjelasan sistematis pemrograman generik di Ada: subprogram generik, paket generik, subprogram formal, kategori tipe, hingga pedoman des...
Kedalaman virtualisasi Windows (Bagian 3) — Mesin virtual yang boot dalam hitungan detik: mengapa WSL2, Windows Sandbox, dan kontainer begitu ringan
Mengapa WSL2 dan Windows Sandbox start dalam hitungan detik dan terasa begitu ringan? Artikel ini menjelaskan mekanismenya, dari dynamic ...
Kedalaman virtualisasi Windows (Bagian 2) — Memori yang bahkan kernel pun tidak bisa lihat: cara kerja VBS, HVCI, dan Credential Guard
Pada instalasi bersih ke perangkat keras yang kompatibel, VBS diaktifkan secara bawaan dan memakai hypervisor serta SLAT untuk membuat is...
Kedalaman virtualisasi Windows (Bagian 1) — Di mana Windows Anda sebenarnya berjalan? Hypervisor dan partisi
Ketika Anda mengaktifkan Hyper-V, Windows host sendiri berjalan di atas hypervisor sebagai root partition. Artikel ini menjelaskan fondas...
Topik terkait
Halaman-halaman ini menempatkan topik dalam konteks layanan dan keputusan yang lebih luas.
Topik teknis Windows
Portal tentang pengembangan Windows, investigasi bug, dan pemanfaatan aset yang ada.
Pertanyaan yang sering diajukan
Pertanyaan yang sering muncul dalam konsultasi tentang topik artikel ini.
- Apa itu task di Ada?
- Task adalah satuan dasar pemrosesan konkuren di Ada. Mirip thread, tetapi tidak selalu berpasangan satu-satu dengan thread OS; runtime Ada yang mengelola penjadwalannya. Task mulai dieksekusi secara otomatis begitu dideklarasikan, dan ketika prosedur utama berakhir, penyelesaian task yang masih berjalan ditunggu secara implisit. Dengan dunia luar, task berkomunikasi secara sinkron lewat rendezvous yang dimediasi entry. Task dan rendezvous sudah tertanam di spesifikasi bahasa sejak Ada 83 (1983).
- Apa bedanya protected object Ada dengan mutex?
- Protected object adalah mekanisme mutual exclusion yang dikelola bahasa: Anda tidak perlu menulis lock dan unlock secara manual. Function bersifat baca-saja dan dapat dipanggil oleh beberapa task sekaligus; procedure untuk baca-tulis, dan selama dieksekusi panggilan lain diblokir; entry menahan pemanggil di antrean sampai kondisi barrier menjadi true. Pengendalian bounded buffer yang di C ditulis dengan kombinasi mutex dan condition variable pthread, di Ada terangkum dalam satu baris barrier seperti «when Count < Buffer_Size».
- Bagaimana mekanisme rendezvous di Ada?
- Rendezvous adalah mekanisme komunikasi sinkron antar-task: pemanggilan entry di sisi pemanggil dan pernyataan accept di sisi task saling menunggu sampai keduanya mencapai titik rendezvous. Mode parameter in/out/in out memungkinkan data berpindah dua arah. Blok do…end pada body accept menjadi critical section: selama dieksekusi pemanggil diblokir, dan task tidak menerima entry lain. Digabungkan dengan pernyataan select, penantian beberapa entry, timeout, dan kondisi guard dapat ditulis secara deklaratif.
- Apa yang tidak boleh dilakukan di dalam protected object?
- Pemrosesan yang memblokir lama: delay, I/O yang memakan waktu, pemanggilan pustaka eksternal yang berat, dan sejenisnya. delay atau I/O tertentu di dalam operasi terproteksi merupakan bounded error menurut standar Ada, dan bergantung pada implementasi dapat memicu Program_Error atau deadlock, sehingga harus dihilangkan sama sekali. Aturan emasnya: perbarui state secara singkat, jalankan perhitungan berat dan I/O di luar protected object, lalu tulis kembali hanya hasilnya dalam waktu singkat.
Profil penulis
Halaman perkenalan penulis artikel.
Go Komura
Direktur KomuraSoft LLC
Berspesialisasi dalam pengembangan perangkat lunak Windows, konsultasi teknis, dan investigasi bug, terutama pada proyek dengan sistem yang sudah ada dan bug yang sulit direproduksi.