线程使用
对于硬件的过程块,它们之间的通信可理解为不同逻辑/时序块之间的通信或者同步,是通过信号的变化来完成的。Verilog通过always, initial过程语句块和信号数据连接实现进程间通信。
对于软件,仿真中的各个模块首先是独立运行的线程(thread),模块(线程)在仿真一开始便并行执行,除了每个线程会依照自身内部产生的事件来触发过程语句块之外,也同时依靠相邻模块间的信号变化来完成模块之间的线程同步。
1、什么是线程
线程即独立运行的程序。 线程需要被触发,可以结束或者不结束。在module中的initial和always, 都可以看做独立的线程,它会在仿真0时刻开始, 而选择结束或者不结束。
硬件中的每一个always语句块,可以看成是独立运行的线程,而这些线程会一直占用仿真资源, 因为它们并不会结束。 软件测试平台中的验证环境都由initial语句块去创建,而在仿真过程中,验证环境中的对象可以创建和销毁, 因此软件测试端的资源占用是动态的。
2、begin-end和fork-join
begin-end中的语句是以顺序方式执行,而fork-join中的语句则是并发方式执行,除此之外还有fork-join_any和fork-join_none。
3、子线程和父线程
线程的执行轨迹是呈结构的, 即任何的线程都应该有父线程。 父线程可以开辟若干个子线程, 父线程可以暂停或者终子线程。 当子线程终止时,父线程可以继续执行。当父线程终止时 其所开辟的所有子线程都应当会终止。
线程控制
1、fork并行线程语句块
fork-join:所有的子线程全部结束,fork-join才会退出。
fork-join_any:任何一个线程结束,fork-join_any就会退出,但是剩下的还是会执行。
fork-join_none:不用等待任何线程运行结束,直接退出,但是剩下的线程还是会运行。
2、等待所有衍生线程
SV中,当程序中的initial块全部执行完毕,仿真器就退出了。如果我们希望等待fork块中的所有线程执行完毕再退出结束initial块,我们可以使用wait fork语句来等待所有子线程结束
3、停止单个线程
可以使用disable来指定需要停止的线程
fork:timeout_block
.........
join_any
disable fork:timeout_block;
4、停止多个线程
disable fork可以停止从当前线程中衍生出来的所有子线程,如果没有声明希望停止线程的名称,就需要注意disable的涉及范围,一般会涉及到外层的fork...jion。稳妥的方法就是使用带标号的disable,明确指出希望停止的线程名称,
5、停止被多次调用的任务
如果你给某一个任务或者线程指明标号, 那么当这个线程被调用多次以后,如果通过disable去禁止这个线程标号,所有的同多线程都将被禁止。
6、动态线程
SV里面可以进行动态线程的创建,而不用等待他们都执行完毕。可以建立一个task,然后再调用task的时候就会产生一个动态的线程。P185
7、线程中的自动变量
如果在创建task的时候使用的是静态存储,那么线程降火共享一个变量,后面进行的调用会自动覆盖前面调用产生的值。但是如果代码是在使用自动存储的程序或者模块里面,那么声明就不需要关键词automatic了,因为模块已经声明过了。
8、线程间的共享变量
例如for循环里面的i变量,多个for循环如果不注意的话,就会都是用i变量,解决方法是在包含所有变量使用的最小范围内声明变量,或者是使用foreach语句。
线程之间的通信
线程之间的通信包含同步并交换数据,一个线程需要等待另外一个,多个线程同时访问同一个资源。
1、event事件
Verilog中,一个线程总是要等待一个带@操作符的事件。这个操作符是边沿敏感的, 所以它总是阻塞着、等待事件的变化。而其它线程可以通过->操作符来触发事件, 结束对第一个线程的阻塞。SV引入了triggered()函数来查询某个事件是否已经触发。
在事件的边沿阻塞:
打印出的的结果是1:before trigger + 2:before trigger + 1:after trigger 第四句display没有打印出来,是因为两个initial启动的时候分别会阻塞在e1和e2上面,e1和e2同时被触发,但是由于delta_cycle的时间差使得两个模块无法等到e1或者e2。
2、等待事件的触发
可以使用电平感的wait(e1.triggered())来替代边沿敏感的阻塞语句@e1如果事件在当前时刻已经被触发,则不会引起阻塞。否则, 会一直等到事件被触发为止 。
3、等待多个事件
可以使用wait fork来等待多个线程,或者通过计数来等待多个线程。
3、semaphore旗语
semaphore可以实现对同一资源的访问控制。semaphore有三种基本操作。 new()方法可以创建一个带单个或者多个钥匙的semaphore, 使用get()可以获取一个或者多 个钥匙, 而put()可以返回一个或者多个钥匙。 想要获取一个 semaphore并且不被阻塞,可以使用try_get()函数。它返回1表示有足够多的钥匙,返回0则表示钥匙不够。
4、mailbox信箱
线程之间如果需要传递信息可以使用mailbox,尤其是两个线程之间。mailbox是一种对象,因此也需要使用new()来例化。例化时可以选择参数size来限定其存储的最大数量。如果size是0或者没有指定,则信箱是无限大的, 可以容纳任意多的条目。 使用put()可以把数据放入mailbox,使用get()可以从信箱移除数据。 如果信箱为满,则put()会阻塞; 如果信箱为空则get()会阻塞 。 peek()可以获取对信箱里数据的拷贝而不移除它。在信箱中放入的是句柄,绝不可能是对象。
不同例化位置带来的影响。
定容信箱:缺省情况下,信箱类似于容量不限的FIFO,在声明new(1)时候,创建了一个定容信箱。定容信箱只是在两个线程之间扮演了一个缓冲器的作用。
5、实现线程同步
使用定容信箱和探视(peek)来实现线程的同步:
使用信箱和事件来实现线程的同步:
让两个线程使用握手信号,以使生产方不要超前于消费方。既然消费方以阻塞的方式等待生产方使用信箱,那么生产方也可以以阻塞的方式来等待消费方完成对信箱条目的处理。这可以通过在生产方增加阻塞语句如事件、旗语或第二个信箱来实现。
使用两个信箱来实现线程的同步:
使用另外一个信箱把另一方的完成信息返回给之前的一方,在MCDF实验中就有所展现。