SV绿皮书笔记(七)

第七章. 线程以及线程间的通信

在实际硬件中,时序逻辑通过时钟沿来激活,而组合逻辑的输出则随着输入的变化而变化。所有这些并发的活动在V的寄存器传输级上是通过initial和always块语句,实例化和连续赋值语句来模拟的为了模拟和检验这些语句块,测试平台使用许多并发的线程。在测试平台的环境里,大多数语句块被模拟成事务处理器,并运行在各自的线程里。

每个线程都会跟相邻的线程通信,因此需要借助线程间的通信(IPC)来完成。常见的线程间通信有标准的V事件(event),事件控制,wait语句,SV信箱和旗语等

7.1 线程的使用

虽然所有的线程结构都可以用在模块和程序块中,但实际上测试平台隶属于程序块,代码总是在initial块中启动,从0时刻开始执行。虽然always块不能放在程序块中,但是,通过在initial块内引入forever循环便可以轻松解决这个问题。

标准的V对语句有两种分组方式,begin...end块和fork...join块。begin…end块内的语句以顺序的方式执行fork…join中的语句以并行方式执行。由于fork...join块内的所有语句都执行完后才能继续执行后续处理,因此在V的测试平台中很少用它。

SV引入了两种新的创建线程的方法,使用fork...join_nonefork...join_any语句。测试平台通过已有的结构如事件,@事件控制,waitdisable语句,以及新的语言元素(如旗语和信箱)来实现线程间的通信,同步以及对线程的控制。

图片

7.1.1 使用fork…join和begin…end语句

//fork...join和begin...end的嵌套
initial
begin
$display("@%0t: start for...join example", $time);
#10 $display("@%0t: sequential after #10", $time);

fork
$display("@%0t: parallel start", $time);
#50 $display("@%0t: parallel after #50", $time);
#10 $display("@%0t: parallel after #10", $time);

begin
#30 $display("@%0t: sequential after #30", $time);
#10 $display("@%0t: sequential after #10", $time);
end
join
           
$display("@%0t: after join", $time);
#80 $display("@%0t: finish after #80", $time);
end

//begin...end和fork...join的输出
@0: start fork...join example
@10: sequential after #10
@10: parallel start
@20: parallel after #10
@40: sequential afrer #30
@50: sequential after #10
@60: parallel after #50
@60: after join
@140: finish after #80

从上例可以看出,fork…join块为其内所有语句各开辟一个线程(begin…end块被当作一条语句),begin…end语句块内部的子语句是顺序执行的。

7.1.2 使用fork…join_none来产生线程

fork...join_none块在调度其块内语句时,父线程继续执行。以下代码与7.1.1中相同,fork...join被换做fork...join_none

initial
begin
$display("@%0t: start for...join example", $time);
#10 $display("@%0t: sequential after #10", $time);

fork
$display("@%0t: parallel start", $time);
#50 $display("@%0t: parallel after #50", $time);
#10 $display("@%0t: parallel after #10", $time);

begin
#30 $display("@%0t: sequential after #30", $time);
#10 $display("@%0t: sequential after #10", $time);
end
join_none
           
$display("@%0t: after join", $time);
#80 $display("@%0t: finish after #80", $time);
end

//begin...end和fork...join_none的输出
@0: start fork...join example
@10: sequential after #10
@10: parallel start
@20: parallel after #10
@40: sequential afrer #30
@50: sequential after #10
@60: parallel after #50
@60: after join
//进入fork...join_none块时(#10时刻)执行线程,其余输出均相同
@90: finish after #80

7.1.3 使用fork…join_any实现线程同步

fork...join_any块对块内语句进行调度,当第一个语句完成后,父线程才继续执行,其他停顿的线程也得以继续。以下代码与7.1.1中相同,fork...join被换做fork...join_any

initial
begin
$display("@%0t: start for...join example", $time);
#10 $display("@%0t: sequential after #10", $time);

fork
$display("@%0t: parallel start", $time);
#50 $display("@%0t: parallel after #50", $time);
#10 $display("@%0t: parallel after #10", $time);

begin
#30 $display("@%0t: sequential after #30", $time);
#10 $display("@%0t: sequential after #10", $time);
end
join_none
           
$display("@%0t: after join", $time);
#80 $display("@%0t: finish after #80", $time);
end

//begin...end和fork...join_any的输出
@0: start fork...join example
@10: sequential after #10
@10: parallel start
@20: parallel after #10
@40: sequential afrer #30
@50: sequential after #10
@60: parallel after #50
@60: after join
//进入fork...join_any块时,第一条语句执行完毕时开启此线程(#10时刻),其余输出均相同
@90: finish after #80

7.1.4 在类中创建线程

class Gen_drive;

task run(int n);
Packet p;
fork
repeat (n)
begin
p = new();
assert(p.randomize());
transmit(p);
end
join_none
endtask

task transmit(input Packet p);
...
endtask

endclass

Gen_drive gen;
initial
begin
gen = new();
gen.run(10);
...
end

注意,事务处理器并不是在new函数中启动的,构造函数只用来对数值进行初始化,并不启动任何线程。把构造函数同真正进行事务处理的代码分开,允许你在开始执行事务处理代码之前修改任何变量(构造函数中初始化变量)。这样,就可以引入错误检测,修改缺省值或者变更代码行为。

7.1.5 动态线程

在V中,线程是可预知的,可以通过源代码中initialalwaysfork...join块的数量来确定一个模块中有多少的线程。而在SV中,可以动态地创建线程,而且不用等到他们都执行完毕。

7.1.6 线程中的自动变量

//不良代码,在循环中使用fork...join语句
program no_auto;
initial
begin
for(int j=0;j<3;j++)
fork
$write(j);//不良代码,三个线程使用的都是j=3
join_none
#0 $display("n");
end
endprogram

#0时延阻塞了当前线程,并且把他们重新调度到了当前时间片之后启动。上例中,时延使得当前线程必须等到在fork...join_none语句中产生的线程执行完以后才得以运行。这种时延在阻塞线程上很有用处,但务必小心,因为过分使用会导致竞争和难以预料的结果。

//改良代码
initial
begin
for(int j=0;j<3;j++)
fork
automatic int k=j;
$write(k);
join_none
#0 $display
end

使用自动(automatic)变量声明在for循环里的线程中运行,每轮循环中,k就会创建一个k的副本,每个k副本会拷贝保存当前循环中的j变量中的值。在循环完成后,#0阻塞了当前线程,因此三个线程一起运行,并打印出各自的拷贝值k。

//自动存储的程序或模块里,变量声明时可以不使用关键词automatic
program automatic bug_free;
initial
begin
for(int j=0;j<3;j++)
begin
fork
$write(k);
join_none
end
#0 $display;
end
endprogram

7.1.7 等待所有衍生线程

SV中,当程序中的initial块全部执行完毕,仿真就退出了,但是如果生成了多个线程,有些线程运行时间比较长,可以使用wait fork语句等待所有子线程结束。

task run_threads;
...
fork
check_trans(tr1);
check_trans(tr2);
check_trans(tr3);
join_none
wait fork;//等待fork内所有子线程结束
endtask

7.1.8 在线程间共享变量

在一个类内部的子程序里,可以使用局部变量,类变量或者在程序中定义的变量。如果忘记声明了某个变量,SV会到更高层的作用范围内寻找,直至找到匹配的声明。如果两部分代码无意间共享了同一个变量,这会导致难以发现的漏洞,而漏洞的原因往往是忘了在最内层声明变量。

program bug;
class Buggy;
int data[10];
task transmit;
fork
//忘记写int导致未声明变量i,会向类上一级程序块中搜索i变量,最终使用的 //是program中的i变量
for(i=0;i<10;i++)
send(data[i]);
join_none
endtass
endclass

int i;//共享的程序级变量i
Buggy b;
event receive;

initial
begin
b=new();
for(i=0;i<10;i++)b.data[i]=i;
b.transmit();
for(i=0;i<10;i++)@(receive) $display(b.data[i]);
end
endprogram
7.2 停止线程

正如需要在测试平台中创建线程,也需要停止线程。V中的disable语句可以用于停止SV中的线程。

7.2.1 停止单个线程

通过禁止一个标签可以精确地指定需要停止的块。

parameter TIME_OUT = 1000;
task check_trans(Transaction tr);
//父线程中有两个子线程,fork...join_any和disable语句,因在begin...end块中,两个线程顺序执行,如果bus总线在TIME_OUT时延之前满足条件,则执行内部打印语句,否则在经过TIME_OUT时延后fork...join_any触发diable线程,终止time_block线程内所有子线程
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_any
endtask

7.2.2 停止多个线程

SV中引入disable fork语句能够停止从当前线程中衍生出来的所有子线程。

initial
begin
check_trans(tr0);//线程0
fork//线程1
begin
check_trans(tr1);//线程2
fork//线程3
check_trans(tr2);//线程4
join
           //disable语句结束fork...join线程及其所有子线程
           #(TIME_OUT/2) disable fork;
       end
join
end
           
//使用带标号的disable来停止所有线程
initial
begin
check_trans(tr0);
fork
  begin:thread_inner
           check_trans(tr1);
check_trans(tr2);
end
      (TIME_OUT/2) disable threads_inner;//结束thread_inner内所有子线程
   join
end

7.3 线程间的通信

SV中可以使用事件,旗语和信箱来完成线程间通信。

7.3.1 事件

V事件可以实现线程的同步。使用@event_a来阻塞线程,使用->event_a来解除线程中的阻塞。SV中对事件进行了增强,事件成为了同步对象的句柄,可以传递给子程序。此特性允许你在对象间共享事件,而不用把事件定义为全局变量。V中,当一个线程在阻塞到一个事件上时,正好另一个线程触发了这个事件,则竞争便有可能发生。SV中可以使用triggered()函数来查询某个时间是否已被触发,线程可以等待这个函数的结果,而不必使用@阻塞符,从而避免了竞争的发生。

7.3.1.1 在事件的边沿阻塞

//@event可以理解为边沿阻塞(解除阻塞是使用->,某个时刻触发,错过触发时刻@不会解除阻塞)
event e1, e2;

initial
begin
$display("@%0t: 1: before trigger", $time);
-> e1;
@e2;
$display("@%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: brfore trigger
@0: 1: after trigger

7.3.1.2 等待事件的触发

可以使用电平敏感的wait(e1.triggered())来替代边沿敏感的阻塞语句@e1,如果事件在当前的时间已经被触发,则不会引起阻塞,否则会一直等到事件被触发为止。

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;
//与7.3.1.1中不同,wait(e1.triggered())不会因为错过->e1而发生阻塞,只要e1事件被触发,triggered()函数就为真
wait(e1.triggered());
$display("@%0t: 2: after trigger", $time);
end
   
//输出
@0: 1: before trigger
@0: 2: brfore trigger
@0: 1: after trigger
@0: 2: after trigger

7.3.1.3 在循环中使用事件

可以使用事件来实现两个线程的同步,但是请务必小心避免使用零时延的循环

//零时延的循环
forever
begin
//handshake一旦被触发,则循环陷入0时延的死循环
wait(handshake.triggered());
$display("Received next event");
process_in_zero_time();
end

//带有时延的循环
forever
begin
//每次循环阻塞
@handshake;
$display("Received next event");
process_in_zero_time();
end

如果需要在同一时刻发送多个通告,不应该使用事件,而应该使用其他内嵌排队机制的线程通信方法,如旗语和信箱。

7.3.1.4 传递事件

SV中的事件可以像参数一样传递给子程序。

class Generator;
event done;

function new(event done);
this.done = done;
endfunction

task run();
fork
begin
...
->done;
end
join_none
endtask
endclass

program automatic test;
event gen_done;
Generator gen;
initial
begin
gen = new(gen_done);
gen.run();
wait(gen_done.triggered());
end
endprogram

7.3.1.5 等待多个事件

event done[N_GENERATORS];
initial
begin
foreach(gen[i])
begin
  //创建多个发生器
gen[i] = new();
gen[i].run(done[i]);
end
       
foreach(gen[i])
fork
automatic int k=i;、
           //等待多个线程被触发
wait (done[k].triggered()):
join_none
       //等待fork...join_none快内所有线程执行完毕
wait fork;
end

也可以使用线程计数的方式来等待所有线程执行完毕。

class Generator;
static int thread_conunt=0;//记录线程的数目
task run();
//调用run函数,线程数+1
thread_count++;
fork
begin
          ...
          //执行完毕所有代码后,线程数-1
thread_count--;
end
join_none
endtask
endclass

Generator gen[N_GENERATORS];
initial
begin
   foreach(gen[i])gen[i]=new();
foreach(gen[i]) gen[i].run();
wait (Generator::thread_count == 0);
end

7.3.2 旗语

使用旗语可以实现对同一资源的访问控制。旗语可以理解为对同一资源的“互斥访问”。

7.3.2.1 旗语的操作

旗语有三种基本操作:

  • new:可以创建一个带单个或者多个钥匙的旗语。
  • get:可以获取一个或多个钥匙。
  • put:可以返回一个或多个钥匙。

如果希望获取一个旗语而不被阻塞,可以使用try_get函数,返回1证明有足够多的钥匙,而返回0则表明钥匙不够。

program automatic test(bus_ifc.TB bus);
semaphore sem;//创建一个旗语
initial
begin
sem = new(1);//分配一个钥匙
fork
sequencer();
sequencer();
join
end

task sequencer;
repeat($urandom()%10)@bus.cb;//等待0~9个周期
sendTrans();//执行总线事务
endtask

task sendTrans;
sem.get(1);//获取总线钥匙
@bus.cb;
bus.cb.addr <= t.addr;
...
sem.put(1);//返回总线钥匙
endtask
endprogram

7.3.3 信箱

SV使用信箱来进行线程间的传递信息。从硬件的角度出发,对信箱的最简单的理解是把它看成一个具有源端和收端的FIFO。源端把数据放进信箱,收端则从信箱中获取数据。信箱可以有容量上限,也可以没有。当源端线程试图向一个容量固定并且已经饱和的信箱放入数值时,会发生阻塞,直到信箱中的数据被移走。同样的收端线程试图从一个空信箱里移走数据,它会被阻塞直到有数据被放入信箱里。如果不希望在访问信箱是出现阻塞,可以使用try_get()try_peek()函数。如果函数执行成功,则返回一个非0值,否则返回0。信箱允许放入任何混合的数据类型,但是不要这样做,务必在一个信箱里只放一种类型的数据。

7.3.3.1 定容信箱

缺省情况下,信箱类似容量不限的FIFO。在构造信箱时可以指定一个最大容量,缺省容量是0,表示信箱容量不限,任何大于0的数值便创建了一个定容信箱。

7.3.3.2 在异步线程间使用信箱通信

如果想让生产方和消费方两个线程保持一致,那就需要额外的握手信号。

7.3.3.3 使用信箱和事件来实现线程的同步

//@handshake和->handshake实现线程同步
program automatic mbx_evt;
mailbox mbx;
event handshake;
class Producer;
task run;
for(int i=1; i<4; i++)
begin
$display("Producer: before put (%0d)", i);
mbx.put(i);
@handshake;
$display("Producer: after put (%0d)", i);
end
entask
endclass
               
   class Consumer;
task run;
int i;
repeat (3)
       begin
           mbx.get(i);
$display("Consumer: after get (%0d)", i);
-> handshake;//如果这个先执行,@handshake陷入永久阻塞???
       end
endclass
           
Producer p;
Cosumer c;

initial
   begin
  mbx = new();
p = new();
c = new();

fork
           p.run();
c.run();
       join
   end
endprogram

7.3.3.4 使用两个信箱来实现线程的同步

//使用两个邮箱同步
program automatic mbx_mbx2;

mailbox mbx, rtn;

class Producer;
task run();
int k;
for(int i=1;i<4;i++)
begin
$display("Producer: before put (%0d)", i);
mbx.put(i);
rtn.get(k);
$display("Producer: after get (%0d)", k);
end
endtask
endclass

class Consumer;
task run();
int i;
repeat(3)
begin
$display("Consumer: before get");
mbx.get(i);
$display("Consumer: after get (%0d)", i);
rtn.put(-i);
end
endtask
endclass

Producer p;
Cosumer c;

initial
begin
mbx = new();
rtn = new();
p = new();
c = new();
fork
p.run();
c.run();
join
end
endprogram

参考文献:

SystemVerilog验证 测试平台编写指南(原书第二版)张春 麦宋平 赵益新 译

 2022-07-31 17:20 发表于江苏

阅读原文

简介:学好技术,大有可为。欢迎关注微信公众号:搬砖小张
(0)
打赏 喜欢就点个赞支持下吧 喜欢就点个赞支持下吧

声明:本文来自“搬砖小张”,分享链接:https://www.zyxiao.com/p/324558    侵权投诉

网站客服
网站客服
内容投稿 侵权处理
分享本页
返回顶部