学习记录14(线程间通信)重要
1.线程间通信的方式
事件event,旗语semaphore,信箱mailbox
2.学习目标
了解三种线程通信方法的用法,以及线程的使用和通信需求
3.线程的使用
verilog中的语句分组使用begin end和fork join,在SV中引入了fork join_any和fork join_none
这块在本子上总结了
fork---join:块内语句都是并行执行的,且所有块内语句执行完毕后才执行块后语句
fork---join_any:块内语句都是并行执行的,且块内任意语句或者任务执行完后就执行块后语句
fork---join_none:块内语句都是并行执行的,且将块内所有任务或者语句都启动之后就开始执行块后语句
4.线程间的通信

解决办法就是上面线程间通信三种方式。
1)事件:事件本身是一个句柄,用于进程间的同步,可同步多个并发的进程,比如某个进程等待着事件,而另一个进程则触发这个事件。
- @操作符是阻塞的,边沿触发的,当且仅当在等待时电平发生偏转才会执行其下的语句。也就是说过了操作的时间点,如果没有捕捉到,那么就会被阻塞在那里,直到下一次到来。如果没有下一次,就一直阻塞。(就例如等待事件是在10ns执行的,触发事件在10ns之前,等待事件就不能检测到这个触发事件。)
- wait也是阻塞的,不同点在于是电平触发,如果在等待前就已经触发,那就是非阻塞,等待后触发就是阻塞。
在SV中,引入了triggered属性,该属性可用于查询事件是否已经被触发,而不是只检测当前时刻事件是否触发,线程只等待该结果,而不是在@处阻塞。
triggered是事件的一个属性,当声明事件句柄ev后,只有当某个进程内部的某个动作触发事件->ev,才会让事件进入已触发(trigerred)的状态,也就是说triggered 不是“设置”开关,而是“查看”状态。它让你告别@的阻塞等待,只在触发时标记成已触发,唤醒所有在该事件上阻塞的线程(后续处理再通过阻塞方式等待triggered),从而让线程可以安全、非阻塞地继续执行。
场景 A:你用的是 if (ev.triggered) —— 非阻塞检查
已触发(True):线程把 if 条件里的代码执行一遍,然后继续往下走(执行)。
未触发(False):线程直接跳过 if 块(不执行里面的代码),继续往下执行后面的语句。它绝对不会去触发事件,也绝对不会卡住等你。
if (ev.triggered) begin
// 如果触发了,就执行这里,然后往下走
end
// 如果没触发,直接跳到这一行继续执行
场景 B:你用的是 wait(ev.triggered)或者@(ev.triggered) —— 阻塞等待(但不是主动触发)
已触发(True):wait 条件满足,线程通过,继续执行下一行。
未触发(False):线程挂起(阻塞),傻傻地等(等待其他线程通过 -> ev; 来触发它),绝对不触发,绝对不往下走。

可以被赋值给其他事件的意思是,事件的句柄1和句柄2指向的是同一个事件,可以与前面类的句柄赋值联系,这样触发任意一个句柄就能触发这个事件。
事件传递给队列主要是用于动态调度。比如你有一组待完成的异步操作,你不知道哪个先完成,但你可以把所有完成事件收集到队列里,然后轮询查看 ev_q[i].triggered 来判断哪一个已经完成了。
事件传递给函数/任务主要是封装公用等待逻辑。你不需要在每个线程里写 @(ev),只需要写一个任务,把事件传进去,任务就能复用等待逻辑(比如带有超时机制的 wait_with_timeout(ev, 100ns))。


理论上initial是同时进行的,同时仿真,但是在时间片尺度上intial块是有先后执行顺序的,从上到下。
注意@与wait在这里的应用,@是在如果等待后触发才通过,等待前触发的不通过,wait等待时就检查是否触发如果已经触发就通过


wait_order是指阻塞等待多个事件触发,且必须按照规定的顺序触发才能执行其下的语句,如果不满足就执行else语句,有点像if,if中没有顺序要求
2)信箱(mailbox):进程间信息交换的机制,通过一个进程发送到信箱并由另一个进程获取,数据可以是任何类型的数据,包括class数据类型。
创建mailbox需要通过new()创建,而event不需要,put()放信息(阻塞等待),try_put()尝试放信息(非阻塞等待),get获取信息,peek()看有没有可获取信息,try_get/try_peek是非阻塞的方式num()返回mailbox信箱里的信息数量。
mailboox声明方式也是通过句柄,一般是 mailbox #(类) 句柄。的方式


在产生器或者顶层中写一次new创建一个信箱就行,驱动器里的纯多余,因为写多了就不知道从哪里拿数据那里读数据。在测试代码中gen2drv=new()才是创建真正的信箱,后面两句是调用方法
3)旗语(semaphore):是一种对共享资源进行访问控制的机制,可以理解为钥匙。如果测试平台中存在一个资源,有多个请求使用的进程,而物理实现只允许单一物理驱动,可以使用旗语,这个与c语言里的互斥锁和二值信号量的概念类似。且如果线程请求时得不到钥匙,也就是说有进程在使用这个共享资源,就会进入阻塞等待,且以先进先出的方式进行排队。

new()指定钥匙的数量,如果没写数字,默认是0,没起作用
get()获取一个或者多个钥匙,put()释放持有的钥匙,让其他进程可以获取。try_get()尝试获取一个或者多个钥匙(非阻塞的获取方式),默认值都是1


舒适申请的时候如果存在多个进程同时申请,由于只有一把钥匙,谁抢到谁先执行,其余的阻塞等待,直到这个执行完毕,后续按照先进先出。
多旗语的使用场景:
1.假设你的 Driver 要发送一个 128 位数据,但物理总线每次只能传 64 位,且需要 2 个 “空闲发送缓冲区(Buffer)” 才能开始拼接。
不用多钥匙(危险):你先 get(1) 抢到 Buffer A,再去抢 Buffer B。如果 B 此时被人抢走了,你只占着 A 不放,但无法发送数据,导致资源死锁(A 被你锁死,B 被别人占着,大家都卡住)。
用 get(2)(安全):旗语检查总空闲 Buffer 是否 ≥ 2。如果是,瞬间把 2 个 Buffer 同时标记为“已占用”,你立刻开始发送。如果不够 2 个,你原地等待,不占用任何 Buffer,直到攒够 2 个为止。这就杜绝了“占着茅坑不拉屎”的死锁隐患。
2.强制独占式高负载任务(比如大数据包搬运)
DMA(直接内存访问)控制器一次突发传输需要占用 4 个数据通道 才能保证带宽。
你用 get(4),一次性拿到 4 个通道钥匙,开启传输。
如果只拿到 2 个,你就不干活(阻塞),让其他只需要 1 个通道的小任务先去执行。等凑齐 4 个后,你一次性垄断所有通道,快速完成大任务。
多旗语的感觉是那种计数信号量加事件组的概念,因为计数信号量本身就是带有计数性质,就是你来一次请求我计数一次,执行完一次减少一次,计数为0才是完全释放资源,而这个也是计数,不满足则阻塞。
浙公网安备 33010602011771号