SystemVerilog(6):线程通信
1、线程
- 线程即独立运行的程序。
- 线程需要被触发,可以结束或者不结束。
- 在 module 中的 initial 和 always,都可以看做独立的线程,它们会在仿真 0 时刻开始,而选择结束或者不结束。
- 硬件模型中由于都是 always 语句块,所以可以看成是多个独立运行的线程,而这些线程会一直占用仿真资源,因为它们并不会结束。
- 软件测试平台中的验证环境都需要由 initial 语句块去创建,而在仿真过程中,验证环境中的对象可以创建和销毁,因此软件测试端的资源占用是动态的。
- 软件环境中的 initial 块对语句有两种分组方式,使用 begin...end 或 fork...join。
- begin...end 中的语句以顺序方式执行,而 fork...join 中的语句则以并发方式执行。
- 与 fork...join 类似的并行方式语句还包括 fork...join_any 、 fork...join_none 。
一些概念的澄清:
- 线程的执行轨迹是呈树状结构的,即任何的线程都应该有父线程。
- 父线程可以开辟若干个子线程,父线程可以暂停或者终止子线程。
- 当子线程终止时,父线程可以继续执行。
- 当父线程终止时,其所开辟的所有子线程都应当会终止。
2、线程的控制
2.1 三种fork
- fork_join:最长时间的程序走完,才会结束 fork 块;
- fork_join_any:最短时间的程序走完,就会结束 fork 块,但 fork 语句里未走完的程序依旧会执行(前提是Initial未结束);
- fork_join_none:一运行就会结束 fork 块,但 fork 语句里未走完的程序依旧会执行(前提是initial未结束);
的
2.2 等待所有fork
在sv中,当程序中的 initial 块全部执行完毕,仿真器就退出了。如果我们希望等待 fork 块中的所有线程执行完毕再退出结束 initial 块,我们可以使用 wait fork 语句来等待所有子线程结束。
task run_threads ; fork check_trans (tr1); //线程1 check_trans (tr2); //线程2 check_trans (tr3); //线程3 join_none ... //等待所有fork中的线程结束再退出task wait fork; endtask
2.2 停止单个fork
在使用了 fork ...join any 或者 fork...join_none 以后,我们可以使用 disable 来指定需要停止的线程。
parameter TIME_OUT = 1000; task check_trans( Transaction tr) ; fork begin //等待回应,或者达到某个最大时延 fork: timeout block begin wait(bus.cb.addr == tr.addr) ; $display("@%0t: Addr match %d",$time, tr.addr); end #TIME_OUT $display("@%0t: Error: timeout",$time); join_any disable timeout_block ; end join_none endtask
disable fork可以停止从当前线程中衍生出来的所有子线程。
initial begin check_trans (tro) ; //线程0 //创建一个线程来限制disable fork的作用范围 fork //线程1 begin check_trans(tr1); //线程2 fork //线程3 check_trans(tr2); //线程4 join //停止线程1-4,单独保留线程0 #(TIME_OUT/2) disable fork; end join end
2.3 停止被多次调用的任务
如果你给某一个任务或者线程指明标号,那么当这个线程被调用多次以后,如果通过disable去禁止这个线程标号,所有衍生的同名线程都将被禁止。
task wait_for_time__out(int id) ; if (id == 0) fork begin #2; $display ("@%0t: disable wait_for_time_out", $time); disable wait_for_time_out; end join_none fork: just_a_little begin $display ("@0t: 8m: 80d entering thread", $time, id); #TIME_OUT; $display("@%0t:%m: %0d done", $time, id); end join_none endtask initial begin wait_for_time_out(0); //Spawn thread 0 wait_for_time_out(1); //Spawn thread 1 wait_for_time_out(2); //Spawn thread 2 #(TIME_OUT*2) $display("@%0t: All done", $time); end
- 任务 wait_for_time_out 被调用了三次,从而衍生了三个线程。
- 线程0在#2延时之后禁止了该任务,而由于三个线程均是"同名”线程,因此这些线程都被禁止了,最终也都没有完成。
3、线程间的通信
- 测试平台中的所有线程都需要同步并交换数据。一个线程需要等待另一个。
- 多个线程可能同时访问同一个资源。线程之间可能需要交换数据。
- 所有这些数据交换和同步称之为线程间的通信(IPC,Interprocess Communication) 。
3.1 事件event
- Verilog中,一个线程总是要等待一个带@操作符的事件。这个操作符是边沿敏感的,所以它总是阻塞着、等待事件的变化。
- 其它线程可以通过 -> 操作符来触发事件,结束对第一个线程的阻塞。
- 这就像在打电话时,一个人等待另一个人的呼叫。
- 唯一不需要 new( ) 的方法。
3.1.1 事件的边沿阻塞
event e1,e2; initial begin $display("@%0t: 1: before trigger", $time); -> e1; @e2; $display("e%0t: 1: after trigger", $time); end initial begin $display("@%0t: 2: before trigger", $time); -> e2; @e1; $display("@%0t: 2: after trigger",$time); end
打印结果如下所示:
@0: 1: before trigger @0: 2: before trigger @0: 1: after trigger
- 第一个初始化块启动,触发e1事件,然后阻塞在e2上。
- 第二个初始化块启动,触发e2事件,然后阻塞在e1上。
- e1 和 e2 在同一个时刻被触发,但由于 delta cycle 的时间差使得两个初始化块可能无法等到 e1 或者 e2。
- 所以,更安全的方式可以使用event的方法 triggered ( )。
3.1.2 等待事件的触发
- 可以使用电平敏感的 wait (e1.triggered ( ) ) 来替代边沿敏感的阻塞语句 @e1 。
- 如果事件在当前时刻已经被触发,则不会引起阻塞。否则,会一直等到事件被触发为止。
- 这个方法比起 @ 而言,更有能力保证,只要 event 被触发过,就可以防止引起阻塞。
event e1,e2; initial begin $display("@%0t: 1: before trigger",$time); -> e1; wait( e2.triggered() ); $display("@%0t: 1: after trigger" ,$time); end initial begin $display("@%0t: 2: before trigger", $time); -> e2; wait( e1.triggered() ) ; $display("@%0t: 2: after trigger", $time); end
打印结果如下所示:
@0: 1: before trigger @0: 2: before trigger @0: 1: after trigger @0: 2: after trigger
3.2 旗语semaphore
- semaphore 可以实现对同一资源的访问控制。
- 对于初学者而言,无论线程之间在共享什么资源,都应该使用 semaphore 等资源访问控制的手段,以此避免可能出现的问题。
- semaphore 有三种基本操作。
- new( ) 方法可以创建一个带单个或者多个钥匙的semaphore;
- 使用 get( ) 可以获取一个或者多个钥匙;
- 而 put( ) 可以返回一个或者多个钥匙。
- 如果你试图获取一个 semaphore 而希望不被阻塞,可以使用 try get( ) 函数。
- 返回 1 表示有足够多的钥匙;
- 返回 0 则表示钥匙不够。
program automatic test(bus_ifc. TB bus) ; sermaphore semn; //创建一-个semaphore initial begin sen = new(1); //分配一个钥匙 fork sequencer(); //产生两个总线事务线程 sequencer(); join end task sequencer; repeat($urandom%10) //随机等待0-9个周期 @bus.cb; sendTrans(); //执行总线事务 endtask task sendTrans; sem.get(1); //获取总线钥匙 @bus.cb; //把信号驱动到总线上 bus.cb.addr <= t.addr; ... sem.put(1); //处理完成时把钥匙返回 endtask endprogram
3.3 信箱mailbox
- 线程之间如果传递信息,可以使用 mailbox 。
- mailbox 和队列 queue 有相近之处。
- mailbox 是一种对象,因此也需要使用 new( ) 来例化。
- 例化时有一个可选的参数 size 来限定其存储的最大数量。
- 如果 size 是 0 或者没有指定,则信箱是无限大的,可以容纳任意多的条目。
- 使用 put( ) 可以把数据放入 mailbox,使用 get( ) 可以从信箱移除数据。
- 如果信箱为满,则 put( ) 会阻塞;
- 如果信箱为空,则 get( )会阻塞。
- 使用 peek( ) 可以获取对信箱里数据的拷贝而不移除它。
- 线程之间的同步方法需要注意,哪些是阻塞方法,哪些是非阻塞方法,即哪些是立即返回的,而哪些可能需要等待时间的。
program automatic bounded; mailbox mbx ; initial begin mbx = new(1); //容器为1 fork //Producer线程 for(int i=1; i<4; i++) begin $display("Producer: before put (80d)", i); mbx.put(i) ; $display("Producer : after put (80d)", i); end //consumer线程 repeat(4) begin int j; #1ns mbx.get(j); $display("consumer : after get (80d)",j); end join end endprogram
打印结果如下所示:
Producer : before put(1) Producer : after put(1) Producer : before put(2) Consumer : after get(1) Producer : after put(2) Producer : before put(3) Consumer : after get(2) Producer : after put(3) Consumer : after get(3)
关于mailbox的其它特性也需要加以了解:
- mailbox 在例化时,通过 new(N) 的方式可以使其变为定长(fixed length) 容器。这样在负载到长度N以后,无法再对其写入。如果用 new( ) 的方式,则表示信箱容量不限大小。
- 除了 put( ) / get( ) / peek( ) 这样的阻塞方法,用户也可以考虑使用 try_put( ) / try_get( ) / try_peek( ) 等非阻塞方法。
- 如果要显式地限定 mailbox 中元素的类型,可以通过 mailbox #(type = T) 的方式来声明。
3.4 三种方法的比较
- event:最小信息量的触发,即单一的通知功能。可以用来做事件的触发,也可以多个 event 组合起来用来做线程之间的同步。
- semaphore:共享资源的安全卫士。如果多线程间要对某一公共资源做访问,即可以使用这个要素。
- mailbox:精小的 SV 原生 FIFO。在线程之间做数据通信或者内部数据缓存时可以考虑使用此元素。
参考资料:
[1] 路科验证V2教程
[2] 绿皮书:《SystemVerilog验证 测试平台编写指南》第2版
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步