我们早在学习Verilog语言时就学过:相对于begin-end顺序执行的语句块,还存在fork-join并行执行的语句块。这些知识用起来已经很顺手了,但是当学习到SystemVerilog语言的时候,突然告诉你:其实fork-join还有两个失散多年的亲兄弟活着!那就是fork-join_any和fork-join_none!!!
这三个兄弟虽然长的比较像,但是其实性格是不一样的!他们的主要性格区别是他们对待称为“线程”的小朋友的态度上。“线程”小朋友是轻量级的“进程”,是程序调度的基本单位。假如某一时段同时来了好几个线程小朋友去他们家吃饭,三兄弟性格可以表现的非常明显。其中fork-join的性格是最温和耐心的,他会静静等待所有线程小朋友全部吃完饭才去做别的事情。而fork-join_any性格是最健忘和丢三落四的,当他看到其中某个线程小朋友吃完后会直接忘了别的小朋友还在吃,以为都完成了,直接去做自己的事情。至于fork-join_none则是脾气最暴躁的,他不会等待任何一个线程小朋友吃饭,会直接去搞自己的事情!
那fork-join_any、fork-join_none一个健忘症一个暴脾气,他们是猴子请来搞笑的吗?除了增加我们的概念记忆还有什么作用?
考虑遇到这样一种情况:当某些线程小朋友是无限循环的,永远吃不完,而下面我们就想结束所有的程序,那一直等下去有时就会出问题。 这时对于fork-join_any这个健忘症,就可以有用武之地了。我们常常设立一个吃的最快的线程小朋友作为“组长”,而fork-join_any通过等待“组长”来控制这段程序运行结束。
实际中的一个常见经典用法如下,(初学者不需要了解代码中每行的含义,只需要对fork-join_any的应用有一个直观认识即可)想在记分板中控制验证平台的结束时,通过配置num参数来控制想收到的实际数据包数量,运行完for循环就会执行drop_objection,结束平台运行。这里面的for循环就是我们前面提到的“组长”,如果没有fork-join_any,单纯的使用fork-join便会一直停不下来,是不能实现这个功能的了。
对于fork-join_none这个暴脾气,其实也是很有用处的。比如有这样一个需求:把某个相同的线程并行启动运行100个。这个需求用fork-join可以实现,但是你要在其中罗列100次这个线程:
这样写显然不合理,太麻烦了,如果更大的数那就是不可能完成的任务了。这时候fork-join_none就显示出了很好的作用,如下,配合for循环几行就可以达到启动的100个的目的。
值得一提的是,这两段代码作用其实是不等价的,通过fork-join_none运行的100个线程,是并行启动了,但是不等他们全部结束程序就会进行到后面的程序中去,如果想要等价可以在后面使用wait fork语句来等待所有线程结束,如下代码就与fork-join控制的完全等价了。
fork-join_none相信大家应该熟悉了,新来的朋友可以回顾下jerry之前的文章,就是之前jerry提到的那个“暴脾气”的哥们,他不会去等别人,直接会着急做自己的事情。
前文回顾(点击查看):fork-join挺好用的了,fork-join_any、fork-join_none有什么用?
回顾下那篇文章中我们举的一个例子,这个暴脾气可以这么用:
fork
aa( );
aa( );
aa( );
……
aa( );
aa( );
join
如上代码,我们想要并行的执行100个 aa( )这个函数进程。通过fork-join要写到手软,用这个暴脾气,几句话就搞定:
for(int i=0; i<100; i++)
fork
aa( );
join_none
但是,今天jerry告诉各位初学者,这个暴脾气有不好驾驭的那一面的哦,弄不好就很容易翻车!!
什么情况下容易翻车呢?
大家仔细看看上面的例子,并行运行的aa( ),都是一样的内容,放在for循环中,却并没有使用for循环的循环因子 i 啊~
有人说,这有什么关系吗?
好的,来,看看jerry今天准备的代码,逼出它的邪恶面!
我们还是通过暴脾气fork-join_none,外加for循环,这次我们用上for的循环因子i,
怎么用i呢?我们直接通过$display来打印,打出10个选美者的编号和颜值等级:
for (int i=0 ; i<10; i++)
fork
$display(“No%0d,My face_grade is %0d”, i ,i );
join_none
大家先不看答案,先猜猜,这个会怎么打?
算了,jerry先猜猜你们是怎么想的?是不是打印出下面这样?
No0,My face_grade is 0
No1,My face_grade is 1
No2,My face_grade is 2
No3,My face_grade is 3
……
No9,My face_grade is 9
大错特错!!太天真了!这个时候这个暴脾气只会在电脑的某个角落里看着你笑着说“愚蠢的人类”!!
jerry告诉你打印出来的是什么:
No10,My face_grade is 10
No10,My face_grade is 10
No10,My face_grade is 10
……
No10,My face_grade is 10
太阴险了!怎么会是这样呢?我0-9怎么还出来10了?
先解释下这个for循环范围0-9,怎么打出来10了?
看下这段代码:
int apple_num;
for (apple_num=0 ; apple_num<10; apple_num++);
$display(“i have %0d apples”, apple_num );
直接告诉大家,这个代码打印出来的是:
i have 10 apples
这个代码,for循环是执行的一个空语句,for结束后才进行打印循环因子。让不注重细节的伙伴们再认识下for,for在最后执行完成他的值是还要再走apple_num++的,正是因为加到了10,才不满足apple_num<10的条件不再进行循环下去了。
我们再回头看看这个代码:
for (int i=0 ; i<10; i++)
fork
$display(“No%0d,My face_grade is %0d”, i ,i );
join_none
现在知道这个打印出来10是怎么来的了,是for循环执行完了以后循环因子i的值啊!!
好像差不多理解了:for循环的时候依次创建了10个进程,然后等for循环结束后,才并行执行10个fork进程。
因为fork-join_none,for全部循环完了以后, 10个 d i s p l a y ( “ N o display(“No%0d,My face_grade is %0d”, i ,i ); 才并行的执行完!!在打印的时候得到的i值就是最后的10了。换句话理解:这10个并行的 display(“Nodisplay里面的i其实是同一个int i,i++是会改变它的。
来,jerry先直接告诉各位怎么解这个问题:
for (int i=0 ; i<10; i++)
fork
automatic int j=i;
$display(“No%0d,My face_grade is %0d”, j ,j );
join_none
这段代码打印的正是我们期望的:
No0,My face_grade is 0
No1,My face_grade is 1
No2,My face_grade is 2
No3,My face_grade is 3
……
No9,My face_grade is 9
为什么呢?我们来分析一下:
如上代码,我们加了一个automatic int j=i 转了一下,把i给j,我们打印j。
此处automatic类型,意味着进入fork进程被创建,结束被撤销。保证了10个并行的display语句,每个进程中的j是它自己的,是独一无二的(不清楚automatic和static的区别的可以自己查或者关注jerry后面的文章哈)。
先不要恍然大悟,仔细想想,仅仅保证了独一无二,就行了?automatic int j=i;
这句话还没执行,for不就应该执行完了?那这里的i岂不是还是应该是10??
是啊!除非……?
没错!
automatic int j=i;在i++之前就执行了!!!
我们来验证下,假如这么写:
for (int i=0 ; i<10; i++)
fork
#0;
begin
automatic int j=i;
$display(“No%0d,My face_grade is %0d”, j ,j );
end
join_none
果然就又出错打印成下面这样了!!!
No10,My face_grade is 10
No10,My face_grade is 10
No10,My face_grade is 10
……
No10,My face_grade is 10
其实不要说那样,就即便如下这样都是不对的!!
for (int i=0 ; i<10; i++)
fork
automatic int j;
j=i;
$display(“No%0d,My face_grade is %0d”, j ,j );
join_none
看来除了保证独一无二,更关键的原因是执行顺序!!
什么执行顺序呢?
简单的说,如果把我们这段代码理解为两个过程:“创建进程”、“执行进程”。
创建进程的时候,创建10个并行的进程,然后统一执行。
这句神奇的automatic int j=i;偏偏就是在创建进程的过程中就执行了!
大家可以看一下下面的视频,DVE上的断点单步调试,上面提到的两种代码执行顺序对比:
先执行的94行for进入第一段代码“创建线程”阶段,然后马上95行神奇的automatic int j=i;可见它也是第一段代码“创建线程”阶段执行的!然后并没有接着执行96行的display,而是101行的for!进入了第二段代码的“创建线程”阶段!线程都创建完成之后才再回去96行进入执行进程阶段,执行了display,最后执行了102行的display。
各位初学者可以这样简单的理解这段代码,但是其实呢要更进一步探究就涉及到了
sv的仿真调度机制!!!
先简单看一眼,就是这些个东西啦:
我擦,短短几句代码需要想到这么多知识吗?这里这个调度机制我们就先不深究了,大家先擦擦汗,jerry后面的文章会娓娓道来的~
我们回到今天要讲的重点:“for循环+fork-join_none结构”的坑,怎么处理呢?最简单的一种方式就是用一个automatic int j=i 转一下,一定要在fork的一开始定义,并且赋值。
SystemVerilog允许大家在使用fork + join/join_any/join_none创建进程之后,通过disable fork来提前结束这些进程。
例如下面的代码片段1,fork + join_any产生了两个并行的子进程:
第一个子进程等待valid信号,然后打印第12行的信息;
第二个子进程等待max_delay个ns,然后打印第16行的信息。
不论是哪一种结果,都会导致join_any跳出fork,接着执行disable fork来结束这个fork进程及其子进程。
代码片段1
这个task在等待valid的同时,为了避免长时间等待,加了一个超时机制。不论是等到valid,还是超时了,都不必再等待另一个子进程继续执行下去。这段代码乍一看好像没什么问题啊?
别急,继续往下看。
假如还有另一个task B,需要在启动task A之前启动,常见的做法就是先fork + join_none的方式启动B,再启动A。
执行task C,会惊奇的发现:不论task A里面是否wait valid成功,当执行后面的disable fork之后,task B始终都没有打印第27行的信息?
为什么会这样?是不是开始怀疑人生了?
别急,这是因为当disable fork的时候,不仅杀掉了task A里面的fork进程,连task C里面的fork/join_none进程也杀死了。disable的杀伤力,远远超出了想象,有没有?
不是我不小心,只是……
要避免这样的误杀,办法其实很多。最常见的做法是添加所谓的guard fork,来限制disable fork的作用范围。
代码片段3
还有一种不太好做法是给fork的进程添加别名,然后disable这个指定的进程,如下面的代码片段4所示:
代码片段4
这种做法看似也OK,但是会引入另外一种风险。思考一下,不知道你是否猜到了?
Q哥带你揭晓答案。
如下面所示的代码片段5,task D里面通过fork join同时启动了两个调用task A的子进程并行执行。当调用A(1000)执行到disable p1的时候,会惊奇地发现,A(2000)也被意外地终结了。
代码片段5
给fork进程命名,弄巧成拙了。推荐大家还是使用guard fork,这是一种良好的coding style。