การทำงานแบบ concurrent ที่ปลอดภัยใน Ada ── คู่มือปฏิบัติเรื่อง task และ protected object
· อัปเดตเมื่อ: · Go Komura · 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 ได้
flowchart LR
accTitle: แผนที่ความรู้ task และ protected object ของ Ada
accDescr: แผนภาพที่แสดงว่า task ของ Ada สื่อสารแบบซิงโครนัสด้วย rendezvous, selective accept รอรับหลาย entry โดยเลี่ยง deadlock, protected object ควบคุม exclusive ด้วย barrier เพื่อกัน data race และ Priority Ceiling Protocol กับโปรไฟล์ Ravenscar ค้ำจุนความเป็นเรียลไทม์
ada_task["task ของ Ada (การประมวลผลพร้อมกัน)"]
protected_object["อ็อบเจ็กต์ป้องกัน (protected object)"]
ada["Ada (ภาษาโปรแกรม)"]
rendezvous["รันเดวู (rendezvous)"]
selective_accept["accept แบบเลือก (คำสั่ง select)"]
deadlock["เดดล็อก (deadlock)"]
barrier["barrier (เงื่อนไข when ของ protected entry)"]
data_race["การแข่งข้อมูล (data race)"]
erroneous_execution["การทำงานที่ผิดพลาด (erroneous execution)"]
bounded_error["ข้อผิดพลาดแบบจำกัด (bounded error)"]
priority_ceiling_protocol["โปรโตคอลเพดานลำดับความสำคัญ (Priority Ceiling Protocol)"]
priority_inversion["การกลับลำดับความสำคัญ (priority inversion)"]
task_priority["ลำดับความสำคัญของ task (pragma Priority)"]
rate_monotonic_scheduling["Rate Monotonic Scheduling (RMS)"]
ravenscar_profile["โปรไฟล์ Ravenscar"]
ada_task -->|"ต้องมี"| ada
protected_object -->|"ต้องมี"| ada
ada_task -->|"ใช้"| rendezvous
selective_accept -->|"ใช้"| rendezvous
selective_accept -.->|"ป้องกัน"| deadlock
protected_object -.->|"ใช้"| barrier
protected_object -->|"ป้องกัน"| data_race
data_race -->|"อาจก่อให้เกิด"| erroneous_execution
bounded_error -.->|"อาจก่อให้เกิด"| deadlock
protected_object -.->|"อาจก่อให้เกิด"| bounded_error
priority_ceiling_protocol -->|"ป้องกัน"| priority_inversion
priority_ceiling_protocol -->|"ต้องมี"| protected_object
priority_ceiling_protocol -->|"ใช้"| task_priority
rate_monotonic_scheduling -->|"ใช้"| task_priority
rate_monotonic_scheduling -.->|"แนวทางที่แนะนำสำหรับ"| ada_task
ravenscar_profile -.->|"บรรเทา"| deadlock
ravenscar_profile -->|"ต้องมี"| ada_task
ada_task -.->|"อาจก่อให้เกิด"| priority_inversion
ada_task -.->|"กำหนดค่าด้วย"| task_priority
rendezvous -.->|"บรรเทา"| deadlock
ในแผนภาพ เส้นทึบหมายถึงความสัมพันธ์ที่ถือเสมอ และเส้นประหมายถึงความสัมพันธ์ที่มีเงื่อนไข (เงื่อนไขอยู่ในการอธิบายของแต่ละความสัมพันธ์ในหน้าละเอียด) รายการความสัมพันธ์ทั้งหมด (รวม 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 อื่น เมื่องานจบ ทั้งคู่เดินต่อ
เรียงตามเวลา การรอจะเป็นแบบนี้
sequenceDiagram
accTitle: ลำดับเวลาของ rendezvous ใน Ada
accDescr: ฝั่งเรียกกับ task Worker ซิงค์ที่ Compute รันตัว body ของ accept แล้วทั้งคู่เดินต่อพร้อมกัน
participant Main as ฝั่งที่เรียก
participant W as task Worker
Main->>W: เรียก Worker.Compute
Note over Main: บล็อกจนกว่าจะถึง accept
W->>W: ถึง accept Compute ... do
Note over Main,W: rendezvous สำเร็จ / ตัว body ของ accept ถูกรัน
W-->>Main: เขียนผลลงพารามิเตอร์ out Result
Note over Main,W: ที่ end Compute ทั้งคู่เดินต่อพร้อมกัน
Main->>Main: งานต่อ
W->>W: งานต่อ
ฝ่ายที่ถึงก่อนจะรอ ถ้าฝั่งเรียกมาก่อน จะค้างจนกว่าจะถึง 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 มาถึงบัฟเฟอร์ว่างก่อน เรียงตามเวลาจะเป็นแบบนี้
sequenceDiagram
accTitle: การประเมิน barrier ของ protected object ใหม่
accDescr: Get บนบัฟเฟอร์ว่างรอในคิว หลัง Put แล้ว barrier ถูกประเมินใหม่และ Get จึงรัน
participant C as task Consumer
participant B as protected object Buf
participant P as task Producer
C->>B: เรียก Get
B->>B: ประเมินว่า barrier Count มากกว่า 0 หรือไม่ → เท็จ
Note over C: รอในคิว entry ของ Get
P->>B: เรียก Put
B->>B: ประเมินว่า barrier Count น้อยกว่า Buffer_Size หรือไม่ → จริง
B->>B: รันตัว body ของ Put / Count เป็น 1
Note over B: ตอน protected operation จบ จะประเมิน barrier ของ entry ที่รอใหม่
B->>B: barrier ของ Get กลายเป็นจริง
B-->>C: รันตัว 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(¬_full, &mutex); // รอ barrier
}
data[tail] = item;
tail = (tail + 1) % BUFFER_SIZE;
count++;
pthread_cond_signal(¬_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 ปีแล้ว
บทความที่เกี่ยวข้อง
บทความล่าสุดที่มีแท็กเดียวกัน เพื่อเจาะลึกหัวข้อใกล้เคียง
Generic programming ใน Ada — เขียนสัญญาด้วยชนิดข้อมูล เพื่อนำกลับมาใช้ใหม่แบบ zero-cost
อธิบาย generic programming ของ Ada อย่างเป็นระบบ ตั้งแต่ generic subprogram, generic package, formal subprogram, type category ไปจนถึงแนว...
โปรแกรมมิงระบบเรียลไทม์ด้วย 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...
หัวข้อที่เกี่ยวข้อง
หน้าเหล่านี้วางหัวข้อไว้ในบริบทที่กว้างขึ้นของบริการและการตัดสินใจ
หัวข้อเทคนิคของ Windows
ประตูสู่การพัฒนา Windows การตรวจสอบบั๊ก และการใช้ประโยชน์จากสินทรัพย์เดิม
คำถามที่พบบ่อย
คำถามที่มักพบในการปรึกษาเกี่ยวกับหัวข้อของบทความนี้
- 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 แล้วเขียนเฉพาะผลกลับในเวลาสั้น