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

在实际硬件中,时序逻辑通过时钟沿来激活,而组合逻辑的输出则随着输入的变化而变化。所有这些并发的活动在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);endjoin$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);endjoin_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
//进入fork...join_none块时(#10时刻)执行线程
@10: parallel start
@10: after join
@20: parallel after #10
@40: sequential afrer #30
@50: sequential after #10
@60: parallel after #50
@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);endjoin_any$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
//进入fork...join_any块时,第一条语句执行完毕时开启此线程(#10时刻)
@10: parallel start
@10: after join
@20: parallel after #10
@40: sequential afrer #30
@50: sequential after #10
@60: parallel after #50
@90: finish after #80

7.1.4 在类中创建线程

class Gen_drive;task run(int n);Packet p;forkrepeat (n)beginp = new();assert(p.randomize());transmit(p);endjoin_noneendtasktask transmit(input Packet p);...endtaskendclassGen_drive gen;
initial
begingen = new();gen.run(10);...
end

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

7.1.5 动态线程

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

7.1.6 线程中的自动变量

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

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

//改良代码
initial
beginfor(int j=0;j<3;j++)forkautomatic int k=j;$write(k);join_none#0 $display
end

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

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

7.1.7 等待所有衍生线程

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

task run_threads;...forkcheck_trans(tr1);check_trans(tr2);check_trans(tr3);join_nonewait 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_noneendtassendclassint i;        //共享的程序级变量iBuggy b;event receive;initialbeginb=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线程内所有子线程forkbeginfork:timeout_blockbeginwait(bus.cb.addr == tr.addr);$display("@%0t: Addr match %d", $time, tr.addr);end#TIME_OUT $display("@%0t: Error: timeout", $time);join_anydisable timeout_block;endjoin_any
endtask

7.2.2 停止多个线程

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

initial
begincheck_trans(tr0);          //线程0fork                       //线程1begin  check_trans(tr1);       //线程2fork                   //线程3check_trans(tr2);  //线程4join//disable语句结束fork...join线程及其所有子线程#(TIME_OUT/2) disable fork;endjoin
end//使用带标号的disable来停止所有线程
initial
begincheck_trans(tr0);forkbegin:thread_innercheck_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);
endinitial
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);
endinitial
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;endfunctiontask run();forkbegin...->done;endjoin_noneendtask
endclassprogram automatic test;event gen_done;Generator gen;initialbegingen = new(gen_done);gen.run();wait(gen_done.triggered());end
endprogram

7.3.1.5 等待多个事件

event done[N_GENERATORS];
initial
beginforeach(gen[i])begin//创建多个发生器gen[i] = new();gen[i].run(done[i]);endforeach(gen[i])forkautomatic 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函数,线程数+1thread_count++;forkbegin...//执行完毕所有代码后,线程数-1thread_count--;endjoin_noneendtask
endclassGenerator gen[N_GENERATORS];
initial
beginforeach(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;                //创建一个旗语initialbeginsem = new(1);          //分配一个钥匙forksequencer();sequencer();joinendtask sequencer;repeat($urandom()%10) @bus.cb;   //等待0~9个周期sendTrans();                      //执行总线事务endtasktask 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);endentaskendclassclass Consumer;task run;int i;repeat (3)beginmbx.get(i);$display("Consumer: after get (%0d)", i);-> handshake;         //如果这个先执行,@handshake陷入永久阻塞???endendclassProducer p;Cosumer c;initialbeginmbx = new();p = new();c = new();forkp.run();c.run();joinend
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);endendtaskendclassclass Consumer;task run();int i;repeat(3)begin$display("Consumer: before get");mbx.get(i);$display("Consumer: after get (%0d)", i);rtn.put(-i);endendtaskendclassProducer p;Cosumer c;initialbeginmbx = new();rtn = new();p = new();c = new();forkp.run();c.run();joinend
endprogram

参考文献:

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

SV绿皮书笔记(七)相关推荐

  1. SV绿皮书笔记(九)暂时完结

    第九章. 功能覆盖率 9.1覆盖率类型 功能覆盖率:功能覆盖率和设计意图是紧密相连的.用来衡量哪些设计特性已经被测试程序测试过的一个指标. 代码覆盖率:包括行覆盖率,路径覆盖率,翻转覆盖率,状态机覆盖 ...

  2. SV绿皮书笔记(四)

    第四章. 连接设计和测试平台 4.1 测试平台和DUT之间通信 DUT和测试平台(Test)通常是分开的模块(module,描述硬件),可以在顶层(top)中将DUT和Test例化,然后根据对应信号进 ...

  3. SV绿皮书笔记(六)

    第六章. 随机化 6.1 哪些对象需要随机化 随机时需考虑设计输入的各个方面,器件配置,环境配置,原始输入数据,封装后的输出数据,协议异常,延时,事务状态,错误和违例等情况. 6.2 SV中的随机化 ...

  4. SV绿皮书笔记(五)

    第五章. 面向对象编程基础 5.1 OOP概述 V属于过程性编程语言(代码逐行执行,无数据结构,类似C语言),V中没有结构,只有位向量和数组.而在对总线事务建模时往往需要数据结构,使用过程性语言不够便 ...

  5. SV绿皮书笔记(二)

    第二章. 数据类型 2.1 基本数据类型的两个属性 双状态/四状态:根据存储数据类型中的每一bit位的可能数分为双状态类型和四值状态类型. 双状态:可能值0,1,双状态默认初始值为0.双状态值具有更低 ...

  6. SV绿皮书笔记(三)

    第三章. 过程语句和子程序 3.1 循环语句 continue:在循环中跳出本轮循环剩下的语句直接进入下一轮循环. break:用于终止并跳出循环 bit[127:0] cmd; int file, ...

  7. SV绿皮书笔记(一)

    第一章. 验证导论 1.1 验证概述 验证的目的:保证设计文件能够完成预期的功能,寻找设计漏洞. 验证计划:参考设计文档,描述需要验证哪些特性,采用什么样的技术. 验证的流程:设计文档(自然语言)→设 ...

  8. Typescript 学习笔记七:泛型

    中文网:https://www.tslang.cn/ 官网:http://www.typescriptlang.org/ 目录: Typescript 学习笔记一:介绍.安装.编译 Typescrip ...

  9. 吴恩达《机器学习》学习笔记七——逻辑回归(二分类)代码

    吴恩达<机器学习>学习笔记七--逻辑回归(二分类)代码 一.无正则项的逻辑回归 1.问题描述 2.导入模块 3.准备数据 4.假设函数 5.代价函数 6.梯度下降 7.拟合参数 8.用训练 ...

最新文章

  1. Android RecyclerView添加Header头部
  2. A Learned Representation for Artistic Style论文理解
  3. 自然语言处理数据集免费资源开放(附学习资料)
  4. Source insight关联QT库函数
  5. 微信小程序开发 使用高德地图(精准一些)
  6. VISIO2010界面介绍
  7. java中同步组件_Java并发编程(自定义同步组件)
  8. java lambda 循环list_Java8--Lambda表达式对List集合操作(一)
  9. 阿里云李克:边缘云技术发展与实践
  10. php_mysql操作
  11. security工作笔记003---SpringSecurity框架启动报错.bcrypt.BCryptPasswordEncoder‘ that could not be found.
  12. ios 自动缩小字体_如何避免iOS自动调整字体大小?
  13. python之--工具类方法
  14. mybatis-plus主键生成策略
  15. python测量 检测软件_pytest首页、文档和下载 - Python 测试工具 - OSCHINA - 中文开源技术交流社区...
  16. 写给零基础小白的网站开发入门
  17. js中的循环(跳过(continue)和中断执行(break))
  18. 《 孙子兵法 》“势”论的美学探析
  19. 脚崴了!又肿又疼怎么办?
  20. 普及游戏:小型团队如何赢得大赛

热门文章

  1. 2022-2027年中国步进电机制造行业发展监测及投资战略研究报告
  2. 基于视觉反馈的步进电机X-Y平台控制
  3. minecraft_如何解决Minecraft LAN游戏问题
  4. nginx php版本隐藏
  5. Windows Admin Center无法访问
  6. 被中国人误传了数千年的七句话 (转)
  7. EasyUI之treegrid学习
  8. 多变量线性回归(机器学习笔记三)
  9. vue-simple-uploader上传组件
  10. python Numpy 生成一个随机矩阵(整数型)