17

棋子 · 9月26日

for循环+fork-join_none结构的坑,你有注意到吗?

1.  回忆下fork-join_none

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告诉各位初学者,这个暴脾气有不好驾驭的那一面的哦,弄不好就很容易翻车!!

image.png

2.  fork-join_none翻车现场

什么情况下容易翻车呢?
大家仔细看看上面的例子,并行运行的aa( ),都是一样的内容,放在for循环中,却并没有使用for循环的循环因子 i 啊~
有人说,这有什么关系吗?
好的,来,看看jerry今天准备的代码,逼出它的邪恶面!

image.png

我们还是通过暴脾气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了?

3.  再认识下for循环

先解释下这个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个$display(“No%0d,My face_grade is %0d”, i ,i ); 才并行的执行完!!在打印的时候得到的i值就是最后的10了。换句话理解:这10个并行的$display里面的i其实是同一个int i,i++是会改变它的。

4.  怎么防止它的翻车

来,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??

是啊!除非……?

image.png

没错!
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上的断点单步调试,上面提到的两种代码执行顺序对比:

image.png

先执行的94行for进入第一段代码“创建线程”阶段,然后马上95行神奇的automatic int j=i;可见它也是第一段代码“创建线程”阶段执行的!然后并没有接着执行96行的display,而是101行的for!进入了第二段代码的“创建线程”阶段!线程都创建完成之后才再回去96行进入执行进程阶段,执行了display,最后执行了102行的display。各位初学者可以这样简单的理解这段代码,但是其实呢要更进一步探究就涉及到了sv的仿真调度机制!!!

先简单看一眼,就是这些个东西啦:

image.png

我擦,短短几句代码需要想到这么多知识吗?这里这个调度机制我们就先不深究了,大家先擦擦汗,jerry后面的文章会娓娓道来的~

我们回到今天要讲的重点:“for循环+fork-join_none结构”的坑,怎么处理呢?最简单的一种方式就是用一个automatic int j=i 转一下,一定要在fork的一开始定义,并且赋值。

今天这个知识点很常见,很重要哦,希望各位初学者不要着急,像这样的知识点,jerry聊一个就记住一个就好,不怕慢只怕站~哈哈,继续关注jerry后面的文章,一起成长~加油!

作者:Jerry
文章来源:处芯积律

推荐阅读

更多IC设计干货请关注IC设计专栏。欢迎添加极术小姐姐微信(id:aijishu20)加入技术交流群,请备注研究方向。

推荐阅读
关注数
18556
内容数
1291
主要交流IC以及SoC设计流程相关的技术和知识
目录
极术微信服务号
关注极术微信号
实时接收点赞提醒和评论通知
安谋科技学堂公众号
关注安谋科技学堂
实时获取安谋科技及 Arm 教学资源
安谋科技招聘公众号
关注安谋科技招聘
实时获取安谋科技中国职位信息