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版