先独立作答,再展开参考答案。八股原题沿用原笔记编号;扩展练习为本站根据对应考点编写。
Q16 · fork…join、join_any 和 join_none 有什么区别?
八股原题 · Q16 · 题型:问答
考点
- 父线程恢复条件
- 子线程启动时机
- 未结束线程的处理
展开参考答案 · Q16
题目来源(原笔记记录)
- 泰凌微 · 数字IC验证 · 校招 · 一面(三类 fork join 语句的区别)
- 知存科技 · 数字IC验证 · 校招(三种 fork join / fork join_any / fork join_none 的区别)
- 集益威 · 数字IC验证 · 校招 · 一面(fork join / join_any / join_none 的区别)
- 达摩院 · 数字IC验证 · 校招 · 一面(三类 fork join 语句的区别)
- 泰凌微 · 数字IC验证 · 实习(三种 fork 语句的区别)
- 行云集成电路 · 数字IC验证 · 实习(fork join / join_any / join_none 三者区别)
- 集益威 · 数字IC验证 · 秋招(fork 系列语句区别)
- 昆仑芯 · 数字IC验证 · 秋招 · 一面(通信机制、三种 fork)
- 昆仑芯 · 数字IC验证 · 秋招 · 二面(fork join 线程控制)
- 面试书《Cracking Digital VLSI Verification Interview》· 第4章 Q57(对应题目:fork-join / fork-join_any / fork-join_none)
join 等全部,join_any 等一个但不杀其余,join_none 一个也不等——区别在父线程什么时候继续。
第一,fork…join。 所有子线程结束,父线程才继续。
第二,fork…join_any。 任意一个子线程完成,父线程就继续;剩余线程还在后台跑,不会自动被杀。
第三,fork…join_none。 父线程立即继续(一个也不等);子线程在父线程遇到阻塞、挂起或结束后才开始调度,所以紧跟其后的零时间代码可能先于子线程执行。
| 形式 | 父线程何时继续 | 未完成子线程 |
|---|---|---|
fork...join | 所有子线程结束 | 无 |
fork...join_any | 任意一个子线程结束 | 继续后台运行 |
fork...join_none | 立即继续(不等任何子线程) | 父线程阻塞/结束后才调度,全部后台运行 |
一句话:join 等全部,join_any 等一个但不杀其余,join_none 一个也不等;不要把父线程继续和子线程终止混为一谈。
E16 · join_any 后另一分支在哪里?
扩展练习 · E16 · 题型:代码阅读 · 对应原题:Q16
考点
- 父线程恢复条件
- 子线程启动时机
- 未结束线程的处理
timeunit 1ns; timeprecision 1ps;
initial begin
fork
#2 $display("A");
#5 $display("B");
join_any
$display("P");
wait fork;
$display("D");
end无其他线程终止仿真时,输出时刻和顺序是什么?
展开参考答案 · E16
2ns 输出 A 后输出 P;5ns 输出 B 后输出 D。join_any 在第一个分支结束后继续父线程,另一分支仍活跃;wait fork 等待剩余直接子进程完成。本题不同分支的完成时刻不同,不涉及同刻分支输出次序的不确定性。
Q17 · wait fork 与 disable fork 分别做什么?
八股原题 · Q17 · 题型:问答
考点
- 等待直接子进程完成
- 终止线程作用域
- guard fork 模式
展开参考答案 · Q17
题目来源(原笔记记录)
- 泰凌微 · 数字IC验证 · 校招 · 一面(挂起后阻塞等待所有线程结束的实现方式)
- 达摩院 · 数字IC验证 · 校招 · 一面(终止 fork join_none 创建的进程的方法)
- 泰凌微 · 数字IC验证 · 实习(fork join 内 while(1) 循环的终止实现)
- 面试书《Cracking Digital VLSI Verification Interview》· 第4章 Q58(对应题目:wait fork 与 disable fork)
wait fork 是等待所有直接子进程完成,disable fork 是杀掉所有活动后代线程。
第一,wait fork。 阻塞当前线程,直到它创建的所有子线程完成,适合多个 join_none 之后统一收口(等待对象是当前进程的直接子进程,不递归等待已脱离的孙进程)。
第二,disable fork。 终止当前线程的所有活动后代;作用范围要注意——它会杀掉当前线程之前启动的所有后台子线程,不只”本次 fork 里”的,所以可能误杀。
第三,guard fork。 工程上包一层隔离,让 disable fork 只清理竞争线程:
fork begin : guard
fork
do_work();
timeout();
join_any
disable fork;
end join一句话:wait fork 等待直接子进程,disable fork 终止活动后代;用 guard fork 限制误杀范围。
原稿修正:wait fork 等待调用进程的直接子进程;disable fork 终止其活动后代,二者范围不能混写为“全部后代”。
E17 · 超时清理如何避免误杀后台线程?
扩展练习 · E17 · 题型:代码改错 · 对应原题:Q17
考点
- 等待直接子进程完成
- 终止线程作用域
- guard fork 模式
父进程先启动永久心跳,然后 fork 工作和超时、join_any、disable fork。为什么心跳也可能被杀?给出隔离结构。
展开参考答案 · E17
disable fork 会清理调用进程的活动后代,包括先前启动的心跳。让竞争逻辑运行在单独的直接子进程中:
fork
heartbeat();
join_none
fork
begin
fork
do_work();
timeout();
join_any
disable fork;
end
join内层 disable fork 的调用者是隔离子进程,心跳是其兄弟进程。心跳最终结束仍需单独制定策略。
Q18 · 为什么 for 循环里的 fork 经常打印出相同的值?
八股原题 · Q18 · 题型:问答
考点
- 循环变量共享
- automatic 快照
- join_none 延迟启动
展开参考答案 · Q18
题目来源(原笔记记录)
- 豪威科技 · 数字IC验证 · 校招 · 一面(fork_join_none 代码题,分析输出结果)
- 沐曦 · 数字IC验证 · 校招 · 笔试(fork-join_none 线程打印输出,缺 automatic 时异常)
- 面试书《Cracking Digital VLSI Verification Interview》· 第4章 Q60、Q61(对应题目:线程中的共享变量陷阱;fork 内嵌 for 循环产生几个并行进程)
for 循环里 fork 的线程共享同一个循环变量,加上 join_none 延迟启动,最后多个线程打印出同一个最终值。
第一,原因。 线程共享循环变量 i;join_none 子线程要等父线程挂起才运行,那时循环已经结束,所有线程看到的都是 i 的最终值。
第二,解决。 每轮循环创建 automatic 局部变量保存快照:
for (int i = 0; i < 3; i++) begin
automatic int j = i; // 每轮进入块时先保存快照
fork
$display("%0d", j);
join_none
end
wait fork;一句话:fork 分支若直接引用循环变量,读到的是共享变量的后续值;每轮用 automatic 局部变量捕获快照。
E18 · automatic 应在哪一层捕获?
扩展练习 · E18 · 题型:代码阅读 · 对应原题:Q18
考点
- 循环变量共享
- automatic 快照
- join_none 延迟启动
initial begin
for (int i = 0; i < 3; i++) begin
fork
begin
automatic int j = i;
$display("%0d", j);
end
join_none
end
wait fork;
end能否保证打印 0、1、2?怎样修复?
展开参考答案 · E18
不能。在子进程 begin 块内部初始化发生于子进程启动时,父进程已结束循环,因此这里会读到最终的 i=3。应把 automatic int j = i; 移到每轮循环的 begin 内、fork 之前,再让分支打印 j。修复后值的集合是 0、1、2,输出先后次序不保证。
Q19 · semaphore 的作用是什么?
八股原题 · Q19 · 题型:问答
考点
- 资源令牌
- 互斥与有限并发
- 阻塞和非阻塞获取
展开参考答案 · Q19
题目来源(原笔记记录)
- 字节跳动 · 数字IC验证 · 实习 · 高频题(旗语 semaphore 的含义与典型使用场景)
- 昆仑芯 · 数字IC验证 · 校招 · 一面(什么是 semaphore)
- 集益威 · 数字IC验证 · 校招 · 一面(是否使用过旗语 semaphore)
- 通用 · 数字IC验证 · 实习 · 基础面经(线程同步方式:event、旗语 semaphore)
- 面试书《Cracking Digital VLSI Verification Interview》· 第4章 Q66(对应题目:信号量 semaphore)
semaphore 用令牌(key)控制共享资源——1 个 key 是互斥锁,多个 key 是并发配额。
第一,获取。 get(n) 拿 n 个 key,不够就阻塞;try_get(n) 不够时立即返回 0。
第二,归还。 用完必须 put(n) 归还,数量要和获取一致。
第三,典型用法。 new(1) 相当于互斥锁,new(N) 限制最大并发 N 个;常见错误是归还数量不一致、异常路径忘归还,导致其他线程永久阻塞。
semaphore bus_lock = new(1);
bus_lock.get(1);
drive_bus();
bus_lock.put(1);一句话:semaphore 用令牌控制共享资源,1 个 key 是互斥锁,多个 key 是并发配额。
E19 · 令牌数量能自动防止重复归还吗?
扩展练习 · E19 · 题型:场景分析 · 对应原题:Q19
考点
- 资源令牌
- 互斥与有限并发
- 阻塞和非阻塞获取
semaphore s = new(1); 成功 get(1) 后误调用 put(2)。它会像有固定最大容量的锁一样拒绝多余归还吗?后果是什么?
展开参考答案 · E19
不会自动限制为构造时的容量。此时可用 key 变成 2,后续两个各 get(1) 的线程可能同时进入临界区,破坏互斥。应让获取与归还数量配对,并检查提前返回、超时终止等路径是否漏还或重复归还。
Q20 · mailbox 的作用是什么?有界和无界 mailbox 有何区别?
八股原题 · Q20 · 题型:问答
考点
- 线程安全消息传递
- 阻塞/非阻塞接口
- 参数化类型
展开参考答案 · Q20
题目来源(原笔记记录)
- 小鹏汽车 · IP验证 · 校招(mailbox 的定义与典型使用场景)
- 国科微 · 数字IC验证 · 实习(driver 如何利用 mailbox 完成 ID 解耦)
- 面试书《Cracking Digital VLSI Verification Interview》· 第4章 Q67、Q68(对应题目:信箱 mailbox;有界与无界 mailbox)
mailbox 是线程安全的消息队列,常用于 generator 给 driver 传 transaction。
第一,阻塞与非阻塞。 put() 在有界 mailbox 已满时阻塞,get() 在空时阻塞;try_put、try_get、try_peek 是非阻塞方法。
第二,有界 vs 无界。 new(8) 是容量 8 的有界 mailbox,可以对生产者形成背压;不带容量或容量为 0 是无界 mailbox。
mailbox #(packet) mbx = new(8); // 容量 8
mbx.put(pkt);
mbx.get(pkt);第三,传对象传句柄。 mailbox 传 class transaction 传的是句柄;生产者如果复用并修改同一个对象,消费者会看到被覆盖的数据,所以发送前通常要建新对象或深复制。最好用参数化 mailbox,避免类型不匹配。
一句话:mailbox 可靠传消息,有界版本还能提供背压;传对象时传的是句柄,注意不要复用后篡改。
E20 · mailbox 里的对象会自动复制吗?
扩展练习 · E20 · 题型:代码阅读 · 对应原题:Q20
考点
- 线程安全消息传递
- 阻塞/非阻塞接口
- 参数化类型
一个容量为 2 的 mailbox #(packet),packet 有 int 字段 data。生产者先将同一个 p.data 设为 1 并 put(p),再设为 2 并 put(p);消费者在两次 put 完成后 get 两次。两次读取 data 是什么?
展开参考答案 · E20
两次都为 2,因为两个队列项保存的是同一个对象句柄。容量 2 保证本题生产者不会因队满而提前阻塞,但不提供对象快照。每次发送应创建独立对象并填值;若字段中还有对象句柄,需要按所有权约定做深复制。