欢迎光临散文网 会员登陆 & 注册

验证基础-线程的控制和同步

2022-05-14 16:43 作者:不吃葱的酸菜鱼  | 我要投稿

        按照软件的思维理解硬件仿真,仿真中的各个模块首先是独立运行的线程(thread)

        模块(线程)在仿真一开始便并行执行,除了每个线程会依照自身内部产生的事件来触发过程语句块之外,也同时依靠相邻模块间的信号变化来完成模块之间的线程同步。  

        线程,其实就是独立运行的程序。在module中的initial和always,都可以看做独立的线程,它们会在仿真0时刻开始,而选择结束或者不结束。        

        有关线程,我们需要先知道几个概念:任何的线程都应该有父线程,父线程可以开辟若干个子线程,父线程可以暂停或者终止子线程,子线程终止时父线程可以继续执行,但父线程终止时,其所开辟的所有子线程都应当会终止。

        多线程的同时工作,显然会对仿真资源造成一定的消耗,那么如何降低仿真时的内存消耗呢?

        1.可以降低模块之间的信号跳变频率(减少该信号的跳变频率,可以减少该事件触发的逻辑,从而降低资源消耗)、2.只在必要的时候创建软件对象、3.在不需要时钟的时候关闭时钟、4.在数据带宽需求低的时候降低时钟频率。

多线程函数 fork...join

        软件环境中的initial块对语句有两种分组方式,使用begin......end 或者 fork......join。其中begin.......end中的语句以顺序方式执行,而fork......join中的语句则以并发方式执行,其中fork......join又有多种类型比如fork......join_any、fork......join、fork_join_none三种。

        对于fork......join,开辟的子线程,必须全部执行完,fork语句才结束;fork......join_any是fork语句块中,有一个线程执行完了,fork语句块就会退出并执行后面的语句,但是fork....join_any中还没执行完的线程还会继续执行下去;fork......join_none只要fork一起头,那么就会离开fork....join_none语句块执行后面的语句,fork_join_none中的线程也会执行下去,fork......join_none就像点火一样,点个火就走。

        但是上述没执行完的语句还会继续执行的说法有个例外,就是在 fork-join_any 如果完成了 fork 内部的一条语句后会执行 fork-join_any 后的语句我们是知道的,但是如果 fork-join_any 后面没有语句,直接是initial语句块的end,那么因为已经执行到end了,所以哪怕fork-join_any里面语句还没执行完,那也结束了,直接被截断,不被执行了

        在上面代码的fork-join语句块中,有四条子线程,display、#50、#10、begin...end,四条线程并行执行,耗时最长的是#50线程,在fork-join语句块前还延时了10个时间单位,所以fork-join语句块会在第60个时间单位的时候完成运行。

运行结果如上。

        在SV中,当程序的 initial 块全部执行完毕,仿真器就退出了。如果我们希望 fork 块中的所有线程都执行完毕再退出结束 initial 块,我们可以使用 wait fork 语句来等待所有子线程结束。

        相反,如果fork-join_any或fork-join_none后想终止目前还未执行的完毕的线程,可以使用disable来指定需要停止的线程。

        给一个fork块命名,想要终止该fork块的所有线程的时候,使用disable + 块名就能终止线程。但是如果你给某一任务或者线程指定了名字,那么当这个线程被调用多次以后,如果通过disable去禁止这个线程名,那么所有衍生的同名线程都将被禁止

线程中的通信

        测试平台中的所有线程都需要同步交换数据。一个线程可能需要等待另一个线程执行完毕,多个线程可能同时访问同一个资源线程之间可能需要交换数据。所有这些数据交换和同步称之为线程间的通信(IPC,Interprocess Communication)。

event 事件

        Verilog 中,一个线程总是要等待一个带@操作符的事件。这个操作符是边沿敏感的,所以它总是阻塞着、等待事件变化。

        其它线程可以通过->操作符来触发事件,结束对第一个线程的阻塞。

仿真结果:

第一个初始化块启动,触发e1事件,然后阻塞在e2上。

第二个初始化块启动,触发e2事件,然后阻塞在e1上。

        e1和e2在同一被触发,但是实际上仿真有一个delta cycle的时间差,使得第二个initial语句块无法等到e1,但是第一个initial语句块可以等到e2。

        所以为了解决上面的问题,可以使用event自带的函数triggered(),这个函数是电平敏感的,@e1是边沿敏感的。如果事件在当前时间点已经被触发过了,那么就不会引起阻塞。

这样修改后,仿真结果为:

        语句$display("@%0t: 2: after trigger",$time);不会被阻塞,因为 triggered() 是电平敏感的函数语句,看名字也可以看的出来,“被触发”,可以看出该事件是否被触发过。这种使用triggered函数的方法,比起使用@ 而言,可以保证同一时刻只要 event 被触发过,就不会引起阻塞。而@是边沿敏感的,可以被多次触发,所以我们需要根据情况来选择是使用 @ 还是 wait(e.triggered()) 来等待事件。

        我们可以先使用wait(A.triggered()),等到event A被触发,或者在某一时刻不分time step的先后有wait和A触发,那么哪怕wait的时刻比触发时刻晚,wait函数也会被满足。但是如果event A 已经触发,再使用wait(A.triggered()),且不是在一个时刻,而是有时间长度的先后关系,则wait会被阻塞。

        综上:使用wait(A.triggered())的方式,可以避免在相同时刻触发event而带来的竞争问题,但同样无法捕捉已经被触发,但后续才等待的事件。


semaphore旗语

        semaphore可以实现对同一个资源的访问控制。对于初学者来说,无论线程之间在共享什么资源,都应该使用semaphore等资源访问控制的手段,来避免多线程同时访问同一块资源。    

        semaphore 有三种基本操作。 new() 方法可以创建一个带单个或者多个钥匙的semaphore,使用 get() 可以获得一个或者多个钥匙,而 put() 可以返回一个或者多个钥匙。如果你试图获取一个semaphore而希望不被阻塞,可以使用 try_get() 函数,它返回1表示有足够多的钥匙,而返回0则表示钥匙不够。

        可以看到上面的代码,在fork-join中,同时使用了两个总线线程sequencer。而sequencer里面又调用了sendTrans任务,会对总线进行同时访问对总线进行操作(bus.cb.addr <= t.addr),这就产生了冲突,所以我们利用旗语来解决这个冲突,我们要保证一次只有一个线程对总线进行操作。利用旗语,只有一把钥匙,那么一定有一个线程先拿到钥匙,拿到钥匙的线程就能对总线进行操作,没拿到钥匙的线程就只能等上一个线程归还钥匙后,才能取得钥匙再对总线进行操作。

        使用旗语时要注意,你返回的钥匙可以比你取出来的钥匙多,因为旗语有一个bug就是你手里没有钥匙的时候你也可以归还钥匙,这就很滑稽,所以你可能会突然间有两把钥匙而实际上只有一辆汽车,。当你的测试程序需要获取和返回多个钥匙时,务必谨慎,假设你剩下一把钥匙,有一个线程请求两把钥匙而被阻塞,这时第二个线程出现,它只请求一把,那么这个get(1)因为满足要求而悄悄排到get(2)前面而获得一把钥匙,但是事实上这并不符合我们先进先出的规则,所以务必谨慎。

如果要解决旗语的这个问题,可以参考如下代码:

上面定义了两个类,用来解决旗语冲突与设置旗语,下面就是正式使用的模块。

mailbox 信箱

        线程之间如果传递信息,可以使用mailbox。mailbox和队列queue有相似之处,mailbox是一种对象,因此也需要使用new()来例化,例化时有一个可选的参数size限定其存储的最大数量。如果size是0或者没有指定,则信箱是无限大的,可以容纳任意多的条目。

        使用put()可以把数据放入mailbox,使用get()可以从信箱移除数据。如果信箱为满,则put()会阻塞,如果信箱为空,则get()会阻塞。peek()可以获取对信箱里数据的拷贝而不移除它。

        mailbox在例化时,通过new(N)的方式可以使其变为定长(fixed length)容器,这样负载到达N后,无法再对其进行写入。如果采用new()的方式,则表示信箱容量不限大小

        线程之间的同步方法需要注意,哪些是阻塞方法,哪些是非阻塞方法,即哪些是立即返回的,而哪些可能需要等待时间

        创建了一个只能存放单条信息的具有最小容量的信箱。此为定容信箱,如果你试图往信箱里放入多于设定容量的物品,则put会阻塞,直到你从邮箱里搬走物品腾出空间。

验证面试高频题:mailbox 和 队列 有什么区别?

        malibox和队列很像,但是也存在区别,mailbox必须通过new()例化,而队列只需要声明即可。mailbox可以将不同的数据类型同时存储,不过不建议这么做;而队列它内部存储的元素类型必须一致

        mailbox的存取方法put()和get()是阻塞方法,使用它们时结果不一定会立即返回,而队列对应的存取方式push_back()和pop_front()是非阻塞的,会立即返回值。因此在用队列取数时,最好加上一句wait(queue.size > 0) 以免对空的队列进行了取数操作。

        mailbox只能够用作FIFO,而queue除了按照FIFO使用,还有其它应用的方式例如LIFO(last in First Out)。

         对于mailbox变量的操作,在传递形式参数时,实际传递并拷贝的是mailbox的指针;如果使用queue时,关于queue的形式参数的声明是ref类型还是默认的input类型需要额外考虑,如果是input类型,那么传递过程中发生的是数据的拷贝,以至于方法内部对queue的操作并不会影响外部的queue本身。如果是ref类型,则相当于是传递了队列的指针,方法内部对queue的修改也会影响到外部的queue


event、semaphore、mailbox区别:

        event:最小信息量的触发,即单一的通知和功能。可以用来做事件的触发,也可以多个event组合起来用来做线程之间的同步。

        semaphore:共享资源的安全卫士。如果多线程间要对某一公共资源做访问,即可以使用这个要素。

        mailbox:精小的SV原生FIFO。在线程之间做数据通信或者内部数据缓存时可以考虑使用此元素。

















  

验证基础-线程的控制和同步的评论 (共 条)

分享到微博请遵守国家法律