Pemrosesan konkuren yang aman di Ada ── panduan praktis task dan protected object

· Diperbarui pada: · · 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.

Peta pengetahuan task dan protected object AdaDiagram 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-timemensyaratkanmensyaratkanmenggunakanmenggunakanmencegahmenggunakanmencegahdapat menyebabkandapat menyebabkandapat menyebabkanmencegahmensyaratkanmenggunakanmenggunakandisarankan untukmengurangimensyaratkandapat menyebabkandikonfigurasi denganmengurangitask Ada (pemrosesan concurrent)objek terlindung (protected object)Ada (bahasa pemrograman)rendezvousselective accept (pernyataan select)deadlockbarrier (kondisi when pada protected entry)data raceeksekusi salah (erroneous execution)kesalahan terbatas (bounded error)protokol plafon prioritas (Priority Ceiling Protocol)pembalikan prioritas (priority inversion)prioritas task (pragma Priority)Rate Monotonic Scheduling (RMS)profil Ravenscar

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.

Urutan rendezvous AdaPemanggil dan task Worker tersinkronisasi pada Compute, menjalankan body accept, lalu keduanya dilanjutkan bersama.Task WorkerPemanggilTask WorkerPemanggilDiblokir sampai mencapai acceptRendezvous terbentuk / body accept dijalankanPada end Compute keduanya dilanjutkan bersamaMemanggil Worker.ComputeMencapai accept Compute ... doMenulis hasil ke parameter out ResultPemrosesan lanjutanPemrosesan 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.

Evaluasi ulang barrier pada protected objectGet pada buffer kosong menunggu di antrean; setelah Put, barrier dievaluasi ulang dan Get dijalankan.Task ProducerProtected object BufTask ConsumerTask ProducerProtected object BufTask ConsumerMenunggu di antrean entry GetPada akhir operasi terproteksi, barrier entry yang menunggu dievaluasi ulangMemanggil GetMengevaluasi apakah barrier Count lebih besar dari 0 → falseMemanggil PutMengevaluasi apakah barrier Count kurang dari Buffer_Size → trueMenjalankan body Put / Count menjadi 1Barrier Get menjadi trueMenjalankan 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(&not_full, &mutex); // menunggu barrier
}
data[tail] = item;
tail = (tail + 1) % BUFFER_SIZE;
count++;
pthread_cond_signal(&not_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:

  • delay atau 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 terbaru dengan tag yang sama untuk mendalami topik-topik terdekat.

Halaman-halaman ini menempatkan topik dalam konteks layanan dan keputusan yang lebih luas.

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.

Kembali ke blog