Fork/join: đợi kết quả, thu dọn nhánh và giữ đúng phạm vi hủy

Khoảng 45 phútMức 2/3Bản nháp: một số khẳng định chưa đối chiếu spec

Sau bài này bạn sẽ

  • Tính thời điểm parent tiếp tục ở join/join_any/join_none
  • Phân biệt scope wait fork và disable fork theo cây process
  • Kiểm cancellation không hủy nhầm sentinel hoặc để loser sống

Parent chạy tiếp không có nghĩa công việc đã xong

Testbench chạy một tác vụ kiểm và một timeout song song. Khi tác vụ kiểm xong trước, parent muốn tiếp tục; timeout vẫn có thể sống và báo lỗi sau đó nếu không được thu dọn. Ngược lại, nếu parent hủy quá rộng, nó có thể dừng cả monitor đáng lẽ phải tiếp tục quan sát.

Ba cách join quyết định parent chờ ai; wait fork và disable fork quyết định xử lý những child còn sống. Hãy vẽ cây process trước khi chọn lệnh. Dấu begin/end trong source, quan hệ cha/con của process và thời điểm child kết thúc là ba thông tin khác nhau cần theo dõi.

Mỗi statement trong fork là một child

Trong fork #2 a=1; #5 b=1; join, hai delay-control statement là hai nhánh. Nếu một nhánh cần nhiều statement chạy nối tiếp, bọc chúng bằng begin/end để chúng thuộc một child. Các delay trong cùng begin/end cộng nối tiếp; delay ở hai child khác nhau bắt đầu theo sự kiện của từng nhánh.

Không lấy số dòng hoặc thụt lề làm số process. Một task call đặt trực tiếp trong fork là một nhánh; bên trong task có thể tạo tiếp descendants. Main dùng các statement ngắn và delay dương để việc tính expected không phụ thuộc thứ tự các child cùng được đánh thức.

[1]

Join đợi mọi child của nhóm hoàn tất

Giai đoạn đầu của main bắt đầu ở 0 ns. Child a hoàn tất sau 2 ns, child b sau 5 ns; parent qua join ở 5 ns và phải thấy cả a/b bằng 1. Thời gian chờ là nhánh chậm nhất, không phải 2+5=7 ns.

Đếm max chỉ đúng khi các nhánh thực sự độc lập như ví dụ. Nếu một child đang đợi mailbox mà không có producer, nó có thể không bao giờ xong; “join đợi tất cả” không cung cấp watchdog hoặc tự giải deadlock. Vì vậy main có watchdog riêng và chỉ in summary khi mọi oracle đã được thực hiện.

Join_any đợi nhánh đầu tiên, không hủy nhánh còn lại

Giai đoạn tiếp theo bắt đầu ở 5 ns, tạo c với delay 2 và d với delay 5. Join_any cho parent chạy tiếp ở 7 ns: c đã 1, d vẫn 0. Nhánh d còn sống và sẽ hoàn tất ở 10 ns nếu không bị hủy.

Parent ra khỏi join_any không tự chọn “giao dịch thành công”: child kết thúc đầu có thể là timeout. Code thực tế cần lưu rõ winner/result rồi quyết định PASS hay FAIL; chỉ nhìn parent đã trở lại là chưa đủ. Main dùng hai cờ riêng để chứng minh thời điểm và trạng thái, không đo một thứ tự in log ngẫu nhiên.

[2]

Wait fork theo parent, không theo fork gần nhất trong văn bản

Sau điểm 7 ns, main gọi wait fork và đợi child d còn sống tới 10 ns. Nó kiểm d=1 và thời gian 10 ns. Chỉ một parent và các child trực tiếp liên quan được dùng trong giai đoạn này.

Wait fork áp dụng những immediate child của process đang gọi, gồm child còn sống từ các fork trước đó. Nó không phải một lệnh “join lại block có dấu end gần nhất”, cũng không tự đợi mọi process khác trong toàn simulator. Không coi một child đã kết thúc là bằng chứng mọi grandchild mà nó từng tạo đều đã xong.

[3]

Join_none cho parent đi tiếp và cần điểm thu dọn rõ

Ở 10 ns, main tạo e sau 1 ns và f sau 3 ns bằng join_none. Statement ngay sau join_none vẫn chạy ở 10 ns, trước khi hai child có delay dương hoàn tất, nên e/f phải còn 0. Wait fork sau đó chặn parent tới 13 ns và cả hai cờ phải bằng 1.

Child của join_none chỉ bắt đầu thực thi khi parent nhường qua một thao tác blocking hoặc kết thúc. Đừng dùng một loop parent không nhường để giả định worker đã chạy, hoặc truyền cùng một biến loop đang đổi cho nhiều worker mà không kiểm lifetime/capture. Main không có fork-loop và không dùng PASS ở đây để claim mọi biến thể automatic capture đã được kiểm.

[4]

Hủy descendants của đúng process đang gọi

Giai đoạn cuối bắt đầu 13 ns. Một child isolation tạo winner sau 1 ns và loser sau 4 ns, đợi join_any rồi disable fork. Winner xong 14 ns; loser dự kiến 17 ns bị hủy. Cùng lúc đó, một child sentinel của outer fork cần sống tới 19 ns.

Disable fork nằm trong process isolation nên chỉ thu dọn descendants của process này; sentinel là sibling, vẫn chạy. Outer join cuối cùng chờ sentinel ở 19 ns. Main kiểm winner=1, loser=0, done=1 và sentinel=1. Nếu bỏ disable, loser lên 1; nếu đặt lệnh ở parent quá rộng, sentinel có thể bị hủy. Tên block và vị trí thụt lề không thay cho cây process thực tế.

Lệnh hủy chỉ ngăn phần việc chưa thực hiện; nó không hoàn tác các side effect đã xảy ra. Nếu loser đã ghi dữ liệu hoặc lấy semaphore trước lúc bị hủy, cần một thiết kế cleanup/ownership riêng. Main chỉ dùng cờ và delay, không claim tự hoàn trả resource.

[5]

Tính sáu checkpoint trước khi chạy

Điểm kiểm Thời điểm ns Điều kiện chính
join 5 a/b đều 1
join_any 7 c1, d0
wait fork 10 d1
join_none parent 10 e/f vẫn 0
drain child 13 e/f đều 1
isolation 19 winner 1, loser 0, sentinel 1

Expected trước chạy: FORK CHECK checks=6 join_ns=5 any_ns=7 drain_ns=13 cancelled_loser=1 sentinel=1 time_ns=19. Main dùng Icarus 11.0, chỉ claim parse/simulate cho source này; không suy ra mọi scheduler hoặc mọi cấu trúc process class đều hỗ trợ.

Fork/join: đợi kết quả, thu dọn nhánh và giữ đúng phạm vi hủy

Expected tính trước chạy:6check tại 5/7/10/10/13/19 ns.

tb.sv testbench
`timescale 1ns/1ps// Expected join at5,join_any at7,wait fork at10,join_none drained13,isolated cancel preserves sentinel19.module tb_fork; bit a=0,b=0,c=0,d=0,e=0,f=0,winner=0,loser=0,sentinel=0,done=0; int checks=0; initial begin  fork #2 a=1;#5 b=1;join  if(!a||!b||$time!=5)$fatal(1,"fork join-all oracle");checks++;  fork #2 c=1;#5 d=1;join_any  if(!c||d||$time!=7)$fatal(1,"fork join-any oracle");checks++;  wait fork;  if(!d||$time!=10)$fatal(1,"fork wait-children oracle");checks++;  fork #1 e=1;#3 f=1;join_none  if(e||f||$time!=10)$fatal(1,"fork join-none parent oracle");checks++;  wait fork;  if(!e||!f||$time!=13)$fatal(1,"fork join-none drain oracle");checks++;  fork   begin    fork #1 winner=1;#4 loser=1;join_any    disable fork;done=1;   end   begin #6 sentinel=1;end  join  if(!winner||loser||!done||!sentinel||$time!=19)$fatal(1,"fork cancellation isolation oracle");checks++;  if(checks!=6)$fatal(1,"fork accounting");  $display("FORK CHECK checks=6 join_ns=5 any_ns=7 drain_ns=13 cancelled_loser=1 sentinel=1 time_ns=19");$finish; end initial begin #30;$fatal(1,"fork watchdog");endendmodule

Đã kiểm chứng thế nào

  • Phân tích và elaborate theo LRM (pyslang 12.1.0)
  • Mô phỏng, exit 0 và output khớp kỳ vọng tính tay (Icarus Verilog 11.0, 4-state)

Output phải khớp: FORK CHECK checks=6 join_ns=5 any_ns=7 drain_ns=13 cancelled_loser=1 sentinel=1 time_ns=19

Icarus 11.0; source dùng cờ/delay có 6 checkpoint. Không claim mọi process class, automatic loop capture, resource cleanup hoặc timing vật lý.

Các kiểm tra chạy lúc xây dựng nội dung (2026-11-06), không chạy trong trình duyệt. Kỳ vọng được tính tay trước khi chạy.

Ba bản sao lỗi giữ nguyên checker: join thay join_any, bỏ disable và join thay join_none. Chúng bị bắt ở các oracle riêng. Watchdog là một đường lỗi bổ sung, không thay việc kiểm đủ sáu checkpoint.

[6]

Tự kiểm tra

Tự kiểm tra: fork

  1. Main qua join_any ở thời điểm nào?
  2. Sau join_any ở 7 ns, d có tự bị hủy không?
  3. Ngay sau join_none ở 10 ns, main yêu cầu gì?
  4. Vì sao sentinel vẫn hoàn tất 19 ns trong main?
  5. Disable fork có tự hoàn tác side effect trước đó của child không?

Vẽ cây process rồi chọn chờ hoặc hủy

Trước một timeout pattern, ghi parent nào tạo worker, waiter nào cần kết quả và process nào được phép hủy ai. Dùng join khi cần tất cả, join_any khi cần xử lý winner nhưng vẫn thu dọn loser, join_none khi có lifecycle quản lý background task rõ ràng. Giữ sentinel hoặc quan sát độc lập để phát hiện cleanup đã hủy nhầm công việc khác.

Nguồn và mức độ đối chiếu

  1. SystemVerilog fork join — bản chụp resources.zip, Syntax; A Basic fork...join Example. Trích dẫn; nguồn đã đọc các mục liên quan.
  2. SystemVerilog fork join\_any — bản chụp resources.zip, Syntax; A Basic fork...join_any Example. Trích dẫn; nguồn đã đọc các mục liên quan.
  3. SystemVerilog wait fork — bản chụp resources.zip, A Basic wait fork Example; Does wait fork Wait for Every Process?. Trích dẫn; nguồn đã đọc các mục liên quan.
  4. SystemVerilog fork join\_none — bản chụp resources.zip, Syntax; A Basic fork...join_none Example. Trích dẫn; nguồn đã đọc các mục liên quan.
  5. SystemVerilog disable fork join — bản chụp resources.zip, Why Disable a Fork?; What Happens When a Fork Is Disabled?. Trích dẫn; nguồn đã đọc các mục liên quan.
  6. Thí nghiệm nguyên bản ex.b7.fork, Source, self-checks, verification.json và concurrency probes. Trích dẫn; nguồn đã đọc toàn văn.
  7. IEEE 1800-2023 SystemVerilog LRM, Parallel blocks, process lifetime, wait fork and disable fork — chưa đối chiếu LRM. Chưa đối chiếu với văn bản nguồn (chưa mở).