欢迎光临
我们一直在努力

80.【SV】SystemVerilog fork join_any

🔥 SystemVerilog fork join_any —— 验证工程师的“先遣队员”

作为芯片验证工程师,经常需要同时启动多个任务,但有时只需要其中一个先完成就能继续下一步,而其他任务仍然在后台运行。
SystemVerilog 的 fork join_any 就是为这种场景设计的:它像一个聪明的“先遣队员”策略,只要有一个子任务完成,主线程就立即恢复执行,而其他子线程继续在后台工作。


🎯 一、理解 fork 家族的三种“老板”模式

想象你是一个项目经理,手里有三个任务需要同时进行:

  • 任务A:打印“Thread1_0” (需要 20ns 准备)
  • 任务B:打印“Thread1_1” (需要 30ns 准备)
  • 任务C:打印“Thread2” (需要 10ns 准备)
  • 你有三种不同的管理模式:

    管理风格对应 fork 类型行为
    守旧老板 fork join 站在旁边盯着,等所有任务都完成才继续
    急躁老板 fork join_any 只要有任意一个任务完成,就立刻继续,其他任务继续在后台
    甩手掌柜 fork join_none 任务派下去后,立刻转身就走,任务在后台自己完成

    fork join_any 就是“急躁老板”模式:他只要看到第一个下属完成任务,就立刻去干别的事,剩下的下属继续做他们的事,互不干扰。


    📦 二、fork join_any 的基本用法

    语法

    fork
    // 线程1
    // 线程2
    // …
    join_any

    执行流程

    • 主线程在 fork 处启动所有子线程,然后等待。
    • 当任意一个子线程完成时,主线程立即从 join_any 之后继续执行。
    • 其他尚未完成的子线程仍然在后台运行,直到它们自己结束或仿真结束。

    示例 1:三个独立线程

    module tb;
    initial begin
    $display("[%0t] 主线程:开始派任务", $time);
    fork
    print(20, "Thread1_0");
    print(30, "Thread1_1");
    print(10, "Thread2");
    join_any
    $display("[%0t] 主线程:有任务完成了,我继续", $time);
    end

    // 注意:必须用 automatic,否则会有数据竞争
    task automatic print(int _time, string t_name);
    #(_time) $display("[%0t] %s", $time, t_name);
    endtask
    endmodule

    输出:

    [0] 主线程:开始派任务
    [10] Thread2
    [10] 主线程:有任务完成了,我继续
    [20] Thread1_0
    [30] Thread1_1

    逐步分析:

  • 时间 0,主线程打印“开始派任务”,然后启动三个子线程。
  • 三个子线程并行运行,各自延时。
  • 时间 10,线程2(Thread2)最先完成,打印 Thread2。
  • 主线程在 join_any 处等待,当线程2完成时,主线程立即恢复,打印“有任务完成了,我继续”。
  • 此时线程1_0 和线程1_1 仍在后台运行,分别在 20ns 和 30ns 打印。
  • 仿真在所有子线程完成后(30ns)结束。
  • 关键点:主线程只等待第一个完成的子线程,而不是所有。


    🪆 三、嵌套的 fork join_any

    嵌套时,join_any 的行为会受外层影响。看下面这个例子:

    示例 2:两层嵌套

    module tb;
    initial begin
    $display("[%0t] 主线程:开始派任务", $time);
    fork
    fork
    print(20, "Thread1_0");
    print(30, "Thread1_1");
    join_any // 内层 fork join_any
    print(10, "Thread2");
    join_any // 外层 fork join_any
    $display("[%0t] 主线程:有任务完成了,我继续", $time);
    end

    task automatic print(int _time, string t_name);
    #(_time) $display("[%0t] %s", $time, t_name);
    endtask
    endmodule

    输出:

    [0] 主线程:开始派任务
    [10] Thread2
    [10] 主线程:有任务完成了,我继续
    [20] Thread1_0
    [30] Thread1_1

    逐步拆解:

  • 时间 0,主线程进入外层 fork,内部有两个子线程:
    • 子线程1:一个内层 fork join_any 块,它本身包含两个子线程(Thread1_0 和 Thread1_1)。
    • 子线程2:直接是 print(10, "Thread2")。
  • 外层 fork join_any 等待任意一个子线程完成。哪个会先完成?显然是子线程2,因为它只需要 10ns 延时,而内层 fork 中最快也需要 20ns(Thread1_0)。
  • 时间 10,子线程2 完成,打印 Thread2。外层 join_any 条件满足,主线程立即恢复,打印“有任务完成了,我继续”。
  • 此时,内层 fork join_any 仍在运行(它还没完成,因为它要等自己的子线程之一完成)。但注意:内层 fork 作为外层的一个子线程,它本身还没有完成,因为它的子线程还没结束。但主线程已经不管它了,继续执行后面的代码。
  • 内层 fork 中的两个子线程继续在后台运行,分别在 20ns 和 30ns 打印。
  • 重点:内层 fork join_any 作为一个整体线程,它的完成时间取决于它内部第一个子线程完成的时间。这里内层最快的子线程是 Thread1_0(20ns),所以内层 fork 会在 20ns 时结束(但此时主线程早已继续)。不过因为主线程已经恢复,内层的结束不会影响主线程。


    ⚠️ 四、为什么需要 automatic 任务?

    和之前讨论的 fork join_none 一样,在 fork join_any 中,如果多个线程调用同一个静态(static)任务,且任务内有延迟,会导致数据竞争。因为静态任务的所有实例共享局部变量,当最后一个线程修改了变量后,所有等待中的线程醒来时都会看到这个最后的值。

    错误示例(不加 automatic)

    task print(int _time, string t_name); // 静态任务
    #(_time) $display("[%0t] %s", $time, t_name);
    endtask

    输出可能会全部变成最后一个线程的名字。

    解决方法

    始终使用 automatic 关键字:

    task automatic print(int _time, string t_name);
    #(_time) $display("[%0t] %s", $time, t_name);
    endtask

    这样每个线程都有自己独立的变量副本,互不干扰。


    🧠 五、fork join_any 的典型应用场景

    场景1:超时检测

    fork
    begin
    #1000; // 超时时间
    $error("Timeout!");
    end
    begin
    wait_for_response(); // 等待响应
    $display("Response received");
    end
    join_any
    disable fork; // 杀掉另一个线程(超时或响应已经发生)

    只要响应到达或超时发生,主线程就继续,然后杀掉另一个未完成的线程。

    场景2:多个激励源中任意一个完成就继续

    fork
    send_via_axi();
    send_via_ahb();
    send_via_gpio();
    join_any
    // 只要有一种方式发送成功,就继续下一步

    场景3:并行监测多个事件

    fork
    @(posedge sig_a);
    @(posedge sig_b);
    @(posedge sig_c);
    join_any
    $display("第一个信号上升沿出现!");


    🆚 六、fork join 家族对比

    类型主线程行为子线程状态典型用途
    fork join 等待所有子线程完成 全部完成后主线程继续 需要所有任务都完成才能下一步
    fork join_any 等待任意一个子线程完成 第一个完成后主线程继续,其余后台运行 超时、竞争、多选一
    fork join_none 立即继续 所有子线程后台运行 启动后台监控,不等待

    💎 核心要点总结

  • fork join_any 让主线程在第一个子线程完成后立即恢复,其他子线程继续在后台运行。
  • 嵌套时注意:内层 fork join_any 作为一个整体子线程,它的完成时间取决于它内部第一个完成的子线程。
  • 必须使用 automatic 任务,避免多线程共享变量导致数据错乱。
  • 适用场景:超时检测、多激励源竞争、并行事件监测等。
  • 配合 disable fork 使用,可以在主线程恢复后杀掉不需要的后台线程,节省资源。
  • 验证工程师的心法:

    fork join_any 是你手中的“应急响应器”,让你能在多个并发任务中第一时间响应最先完成的那一个。用它构建超时机制、竞争场景,能让你的验证环境更加灵活高效。但记得给每个线程独立的存储空间(automatic),否则它们会互相串扰,让你抓狂。

    最后的小测验:
    如果在 fork join_any 中启动了一个无限循环的线程(如 forever),而其他线程都很快结束,主线程会在何时恢复?
    (答案:主线程会等那个无限循环的线程完成吗?不会!join_any 只要有一个线程完成就恢复,无限循环的线程永远不会完成,所以主线程会等待其他某个可完成的线程。如果没有其他线程,主线程将永远等下去,导致仿真卡住。因此要避免这种情况。)

    掌握了 fork join_any,你就能像一位临危不乱的指挥官,在众多并行任务中抓住最先的信号,果断决策!

    赞(0)
    未经允许不得转载:171主机测评 » 80.【SV】SystemVerilog fork join_any
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址