การทำงานแบบ concurrent ที่ปลอดภัยใน Ada ── คู่มือปฏิบัติเรื่อง task และ protected object

· อัปเดตเมื่อ: · · Ada, Concurrency, Tasking, Protected Objects, Rendezvous, Real Time, Parallel Programming, Programming Language, การทำงานแบบ concurrent, ความน่าเชื่อถือสูง

1. บทนำ ── concurrency ที่ฝังในภาษา

concurrency เป็นหัวข้อที่การพัฒนาซอฟต์แวร์สมัยใหม่เลี่ยงไม่ได้ แต่ในหลายภาษา concurrency เป็นสิ่งที่ «ต่อท้ายทีหลัง» พึ่งพาไลบรารีหรือฟีเจอร์ของ OS จึงต้องมีความรู้ลึกและออกแบบอย่างระมัดระวังกว่าจะใช้ถูก

Ada มีคำตอบของตัวเองต่อปัญหานี้ concurrency ฝังอยู่ในข้อกำหนดภาษาเอง

โมเดล concurrency ของ Ada:
- task ── หน่วย concurrent ที่รันอิสระ
- rendezvous ── การสื่อสารแบบซิงโครนัสระหว่าง task
- protected object ── mutual exclusion ที่ภาษาจัดการให้
- ลำดับความสำคัญเรียลไทม์ ── ฟีเจอร์เรียลไทม์ใน Annex D

task กับ rendezvous มีมาตั้งแต่ Ada 83 ปี 1983 ส่วน protected object และฟีเจอร์เรียลไทม์ใน Annex D ถูกเพิ่มใน Ada 95 แล้ววิวัฒน์ต่อใน Ada 2005 และ 2012 จุดเด่นสูงสุดของ concurrency ใน Ada ไม่ใช่ synchronization primitive ระดับต่ำอย่าง mutex หรือ semaphore แต่คือการที่ เจตนาออกแบบเขียนเป็นโค้ดได้โดยตรง

บทความนี้ไล่ concurrency ของ Ada ทีละขั้นด้วยตัวอย่างโค้ดเชิงปฏิบัติ 8 ชุด แต่ละชุดเป็น snippet อิสระที่คอมไพล์และรันได้จริง ทดลองบนเครื่องตัวเองได้

อนึ่ง ชิ้นโค้ดในบทความนี้เผยแพร่บน GitHub เป็นชุดอ้างอิงที่จัดเป็นไฟล์ตามบท

ada-task-concurrency - komurasoft-blog-samples (GitHub)

ลองบนเครื่องตัวเอง ── คอมไพล์และรัน

เมื่อเขียนว่า «คอมไพล์และรันได้» ก็ควรโชว์ขั้นตอนนั้นก่อน

เตรียม GNAT

GNAT คือคอมไพเลอร์ Ada ของ GCC บน Linux ติดตั้งด้วย apt install gnat-13 บน Windows ใช้แพ็กเกจ MSYS2 mingw-w64-x86_64-gcc-ada หรือติดตั้งผ่าน Alire (ตัวจัดการแพ็กเกจของ Ada / SPARK)

คอมไพล์แล้วรัน

แต่ละ snippet รวมหลายหน่วยคอมไพล์ในไฟล์เดียว (ข้อกำหนดของ task, ตัว body ของ task, โพรซีเจอร์หลัก) จึงต้องแยกด้วย gnatchop แล้วค่อย gnatmake gnatchop คือเครื่องมือที่ตัดไฟล์ตามกติกาตั้งชื่อของ GNAT ที่ว่า «ชื่อหน่วย = ชื่อไฟล์»

mkdir work && cd work
gnatchop ../src/snippets/01_hello_task.ada
gnatmake hello_task_demo
./hello_task_demo

ชื่อโพรซีเจอร์หลักหลังแยก คือชื่อไฟล์รันตามนั้น

ตารางจับคู่บทกับไฟล์

เลขบทกับเลขไฟล์เหลื่อมกันหนึ่ง (บทที่ 3 คือ 01_) คู่กันดังนี้

บท ไฟล์ ไฟล์รัน เนื้อหา
บทที่ 3 01_hello_task.ada hello_task_demo รูปแบบพื้นฐานของ task
บทที่ 4 02_rendezvous_intro.ada rendezvous_demo ส่งข้อมูลสองทิศทางด้วย rendezvous
บทที่ 5 03_selective_accept.ada selective_accept_demo selective accept และ server task
บทที่ 6 04_producer_consumer.ada producer_consumer_demo producer-consumer
บทที่ 7 05_protected_counter.ada protected_counter_demo mutual exclusion ด้วย protected object
บทที่ 8 06_bounded_buffer.ada bounded_buffer_demo protected entry พร้อม barrier (bounded buffer)
บทที่ 9 07_timed_entry.ada timed_entry_demo การเรียก select พร้อม timeout
บทที่ 10 08_task_priorities.ada task_priorities_demo ลำดับความสำคัญของ task และการจัดตารางเรียลไทม์

ชิ้นโค้ดในเนื้อหาถูกตัดเฉพาะส่วนที่ต้องใช้ประกอบคำอธิบาย ตามที่เป็นจะรันไม่ได้ เมื่อจะลงมือทดลอง ให้ใช้ไฟล์ด้านบน

แผนที่ความรู้ของบทความนี้

task ของ Ada สื่อสารแบบซิงโครนัสกับภายนอกด้วย rendezvous ผ่าน entry และ accept ส่วน selective accept (คำสั่ง select) รอรับหลาย entry พร้อมเงื่อนไข guard และด้วย or terminate จึงเลี่ยง server task ที่รอต่อไปเรื่อย ๆ อันเป็นต้นเหตุ deadlock ได้ protected object คือการควบคุม exclusive ที่ภาษารับผิดชอบ ทำให้ฝั่งเรียกรอจนกว่าเงื่อนไข barrier ของ entry เป็นจริง จึงรับประกัน exclusive access ต่อข้อมูลที่ใช้ร่วมและกัน data race ในทางกลับกัน ถ้าทำงานที่ห้ามอย่าง delay ใน protected operation จะเข้าข่าย bounded error และบาง implementation อาจนำไปสู่ deadlock Priority Ceiling Protocol ตั้ง ceiling priority ให้ protected object เพื่อกัน priority inversion และความเป็นเรียลไทม์ถูกค้ำจุนบนพื้นฐานทฤษฎีของ Rate Monotonic Scheduling โปรไฟล์ Ravenscar จำกัดโมเดล task เพื่อให้วิเคราะห์ deadlock แบบ static ได้

แผนที่ความรู้ task และ protected object ของ Adaแผนภาพที่แสดงว่า task ของ Ada สื่อสารแบบซิงโครนัสด้วย rendezvous, selective accept รอรับหลาย entry โดยเลี่ยง deadlock, protected object ควบคุม exclusive ด้วย barrier เพื่อกัน data race และ Priority Ceiling Protocol กับโปรไฟล์ Ravenscar ค้ำจุนความเป็นเรียลไทม์ต้องมีต้องมีใช้ใช้ป้องกันใช้ป้องกันอาจก่อให้เกิดอาจก่อให้เกิดอาจก่อให้เกิดป้องกันต้องมีใช้ใช้แนวทางที่แนะนำสำหรับบรรเทาต้องมีอาจก่อให้เกิดกำหนดค่าด้วยบรรเทาtask ของ Ada (การประมวลผลพร้อมกัน)อ็อบเจ็กต์ป้องกัน (protected object)Ada (ภาษาโปรแกรม)รันเดวู (rendezvous)accept แบบเลือก (คำสั่ง select)เดดล็อก (deadlock)barrier (เงื่อนไข when ของ protected entry)การแข่งข้อมูล (data race)การทำงานที่ผิดพลาด (erroneous execution)ข้อผิดพลาดแบบจำกัด (bounded error)โปรโตคอลเพดานลำดับความสำคัญ (Priority Ceiling Protocol)การกลับลำดับความสำคัญ (priority inversion)ลำดับความสำคัญของ task (pragma Priority)Rate Monotonic Scheduling (RMS)โปรไฟล์ Ravenscar

ในแผนภาพ เส้นทึบหมายถึงความสัมพันธ์ที่ถือเสมอ และเส้นประหมายถึงความสัมพันธ์ที่มีเงื่อนไข (เงื่อนไขอยู่ในการอธิบายของแต่ละความสัมพันธ์ในหน้าละเอียด) รายการความสัมพันธ์ทั้งหมด (รวม 20 รายการ พร้อมหลักฐานและระดับความเชื่อมั่น) และนิยามของแนวคิดหลักรวบรวมไว้ที่ หน้าละเอียดของแผนที่ความรู้ (เป็นภาษาญี่ปุ่น) ข้อมูล: JSON-LD / Turtle

2. ทบทวน «อันตราย» ของ concurrency

ก่อนเข้าเรื่อง Ada ขอทบทวนสั้น ๆ ว่าทำไม concurrency ที่ «ปลอดภัย» จึงสำคัญ

บั๊กคลาสสิกของ concurrency มีประมาณนี้

  • data race: หลาย thread เข้าถึงตำแหน่งหน่วยความจำเดียวกันพร้อมกัน และอย่างน้อยหนึ่งฝ่ายเป็นการเขียน ผลไม่กำหนด
  • deadlock: หลาย task รอให้กันและกันจบต่อไปเรื่อย ๆ จึงไม่เดินหน้าอีกเลย
  • priority inversion: task ลำดับความสำคัญสูงรอทรัพยากรที่ task ลำดับความสำคัญต่ำถืออยู่ แล้ว task ลำดับความสำคัญกลางไป preempt task ลำดับความสำคัญต่ำ (หยุด task ที่กำลังรันแล้วสลับไปอีก task)
  • starvation: task หนึ่งไม่สามารถได้ทรัพยากรที่ต้องการได้เลยตลอดไป

โมเดล concurrency ของ Ada ให้ การป้องกันในระดับภาษา ต่อปัญหาเหล่านี้

data race        → protected object รับประกัน exclusive access
deadlock         → โมเดล rendezvous ให้การซิงค์ที่มีโครงสร้าง
priority inversion → Priority Ceiling Protocol ใช้ได้ในฐานะฟีเจอร์ของภาษา
starvation       → barrier ของ entry และนโยบายคิวช่วยควบคุม

3. พื้นฐานของ task ── หน่วยรันอิสระ

หน่วยพื้นฐานของ concurrency ใน Ada คือ task task คล้าย thread แต่ไม่จำเป็นต้องจับคู่หนึ่งต่อหนึ่งกับ OS thread Ada runtime เป็นผู้จัดตาราง

task Greeter is
   entry Start;
end Greeter;

task body Greeter is
begin
   accept Start;
   Put_Line ("Hello from a task!");
end Greeter;

โค้ดนี้ (01_hello_task.ada) มีจุดสำคัญหลายข้อ

ประกาศแล้ว task จะเริ่มรันเอง task Greeter เริ่มทำงานตอนโพรซีเจอร์ที่ครอบมันถึง begin แล้วรอคำขอ rendezvous จากฝั่งเรียกที่ accept Start;

entry คืออินเทอร์เฟซที่ task เปิดให้ภายนอก เมื่อฝั่งเรียกทำ Greeter.Start; จะซิงค์กับ accept Start; ของ task นี่คือ rendezvous

การจบของ task ถูกรอให้อัตโนมัติ เมื่อโพรซีเจอร์หลักจบ ถ้ายังมี task ที่รันอยู่ ระบบจะรอให้พวกมันจบโดยปริยาย ตรงข้ามกับแครชใน C++ ที่เกิดจากลืมเรียก std::thread::join

ตัวอย่างการรัน

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.

มีข้อควรระวังหนึ่งข้อ accept Start; ไม่มีบล็อก do ... end คือเมื่อ rendezvous สำเร็จ ทั้งคู่ถูกปล่อยทันที แล้วจากนั้นเดินแบบ concurrent ดังนั้นลำดับของสองบรรทัดท้ายไม่รับประกัน บางสภาพแวดล้อม Main: task has completed. ออกก่อน ถ้าต้องการให้ลำดับคงที่ด้วย ให้วางงานที่อยากรักษาลำดับไว้ใน do ... end ของ accept Start do ... end Start; มีเพียงบรรทัดแรกที่มาเป็นหัวเสมอ เพราะ Greeter ค้างที่ accept จนกว่าจะถูกเรียก Greeter.Start;

4. rendezvous ── การสื่อสารแบบซิงโครนัสที่ส่งข้อมูล

rendezvous ไม่ใช่แค่การซิงค์ แต่ส่งข้อมูลได้สองทิศทางด้วย

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;

ฝั่งเรียกใช้แบบนี้ (02_rendezvous_intro.ada)

Worker.Compute (3, 4, Answer);
Put_Line ("Main: result = " & Integer'Image (Answer));

จุดออกแบบสำคัญคือ โหมดพารามิเตอร์ถูกระบุชัด

  • โหมด in: ส่งค่าจากฝั่งเรียกไปยัง task
  • โหมด out: ส่งผลจาก task กลับฝั่งเรียก
  • โหมด in out: สองทิศทาง

บล็อก do ... end ในตัว body ของ accept คือ critical section ระหว่างนั้นฝั่งเรียกถูกบล็อก และ task ไม่รับ entry อื่น เมื่องานจบ ทั้งคู่เดินต่อ

เรียงตามเวลา การรอจะเป็นแบบนี้

ลำดับเวลาของ rendezvous ใน Adaฝั่งเรียกกับ task Worker ซิงค์ที่ Compute รันตัว body ของ accept แล้วทั้งคู่เดินต่อพร้อมกันtask Workerฝั่งที่เรียกtask Workerฝั่งที่เรียกบล็อกจนกว่าจะถึง acceptrendezvous สำเร็จ / ตัว body ของ accept ถูกรันที่ end Compute ทั้งคู่เดินต่อพร้อมกันเรียก Worker.Computeถึง accept Compute ... doเขียนผลลงพารามิเตอร์ out Resultงานต่องานต่อ

ฝ่ายที่ถึงก่อนจะรอ ถ้าฝั่งเรียกมาก่อน จะค้างจนกว่าจะถึง accept ถ้าฝั่ง task มาก่อน จะค้างจนกว่ามีคนเรียก ไม่ว่าใครมาก่อน ข้างใน do ... end รันเมื่อทั้งสองฝ่ายอยู่ครบเสมอ

สรุปลักษณะของ rendezvous:

ลักษณะ คำอธิบาย
ซิงค์ ฝั่งเรียกกับฝั่ง task รอจนกว่าทั้งคู่ถึงจุด rendezvous พร้อมกัน
ส่งข้อมูล พารามิเตอร์ in / out / in out ส่งค่าได้สองทิศทาง
mutual exclusion ขณะรันตัว body ของ accept entry อื่นของ task ถูกบล็อก
โครงสร้างชัด ว่า entry ไหนรับเมื่อใด เขียนชัดในตัว body ของ task

ตัวอย่างการรัน

gnatchop ../src/snippets/02_rendezvous_intro.ada
gnatmake rendezvous_demo
./rendezvous_demo
Main: calling Worker.Compute...
Main: result = 25

3 * 3 + 4 * 4 = 25 กลับมาทางพารามิเตอร์ out ช่องว่างหนึ่งตัวหลัง = เป็นสเปกของ 'Image ของชนิดจำนวนเต็ม ที่ใส่ช่องว่างหนึ่งตัวหน้าค่าที่ไม่เป็นลบ ต่างจากบทที่ 3 ตรงนี้ลำดับสองบรรทัดรับประกัน เพราะการเรียก Worker.Compute ไม่กลับจนกว่า end Compute;

5. selective accept ── รอรับหลายบริการ

server task จริงต้องรอรับหลายชนิดคำขอ ประโยค select ของ Ada ทำให้ได้ในระดับภาษา

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;

ประโยค select ในโค้ดนี้ (03_selective_accept.ada) มีหลายกิ่ง or และถูกเลือกหนึ่งใน entry ที่มีการเรียกค้าง (การเลือกขึ้นกับ implementation) ถ้ายังไม่มี entry ถูกเรียก จะรอจนกว่ามีใครเรียกสักอัน

or terminate; เป็นกิ่งพิเศษ ที่จบ task อย่างปลอดภัยเมื่อ «โพรซีเจอร์หลักจบแล้ว และไม่มีใครสามารถเรียก entry ไปยัง task นี้อีก» เป็นกลไกเฉพาะของ Ada ที่แก้ปัญหา «server task ที่รอต่อไปเรื่อย ๆ» ซึ่งเป็นต้นเหตุ deadlock

จุดแข็งของ selective accept คือเขียนเงื่อนไข guard ได้ด้วย

task ต่อไปมี ring buffer อยู่ภายใน ถ้าตัดเฉพาะส่วน select จะไม่รู้ว่า Count หรือ Head มาจากไหน จึงโชว์ตั้งแต่ส่วนประกาศต่อเนื่อง ความคิดชุดเดียวกันที่เขียนใหม่ด้วย protected object อยู่ในบทที่ 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;   -- ตำแหน่งที่จะดึงออกถัดไป
   Tail  : Integer := 0;   -- ตำแหน่งที่จะเขียนถัดไป
   Count : Integer := 0;   -- จำนวนสมาชิกปัจจุบัน
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;

กิ่งที่เงื่อนไข guard เป็นเท็จ จะหลุดจากตัวเลือก ณ ขณะนั้น ทำให้เขียนแบบประกาศได้ว่า «ถ้าบัฟเฟอร์ว่าง ให้ Get รอ ถ้าเต็ม ให้ Put รอ» การเลื่อน Head กับ Tail ด้วย mod Max คือแกนของ ring buffer และเงื่อนไข guard ยังทำหน้าที่รับประกันว่าดัชนีเหล่านั้นอยู่ในช่วงที่ใช้ได้

6. producer-consumer ── ซิงค์ด้วย rendezvous

ลองดูแพตเทิร์นคลาสสิกที่ใช้ rendezvous คือ producer-consumer

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;

ในแพตเทิร์นนี้ (04_producer_consumer.ada) ทุกครั้งที่ producer เรียก Deliver จะซิงค์กับ consumer ถ้า producer เร็วเกิน จะถูกรอจนกว่า consumer จะ accept ถ้า consumer เร็วเกิน จะรอการเรียกครั้งถัดไปของ producer นี่คือ backpressure ตามธรรมชาติ (เมื่อผู้รับตามไม่ทัน ความเร็วฝั่งส่งถูกกดลงเอง) ใน rendezvous ที่ไม่มีคิวคั่น สิ่งนี้เกิดได้โดยไม่ต้องกังวลบัฟเฟอร์ล้น

7. protected object ── mutual exclusion โดยไม่ต้อง lock

ถ้า task คือ «ตัวกระทำที่ทำงานเอง» protected object คือกลไกสำหรับ «ข้อมูลที่ใช้ร่วมแบบพาสซีฟ»

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;

กติกาสำคัญของ protected object มีดังนี้

  • function เป็นแบบอ่านอย่างเดียว หลาย task เรียก function พร้อมกันได้
  • procedure เป็นแบบอ่าน-เขียน ขณะ procedure รัน ทั้ง procedure อื่นและ function ถูกบล็อก
  • entry มี barrier ฝั่งเรียกรอในคิวจนกว่าเงื่อนไข barrier เป็นจริง

ในโค้ดนี้ (05_protected_counter.ada) worker task สามตัวเรียก Increment คนละ 1,000 ครั้ง protected object รับประกัน mutual exclusion ดังนั้นค่าเคาน์เตอร์สุดท้ายเป็น 3,000 เสมอ ไม่ต้องเขียน lock/unlock ของ mutex เอง

task type Worker (Id : Integer; Rounds : Integer);

task body Worker is
begin
   for I in 1 .. Rounds loop
      Counter.Increment;  -- protected object รับประกัน mutual exclusion
   end loop;
end Worker;

W1 : Worker (1, 1_000);
W2 : Worker (2, 1_000);
W3 : Worker (3, 1_000);

ตัวอย่างการรัน

มีปัญหาหนึ่งตรงนี้ ถ้าโพรซีเจอร์หลักอ่าน Counter.Value ทันที จะได้ค่าที่ worker ยังวนอยู่ครึ่งทาง ดังนั้นฉบับสมบูรณ์ (05_protected_counter.ada) จึงเพิ่ม procedure นับการจบ และ entry ที่รอให้ทุกคนจบ

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 และแต่ละ worker เมื่อออกจากลูปแล้วเรียก Counter.Mark_Done; โพรซีเจอร์หลักรอให้ทุกคนจบด้วย Counter.All_Done; แล้วค่อยอ่านค่า ไม่ต้องมีตัวแปรแฟลกรอแยก และไม่ต้อง sleep

gnatchop ../src/snippets/05_protected_counter.ada
gnatmake protected_counter_demo
./protected_counter_demo
Final counter value = 3000

รันกี่ครั้งก็ได้ 3000 เพราะสาม task เรียก Increment รวม 3,000 ครั้ง และ protected object รันทีละครั้งแบบ exclusive

ถ้าไม่มี protected object จะเกิดอะไร

เพื่อเข้าใจคุณค่าของ protected object ลองดูโค้ดอันตรายกรณี ไม่ป้องกัน

-- ⚠ อันตราย: จัดการตัวแปรที่ใช้ร่วมกันโดยตรง
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 ในระดับ CPU คือสามขั้น «อ่าน → บวก → เขียนกลับ» ถ้าหลาย task ทำพร้อมกัน ผลบวกของ task หนึ่งอาจตามไม่ทันการอ่านของอีก task แล้วการเพิ่มค่าหาย นอกจากนี้ยังเข้าข่าย erroneous execution (การรันที่ผิด) ตาม Ada RM 9.10 «การรันที่ผิด» เป็นศัพท์มาตรฐานที่แรงกว่า «ค่าเพี้ยน» หมายความว่า มาตรฐานไม่รับประกันอะไรเกี่ยวกับพฤติกรรมของโปรแกรมนั้นอีก การอ่านเขียนพร้อมกันบนตัวแปรที่ใช้ร่วมกันที่ไม่ได้ซิงค์ ไม่จบแค่ค่าเคาน์เตอร์สุดท้ายไม่แม่น พฤติกรรมทั้งโปรแกรมอาจเป็นผลใดก็ได้ แม้สอง task จะรันคนละ 10,000 รอบ ก็ไม่มีหลักประกันเลยว่าค่าสุดท้ายจะเป็น 20,000

ถ้าอยากเห็น «ไม่มีการรับประกัน» ด้วยตาตัวเอง ให้ทำฉบับ Bad_Worker ด้านบนแล้วรันซ้ำหลายครั้ง บันทึกค่าสุดท้ายแต่ละรอบ บทความนี้ไม่ลงค่าที่วัดได้จริง ผลของ data race เปลี่ยนตาม CPU, ตัวเลือก optimization และจังหวะตอนรัน การยกตัวเลขที่ออกในสภาพแวดล้อมหนึ่งมาเป็น «เป็นแบบนี้» จะกลายเป็นเกณฑ์ที่ผิดว่า «เพี้ยนประมาณนี้» สิ่งที่ต้องตรวจไม่ใช่ «ได้ค่าเฉพาะที่น้อยกว่า 20,000» แต่คือ ผลเปลี่ยนทุกรอบ และแม้จะได้ค่าถูกสักครั้งก็ไม่มีความหมาย

protected object คือกลไกที่ «กันปัญหานี้ด้วยไวยากรณ์» แค่เรียก Counter.Increment; คอมไพเลอร์กับ runtime ก็รับประกัน mutual exclusion ให้

8. protected entry และ barrier ── bounded buffer

เมื่อเพิ่ม entry ให้ protected object จะได้ การซิงค์แบบมีเงื่อนไข ลองดูด้วย bounded buffer คลาสสิก

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 คือ barrier barrier ถูกประเมินทุกครั้งที่มีการเรียก entry ถ้าจริงก็รัน ถ้าเท็จ task ฝั่งเรียกรอในคิว ทุกครั้งที่สถานะบัฟเฟอร์เปลี่ยน (task อื่นรัน Put หรือ Get) barrier ของ task ที่รอจะถูกประเมินใหม่

ว่าประเมินใหม่เมื่อใด ตามข้อความอย่างเดียวตามยาก ถ้า Get มาถึงบัฟเฟอร์ว่างก่อน เรียงตามเวลาจะเป็นแบบนี้

การประเมิน barrier ของ protected object ใหม่Get บนบัฟเฟอร์ว่างรอในคิว หลัง Put แล้ว barrier ถูกประเมินใหม่และ Get จึงรันtask Producerprotected object Buftask Consumertask Producerprotected object Buftask Consumerรอในคิว entry ของ Getตอน protected operation จบ จะประเมิน barrier ของ entry ที่รอใหม่เรียก Getประเมินว่า barrier Count มากกว่า 0 หรือไม่ → เท็จเรียก Putประเมินว่า barrier Count น้อยกว่า Buffer_Size หรือไม่ → จริงรันตัว body ของ Put / Count เป็น 1barrier ของ Get กลายเป็นจริงรันตัว body ของ Get แล้วปล่อย Consumer

จุดสำคัญคือ การประเมิน barrier ใหม่ถูกทำรวมตอนท้ายของ protected operation ในช่วงตั้งแต่ตัว body ของ Put จบ จน lock ของ Buf ถูกปล่อย barrier ของ entry ที่รอจะถูกประเมิน และอันที่เป็นจริงจะถูกรันต่อเลย ไม่มีรูปแบบพลาดแบบ condition variable ของ C ที่ว่า «ถ้าใครลืมเรียก signal จะไม่ตื่นอีกเลย»

แพตเทิร์นนี้ (06_bounded_buffer.ada) คือจังหวะที่ protected object ของ Ada โดดเด่นที่สุดอย่างหนึ่ง ลองเทียบกับกรณีเขียนด้วย mutex + condition variable ของ pthread ใน C

// กรณี C + pthread (สำหรับเทียบกับ Ada)
pthread_mutex_lock(&mutex);
while (count >= BUFFER_SIZE) {           // เทียบเท่า when ของ Ada
    pthread_cond_wait(&not_full, &mutex); // รอ barrier
}
data[tail] = item;
tail = (tail + 1) % BUFFER_SIZE;
count++;
pthread_cond_signal(&not_empty);         // แจ้ง task ที่รอ
pthread_mutex_unlock(&mutex);

ใน Ada ทั้งหมดนี้ยุบเหลือบรรทัด when Count < Buffer_Size เงื่อนไข while, การส่งสัญญาณ, จังหวะปลด lock ที่พลาดได้ — โอกาสบั๊กพวกนี้หายไป

9. การเรียกพร้อม timeout ── ไม่รอตลอดไป

ในระบบเรียลไทม์ «รอตลอดไป» ไม่ได้รับอนุญาต Ada รองรับ timeout ด้วยไวยากรณ์ 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;

ในโค้ดนี้ (07_timed_entry.ada) Slow_Worker ยังอยู่ระหว่าง delay 2.0 และยังไม่ถึง accept ดังนั้นการเรียก entry ที่เข้าคิวจะ timeout ที่ 500ms (timeout มีผลกับเวลารอในคิวก่อนที่ rendezvous จะถูกรับ ไม่ได้ตัดการรันตัว body ของ rendezvous เอง) delay until ระบุเป็นเวลาสัมบูรณ์ เป็นเทคนิคพื้นฐานของโปรแกรมมิงเรียลไทม์ที่กัน cumulative drift

Ada ยังรองรับ conditional entry call ด้วย

select
   Server.Process (Item);
else
   Put_Line ("Server is busy, will retry later");
end select;

กิ่ง else ทำให้ถ้า rendezvous ทันทีไม่ได้ จะไปงานทางเลือกทันที ไม่ต้องเขียน polling เอง

อย่าลืมออกแบบหลัง timeout

timeout สะดวก แต่แกนของการออกแบบคือ «จะทำอะไรหลังจากรอไม่ได้» ค่าทิ้งได้จริงหรือ ควรลองใหม่หรือ ควรแจ้งเป็นข้อผิดพลาดขึ้นข้างบนหรือ — ถ้าคลุมเครือ สิ่งนี้จะกลายเป็นข้อมูลขาดหรือบริการหยุดในโปรดักชัน เมื่อเขียน timeout ให้ออกแบบความรับผิดชอบหลัง timeout ในที่เดียวกันด้วย

periodic task และ delay until

delay until ไม่ได้ใช้แค่ timeout แต่ใช้กับ การรันเป็นคาบ ได้ด้วย delay 0.1 ธรรมดาจะทำให้คาบกลายเป็น «เวลาประมวลผล + 0.1 วินาที» ส่วน delay until กำหนดจุดปลุกครั้งถัดไปด้วยเวลาสัมบูรณ์ จึงรักษาคาบให้เสถียรโดยไม่ขึ้นกับเวลาประมวลผล

loop
   Next := Next + Period;
   Do_Work;
   delay until Next;
end loop;

แพตเทิร์นนี้ใช้ได้ทุกที่ที่ต้องการงานคาบคงที่ เช่น เฝ้าเซ็นเซอร์และลูปควบคุม

10. ลำดับความสำคัญของ task และการจัดตารางเรียลไทม์

ฟีเจอร์เรียลไทม์ของ Ada ถูกกำหนดใน Annex D (Real-Time Systems) ถ้า implementation ของ Ada รองรับ Annex D จะระบุลำดับความสำคัญของ task และนโยบายการจัดตารางได้

ตรวจว่าใช้ได้ในสภาพแวดล้อมตัวเองหรือไม่

Annex D เป็นหนึ่งใน Specialized Needs Annex (ภาคผนวกสำหรับโดเมนเฉพาะ) การรองรับขึ้นกับ implementation และสภาพแวดล้อมรัน ว่าใช้ได้บนเครื่องตัวเองหรือไม่ ให้แยกเป็นสามขั้น

1. ดูช่วงของลำดับความสำคัญ

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;

ช่วงและค่าเริ่มต้นของ System.Priority ขึ้นกับ implementation จึงไม่ลงตัวเลขเฉพาะที่นี่ ถ้าช่วงที่แสดงกว้างพอ สภาพแวดล้อมนั้นการระบุอย่าง pragma Priority (System.Default_Priority + 5) จึงมีความหมาย

2. ดูว่าการระบุนโยบายผ่านการคอมไพล์หรือไม่

pragma Task_Dispatching_Policy (FIFO_Within_Priorities);
pragma Locking_Policy (Ceiling_Locking);

ถ้าใส่แล้วคอมไพล์ผ่าน อย่างน้อยในระดับไวยากรณ์ถูกรับแล้ว

3. ดูว่าลำดับความสำคัญทำงานตามจริงหรือไม่

นี่คือกับดักใหญ่ที่สุด การคอมไพล์ผ่าน กับการที่ scheduler ของ OS จัดตามลำดับความสำคัญจริง เป็นคนละเรื่อง บน OS ทั่วไปอย่าง Linux หรือ Windows การสะท้อนลำดับความสำคัญเรียลไทม์เข้า scheduler จริง อาจต้องตั้งสิทธิ์ฝั่ง OS README ของชุดตัวอย่างในบทความนี้ก็มีหมายเหตุว่า ในสภาพแวดล้อมที่ Annex D รองรับไม่ครบ 08_task_priorities.ada จะทำงานเป็น task ปกติ

กล่าวคือ แม้ลำดับความสำคัญจะไม่เกิดผล โปรแกรมก็ยังรันได้ ในงานที่ต้องการ hard real-time นอกจากออกแบบลำดับความสำคัญบนกระดาษ ต้องมีขั้นวัดลำดับจริงบนสภาพแวดล้อมเป้าหมายด้วย

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;

ตั้งค่าขั้นสูงกว่านั้น ระบุนโยบายการจัดตารางและ Priority Ceiling Protocol ได้ด้วย

pragma Task_Dispatching_Policy (FIFO_Within_Priorities);
pragma Locking_Policy (Ceiling_Locking);

Priority Ceiling Protocol คือโปรโตคอลที่กัน priority inversion แต่ละ protected object ตั้ง ceiling priority ชัดด้วย pragma Priority (หรือ Priority aspect) ถ้า active priority ของ task ฝั่งเรียกเกิน ceiling นั้น จะเกิด Program_Error ขณะล็อก object จะรันที่ ceiling priority จึงกันการ preempt จาก task ลำดับความสำคัญกลาง

protected Shared_Data is
   pragma Priority (15);  -- ceiling priority
   procedure Update (Val : Integer);
   function Read return Integer;
private
   Data : Integer := 0;
end Shared_Data;

ฟีเจอร์เหล่านี้ตั้งบนพื้นทฤษฎีของ Rate Monotonic Scheduling (RMS) และมีผลงานจริงในระบบ hard real-time อย่างระบบควบคุมการบินและอุปกรณ์การแพทย์ RMS เป็นวิธีลำดับความสำคัญคงที่ที่ว่า «task ที่คาบสั้นกว่าได้ลำดับความสำคัญสูงกว่า» จุดที่ถูกให้ความสำคัญในงาน hard real-time คือวิเคราะห์ก่อนรันได้ว่าชุด periodic task จะรักษา deadline ได้หรือไม่

11. แนวทางออกแบบสำหรับงานจริง

จนถึงตรงนี้เราดูไวยากรณ์พื้นฐานของ task และ protected object แล้ว สุดท้ายขอจัดแนวทางออกแบบที่ควรมีในหัวเมื่อใช้ concurrency ของ Ada ในงานจริง

สิ่งที่ห้ามทำใน protected object

กฎเหล็กใน protected object คือ อัปเดตสถานะสั้น ๆ แล้วย้ายงานหนักออกไปทำข้างนอก ปฏิบัติการบน protected object ถูกควบคุมแบบ mutual exclusion ภายใน ถ้าบล็อกนานข้างใน จะหยุด task อื่นทั้งหมดที่ใช้ protected object เดียวกัน

งานที่ควรเลี่ยง:

  • delay และ I/O ที่ใช้เวลานาน
  • การเรียกซับซ้อนไปยัง protected object อื่น
  • การเรียกไลบรารีภายนอกที่หนัก

อนึ่ง delay หรือ I/O บางชนิดใน protected operation ไม่ใช่แค่ปัญหาประสิทธิภาพ แต่เป็น bounded error ตามมาตรฐาน Ada bounded error คือชนิดข้อผิดพลาดที่ «ช่วงผลที่เป็นไปได้มาตรฐานกำหนด แต่จะตกที่ใดในนั้นไม่กำหนด» ไม่ไร้ขอบเขตเท่า erroneous execution แต่ก็ไม่มีการรับประกันว่าจะทำงานถูก ในทางปฏิบัติบาง implementation อาจเกิด Program_Error หรือเข้าสู่ deadlock ดังนั้นไม่ใช่เรื่อง «ใช้ให้น้อย» แต่ต้องตัดออกให้หมด

การออกแบบที่ดีคือแพตเทิร์น «ดึงค่าที่ต้องใช้จาก protected object ในเวลาสั้น → ทำคำนวณหนักหรือ I/O ข้างนอก → เขียนเฉพาะผลกลับเข้า protected object ในเวลาสั้น»

รักษาเงื่อนไข barrier ให้เรียบง่าย

barrier ของ entry ... when <condition> ทรงพลัง แต่ถ้าซับซ้อนเกินจะอ่านยาก และยากที่จะไล่ว่าทำไม task จึงไม่ถูกปล่อย

ระดับที่อ่านแล้วรู้ความหมายของสถานะทันทีอย่าง when Count < Buffer_Size หรือ when Used > 0 คืออุดมคติ ถ้าต้องมีหลายเงื่อนไข ให้พิจารณาแทนสถานะด้วยชนิด enumeration แล้วทำให้ barrier อ่านเป็นชื่อสถานะอย่าง when State = Running

exception ของ task และการหยุด

นโยบายเมื่อเกิด exception ใน task ต้องตัดสินไว้ชัด อย่างน้อยจับ exception ที่ชั้นบนสุดของตัว body ของ task แล้วบันทึกว่าเกิดอะไร

สำคัญกว่านั้นคือการออกแบบ หลัง exception ถ้า task นั้นหยุด ระบบเดินต่อได้หรือ รีสตาร์ตได้หรือ แจ้ง task อื่นอย่างไร คืนสถานะที่ใช้ร่วมกันสู่สภาพปลอดภัยอย่างไร — ต้องตอบคำถามพวกนี้ได้ Ada มีกลไก exception ในฐานะฟีเจอร์ของภาษา แต่ความปลอดภัยหลัง exception เป็นความรับผิดชอบของการออกแบบแอป

มินิเช็กลิสต์ออกแบบ

มุมมอง สิ่งที่ต้องตรวจ
สถานะที่ใช้ร่วม ถูกเก็บไว้ใน protected object หรือไม่ ไม่ถูกแตะจากภายนอกโดยตรงหรือไม่
protected operation สั้นหรือไม่ ไม่บล็อกข้างในหรือไม่
entry barrier เรียบง่ายหรือไม่ มีโอกาสรอต่อไปเรื่อย ๆ หรือไม่ มีนโยบาย timeout หรือไม่
อายุของ task เงื่อนไขจบชัดหรือไม่ มีนโยบายตอนเกิด exception หรือไม่
งานเป็นคาบ พิจารณา delay until แทน delay หรือยัง

ใน concurrency คำว่า «น่าจะพอ» อันตรายที่สุด การทำให้สถานะที่ใช้ร่วม เงื่อนไขรอ เงื่อนไขจบ และนโยบาย exception ปรากฏชัดในโค้ด คือก้าวแรกของ concurrency ที่ปลอดภัย

12. สรุป ── ภาษาที่ทำให้ concurrency เป็น «ไวยากรณ์»

สิ่งที่ทำให้โมเดล concurrency ของ Ada ต่างจากภาษาอื่นคือ concurrency ที่ปลอดภัยไม่ใช่ «best practice ที่ต่อท้ายทีหลัง» แต่ฝังเป็น «ไวยากรณ์»

สิ่งที่อยากทำ ไวยากรณ์ของ Ada
หน่วยรันอิสระ task / task body
สื่อสารแบบซิงโครนัส entry / accept
รอรับหลายคำขอ select / or / else
mutual exclusion protected / function / procedure
ซิงค์แบบมีเงื่อนไข entry ... when <barrier>
timeout or delay until <time>
ควบคุมลำดับความสำคัญ pragma Priority

โครงสร้างเหล่านี้เป็นเป้าที่คอมไพเลอร์ตรวจ ตัวอย่างเช่น ถ้าใน function ของ protected object พยายามเขียนทับ private component ของตัวมันเอง จะเป็น compile error เมื่อ protected operation จบ barrier ของ entry ที่รอจะถูกประเมินใหม่อัตโนมัติ — ไม่ต้องส่งสัญญาณเอง

«เช่นเดียวกับที่ระบบชนิดรับประกันความปลอดภัยของหน่วยความจำ
  ไวยากรณ์ concurrency ของ Ada รับประกันความปลอดภัยของการซิงค์»

ตัวอย่างโค้ด 8 ชุดในบทความนี้เป็นบทนำเชิงปฏิบัติเรื่อง task, rendezvous, protected object และฟีเจอร์เรียลไทม์ ลองรันบนเครื่องตัวเอง แล้วต่อด้วยหัวข้อขั้นสูงเหล่านี้

  • โปรไฟล์ Ravenscar: โปรไฟล์จำกัด task สำหรับระบบเรียลไทม์ความน่าเชื่อถือสูง โมเดล task ที่ถูกจำกัดทำให้วิเคราะห์ deadlock แบบ static ได้
  • บล็อกขนานของ Ada 2022: ประมวลผลแบบ data-parallel ด้วยไวยากรณ์ parallel ... do
  • การรวมกับ SPARK: formal verification พฤติกรรมของโปรแกรม concurrent (GNATprove รองรับภายใต้โปรไฟล์ Ravenscar)

ถึงอย่างนั้น «ใช้ Ada» ก็ไม่ได้แปลว่าปลอดภัย

ข้อควรระวังสำคัญท้ายบท ไวยากรณ์ concurrency ของ Ada ทรงพลัง แต่ใช้ Ada แล้วไม่ได้ปลอดภัยอัตโนมัติ แตะข้อมูลที่ใช้ร่วมโดยไม่ใส่ใน protected object บล็อกนานใน protected object หลาย protected object เรียกกันอย่างซับซ้อน — ความผิดพลาดออกแบบพวกนี้เกิดใน Ada ได้เช่นกัน

ฟีเจอร์ภาษาถูกออกแบบให้ «การเขียนแบบอันตรายต้องใช้ความพยายามโดยเจตนา» แต่ไม่ได้มาแทนการออกแบบที่ถูกต้อง Ada มีค่าจริงตรงที่ดึงการถกเรื่องความปลอดภัยเข้าใกล้โค้ด — คำถามอย่าง «สถานะนี้ถูกป้องกันหรือไม่» «task นี้จบเมื่อใด» «entry นี้รอด้วยเงื่อนไขใด» ทิ้งไว้เป็นไวยากรณ์บนโค้ดได้

ปรัชญาของ Ada ที่พูดการออกแบบด้วยชนิดข้อมูล สอดคล้องใน concurrency ด้วย concurrency ที่ปลอดภัยไม่ได้เริ่มจากการจับ lock อย่างระมัดระวัง แต่เริ่มจากการไม่ปล่อยให้สถานะที่ใช้ร่วมที่อันตรายอยู่โดยไม่มีการป้องกัน

ต่อความเชื่อทั่วไปที่ว่า «concurrency ยาก» Ada ตอบว่า «ถ้าเลือกไวยากรณ์ถูก ความปลอดภัยคอมไพเลอร์รับประกันให้» ปรัชญาออกแบบนั้นใกล้กับ Rust หรือ Pony สมัยนี้ แต่ Ada ถือมันเป็นข้อกำหนดภาษามา 40 ปีแล้ว

บทความล่าสุดที่มีแท็กเดียวกัน เพื่อเจาะลึกหัวข้อใกล้เคียง

โปรแกรมมิงระบบเรียลไทม์ด้วย Ada — ลำดับความสำคัญ คาบ และการควบคุมเวลาประมวลผลในทางปฏิบัติ

เรียนรู้ Annex D (ระบบเรียลไทม์) ของ Ada ผ่านตัวอย่างโค้ดเชิงปฏิบัติ 8 ชุด จัดเรียงทีละขั้นตั้งแต่ลำดับความสำคัญของ task, Ceiling_Locking...

เชิงลึกของการจำลองเสมือนบน Windows (ตอนที่ 3) — เครื่องเสมือนที่บูตในไม่กี่วินาที: ทำไม WSL2, Windows Sandbox และคอนเทนเนอร์จึงเบา

ทำไม WSL2 และ Windows Sandbox จึงเริ่มในไม่กี่วินาทีและรู้สึกเบา? บทความนี้อธิบายกลไก ตั้งแต่ dynamic base image และ direct map ไปจนถึงกา...

เชิงลึกของการจำลองเสมือนบน Windows (ตอนที่ 2) — หน่วยความจำที่แม้เคอร์เนลก็มองไม่เห็น: กลไกของ VBS, HVCI และ Credential Guard

เมื่อติดตั้งใหม่บนฮาร์ดแวร์ที่รองรับ VBS จะเปิดตามค่าเริ่มต้น และใช้ hypervisor กับ SLAT สร้างการแยกที่แข็งกว่าเคอร์เนล บทความนี้อธิบายโค...

เชิงลึกของการจำลองเสมือนบน Windows (ตอนที่ 1) — Windows ของคุณรันอยู่ที่ไหนจริง ๆ? Hypervisor และพาร์ติชัน

เมื่อเปิด Hyper-V แล้ว Windows ฝั่งโฮสต์เองก็รันบน hypervisor ในฐานะ root partition บทความนี้อธิบายรากฐานของการจำลองเสมือนผ่านบทบาทของ VT...

หน้าเหล่านี้วางหัวข้อไว้ในบริบทที่กว้างขึ้นของบริการและการตัดสินใจ

คำถามที่พบบ่อย

คำถามที่มักพบในการปรึกษาเกี่ยวกับหัวข้อของบทความนี้

task ใน Ada คืออะไร?
task คือหน่วยพื้นฐานของ concurrency ใน Ada คล้าย thread แต่ไม่จำเป็นต้องจับคู่หนึ่งต่อหนึ่งกับ OS thread เพราะ Ada runtime เป็นผู้จัดตาราง ประกาศแล้ว task จะเริ่มรันเอง และเมื่อโพรซีเจอร์หลักจบ ระบบจะรอให้ task ที่ยังรันอยู่จบโดยปริยาย สื่อสารแบบซิงโครนัสกับภายนอกผ่าน rendezvous ที่ใช้ entry ส่วน task และ rendezvous ฝังอยู่ในข้อกำหนดภาษาตั้งแต่ Ada 83 ปี 1983
protected object ของ Ada ต่างจาก mutex อย่างไร?
protected object คือกลไก mutual exclusion ที่ภาษาจัดการให้ ไม่ต้องเขียน lock/unlock เอง function เป็นแบบอ่านอย่างเดียว หลาย task เรียกพร้อมกันได้ procedure เป็นแบบอ่าน-เขียน ขณะรันการเรียกอื่นถูกบล็อก และ entry จะให้ฝั่งเรียกรอในคิวจนกว่าเงื่อนไข barrier เป็นจริง การควบคุม bounded buffer ที่ใน C ต้องผสม mutex กับ condition variable ของ pthread ใน Ada ยุบเหลือ barrier บรรทัดเดียวอย่าง «when Count < Buffer_Size»
rendezvous ใน Ada ทำงานอย่างไร?
rendezvous คือกลไกสื่อสารแบบซิงโครนัสระหว่าง task ฝั่งเรียกที่เรียก entry และประโยค accept ฝั่ง task จะรอกันจนกว่าทั้งคู่ถึงจุด rendezvous พร้อมกัน ส่งข้อมูลได้สองทิศทางด้วยโหมดพารามิเตอร์ in/out/in out บล็อก do…end ในตัว body ของ accept คือ critical section ระหว่างนั้นฝั่งเรียกถูกบล็อก และ task ไม่รับ entry อื่น ถ้าใช้ร่วมกับประโยค select จะเขียนการรอหลาย entry, timeout และเงื่อนไข guard แบบประกาศได้
ใน protected object ห้ามทำอะไร?
งานที่บล็อกนาน เช่น delay, I/O ที่ใช้เวลานาน และการเรียกไลบรารีภายนอกที่หนัก delay หรือ I/O บางชนิดใน protected operation ถือเป็น bounded error ตามมาตรฐาน Ada และบาง implementation อาจเกิด Program_Error หรือ deadlock จึงต้องตัดออกให้หมด กฎเหล็กคืออัปเดตสถานะสั้น ๆ แล้วย้ายงานคำนวณหนักและ I/O ออกนอก protected object แล้วเขียนเฉพาะผลกลับในเวลาสั้น

โปรไฟล์ผู้เขียน

หน้าแนะนำผู้เขียนบทความ

Go Komura

ผู้แทนของ KomuraSoft LLC

เชี่ยวชาญด้านการพัฒนาซอฟต์แวร์ Windows ที่ปรึกษาเทคนิค และการตรวจสอบบั๊ก โดยเฉพาะโปรเจกต์ที่มีระบบเดิมและบั๊กที่ทำซ้ำได้ยาก

ลิงก์สาธารณะ

กลับไปยังบล็อก