SV线程
线程的使用
定义特性
- 线程是独立运行的程序单元,线程(thread)与进程(process)经常互换使用
- 每个
initial
或always
过程块均派生一个独立线程,仿真0时刻开始执行initial
及显式线程可主动结束always
线程持续监控信号
- 父线程可衍生多个子线程,如
fork
内嵌多个子块fork-join
结构可显式创建并行子线程,但必须等所有子线程执行完后才可进行后续的处理begin-end
结构以顺序结构执行,可以用于过程块initial
中,或用作fork-join
中的一个顺序子线程
并行线程
fork-join
:所有子线程结束才继续后续代码- 标准Verilog语句,适用于需要等待所有并发任务完成的场景
fork-join_any
:任一子线程结束即可继续后续代码- SV引入的创建线程新方法,适用于等待首个任务响应,如超时控制等
fork-join_none
:立即继续主线程,子线程后台并行- SV引入的创建线程新方法,适用于启动异步任务,如监测信号等
wait fork;
语句放在fork-join
及其变体后,用于等待所有子线程结束
动态线程
- 动态线程是在运行时通过显示语法(
fork-join
及其变体)创建的子线程,而非静态编译时定义的线程(always
、initial
、fork-join
)- 允许类、任务或函数内部生成子线程,实现子线程与父线程并发运行
- 若线程使用
automatic
存储类,变量在每次调用时独立分配内存,避免多线竞争 - 子线程在启动后独立运行,可自动结束或通过同步机制(事件
event
、旗语semaphore
、信箱mailbox
)控制终止 - 常用于测试平台的事务处理、验证场景的并行激励生成等
- 下例中,测试平台产生随机事务并发送到被测设计中,在等待被测设计回复的过程中,同时也不停止随机数据的产生
1 | program automatic test (bus_ifc.TB bus); |
类的线程
- 在类中创建线程,要求线程控制权与任务逻辑分离
- 类内线程不应该在父类中启动,而是在任务
run
里产生,受到到子类控制 - 构造函数
new()
只用来对数值进行初始化,不启动任何线程
- 类内线程不应该在父类中启动,而是在任务
1 | class Gen_drive; //带任务run的发生器/驱动器 |
线程的停止
- 在SV中,停止线程主要通过
disable
语句实现,避免使用暴力终止,如使用过时的stop()
停止内部线程
- 在
fork-join
块内使用disable fork;
停止所有子进程- 使用
fork-join
把目标代码包围起来以限制disable fork
语句的作用范围
- 使用
1 | initial begin |
停止指定线程
- 为线程指定标签,使用带标签的
disable
语句
1 | initial begin |
- 停止指定
fork-join
块中所有线程- 在下例中,如果正确的总线来得足够早,则等
wait
结构先完成,fork-join_any
得以执行,之后的disable
结束剩余的线程 - 如果正确的总线在
TIME_OUT
时延完成时没有到来,则会打印错误警告的信息,fork-join_any
被执行,之后disalbe
结束wait
线程
- 在下例中,如果正确的总线来得足够早,则等
1 | parameter TIME_OUT = 1000ns; |
停止任务线程
- 当从任务块内部停止该块时,将会停止所有由该任务启动的线程
- 使用
disable
标签语句将停止所有所有使用这段代码的线程,不仅仅是当前线程
- 使用
- 在下例中,任务
wait_for_time_out
被调用三次,从而产生了三个线程- 线程0在
#2ns
延时后禁止了该任务,三个线程都启动了,但最终都停止了 - 如果这个任务位于多次实例化的驱动器类中,则其中的
disable
标签语句将停止所有块
- 线程0在
1 | task wait_for_time_out (input int id); |
线程间通信
IPC概念
- 每个线程都会和相邻的线程通信
- 环境类需要知道发生器什么时候完成任务,以便及时终止测试平台还在运行的线程
- 如下图测试平台环境中的发生器把激励传递给代理
- 所有数据交换和控制的同步被称为线程间的通信(Inter-Process Communication, IPC),本章介绍以下三种
- 事件:用于控制多线程间的同步
- 旗语:用于控制多线程对共享资源的访问
- 信箱:用于控制多线程间的信息传递或者线程间通信
事件
事件操作
- 触发事件(Trigger):通过
->
操作符通知事件发生 - 等待事件(Wait):通过阻塞线程知道事件被触发
- 边沿敏感型:
@
,检测事件的上升沿,从无触发到触发 - 电平敏感型:SV引入
wait(event_example.triggered)
,检测事件是否已经被触发过
- 边沿敏感型:
事件竞争
- 在同一仿真时间步内,多个线程因时间等待与触发顺序不确定而发生的竞争状态,导致在不同的仿真器中运行的结果可能不同
- 下例中,先执行B?A永远阻塞
- 先执行A?A需要捕获事件才继续
- 同时执行?取决于仿真器
1 | event my_event; |
- 若在循环中使用
wait(handshake.triggered)
,需确保在下次等待所处的仿真时间步不能与本次等待的相同,否则代码进入一个零时延循环- 若时间步不变,
.triggered
不会重置,wait
再次立即通过,形成无线循环 - 可以使用边沿敏感型等待事件,避免
.triggered
不重置的问题 - 或使用
#0
使进程挂起指导指定延时结束,当恢复执行,时间已推进
- 若时间步不变,
1 | forever begin // 这是一个零时延循环,造成仿真的不确定 |
事件传递
- 在SV中,事件作为同步对象的句柄,可以传递给子程序
- 允许在对象间共享事件,不用把事件定义成全局的,最常见的方式是把事件传递到一个对象的构造器中
- 下例中,一个事件被事务处理器用来作为其执行完毕的标志信号
1 | program automatic test; |
多事件等待
- 上例中只有单个发生器释放单个时间,如果测试环境类有N个发生器,必须等待多个子线程完成
- 传递事件句柄等待多事件
- 使用
wait fork
来等待所有子线程约束 - 也可以使用
wait_order(event1, event2)
来等待指定顺序的事件
- 使用
1 | event done[N_GENERATORS]; // 定义N个发生器的阻塞事件 |
- 通过对触发事件进行计数来等待多个线程
1 | event done[N_GENERATORS]; |
- 使用线程计数来等待多个线程
- 使用类作用域分辨操作符
::
:用于明确指定访问某个类作用域内的成员
- 使用类作用域分辨操作符
1 | class Generator; |
旗语
基本特性
- 作用:一种同步机制,用于控制多线程对共享资源的访问
- 类型:旗语
semaphore
是SV内置类,维护一个可用资源数量计数器
操作方法
- 创建旗语:
semaphore sem = new(1);
- 包含两步操作:声明旗语变量
semaphore sem;
,创建旗语并初始化计数器为1sem = new(1);
- 包含两步操作:声明旗语变量
- 获取钥匙:
sem.get(1);
- 进程访问共享资源前,通过
get()
获取钥匙,减少计数器,若钥匙不足则阻塞等待 - 多个阻塞线程以先进先出(FIFO)的方式排队
- 进程访问共享资源前,通过
- 归还钥匙:
sem.put(1);
- 访问完成后,通过
put()
归还钥匙,增加计数器,允许其他进程获取 - 注意归还的钥匙可以比取出来的多
- 访问完成后,通过
- 尝试获取:
sem.try_get(1);
- 尝试获取一把钥匙,成功返回
1
,失败返回0
,非阻塞
- 尝试获取一把钥匙,成功返回
1 | semaphore sem = new(1); // 创建含1把钥匙的信号量 |
信箱
基本特性
- 作用:用于并发线程间的数据交换
- 类型:信箱
mailbox
是SV内置类,维护一个有界或无界的消息队列- 有界:固定容量,满时阻塞;无界,理论上无限容量
操作方法
- 创建信箱
- 声明参数化信箱,可以强制邮箱使用一种数据类型
1 | mailbox mbx; // 声明信箱变量 |
- 放入数据:
mbx.put(data)
- 生产方将
data
放入信箱,若信箱满则阻塞 - 数据可以是整数、任意宽度
logic
或句柄
- 生产方将
- 获取数据:
mbx.get(data)
- 消费方从信箱获取数据到
data
变量,同时在信箱中移除,若信箱空则阻塞
- 消费方从信箱获取数据到
- 尝试放入:
mbx.try_put(data)
- 尝试放入数据,成功返回1,信箱满返回0,非阻塞
- 尝试获取:
mbx.try_get(data)
- 尝试获取数据,成功返回1,信箱空返回0,非阻塞
- 探视数据:
mbx.peek(data)
- 探视信箱里的数据但不将其移除
- 允许接收方检查数据内容后再决定是否处理
- 允许同一数据可以被多个线程查看
- 获取数量:
mbx.num()
- 返回信箱中当前消息的数量
1 | mailbox #(int) mbx = new(5); // 容量为5的有界参数化信箱,指定存储整型 |