SV线程

线程的使用

定义特性

  • 线程是独立运行的程序单元,线程(thread)与进程(process)经常互换使用
  • 每个initialalways过程块均派生一个独立线程,仿真0时刻开始执行
    • initial及显式线程可主动结束
    • always线程持续监控信号
  • 父线程可衍生多个子线程,如fork内嵌多个子块
    • fork-join结构可显式创建并行子线程,但必须等所有子线程执行完后才可进行后续的处理
    • begin-end结构以顺序结构执行,可以用于过程块initial中,或用作fork-join中的一个顺序子线程

并行线程

image-20250728173027785

  • fork-join:所有子线程结束才继续后续代码
    • 标准Verilog语句,适用于需要等待所有并发任务完成的场景
  • fork-join_any:任一子线程结束即可继续后续代码
    • SV引入的创建线程新方法,适用于等待首个任务响应,如超时控制等
  • fork-join_none:立即继续主线程,子线程后台并行
    • SV引入的创建线程新方法,适用于启动异步任务,如监测信号等
  • wait fork;语句放在fork-join及其变体后,用于等待所有子线程结束

动态线程

  • 动态线程是在运行时通过显示语法(fork-join及其变体)创建的子线程,而非静态编译时定义的线程(alwaysinitialfork-join
    • 允许类、任务或函数内部生成子线程,实现子线程与父线程并发运行
    • 若线程使用automatic存储类,变量在每次调用时独立分配内存,避免多线竞争
    • 子线程在启动后独立运行,可自动结束或通过同步机制(事件event、旗语semaphore、信箱mailbox)控制终止
    • 常用于测试平台的事务处理、验证场景的并行激励生成等
  • 下例中,测试平台产生随机事务并发送到被测设计中,在等待被测设计回复的过程中,同时也不停止随机数据的产生
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
program automatic test (bus_ifc.TB bus);
//省略接口代码
//当check_trans被调用时,将产生一个线程用来检测总线来获取匹配的事务数据
task check_trans(input Transaction tr);
fork
begin
wait (bus.cb.data == tr.data);
$display("@%0t: data match %d", $time, tr.data);
end
join_none //不阻塞的并发线程
endtask

Transaction tr; // 声明事务类型

initial begin
repeat (10) begin
tr = new(); //创建一个随机事务
`SV_RAND_CHECK(tr.randomize());
transmit(tr); //把事务发送到被测设计中
check_trans(tr); //并行等待被测设计回复
end
#100;
end
endprogram

类的线程

  • 在类中创建线程,要求线程控制权与任务逻辑分离
    • 类内线程不应该在父类中启动,而是在任务run里产生,受到到子类控制
    • 构造函数new()只用来对数值进行初始化,不启动任何线程
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Gen_drive;             //带任务run的发生器/驱动器
task run (input int n); //N个数据包的事务处理器
Packet p;

fork
repeat (n) begin //顺序重复n次代码块
`SV_RAND_CHECK(p.randomize());
transmit(p);
end
join_none //使用fork-join_none避免repeat(n)阻塞run()
endtask

task transmit(input Packet p);
...
endtask
endclass

Gen_drive gen;

initial begin
gen = new(); //调用构造函数,完成初始化
gen.run(10); //启动类内线程
... //启动检验、监测和其他线程
end

线程的停止

  • 在SV中,停止线程主要通过disable语句实现,避免使用暴力终止,如使用过时的stop()

停止内部线程

  • fork-join块内使用disable fork;停止所有子进程
    • 使用fork-join把目标代码包围起来以限制disable fork语句的作用范围
1
2
3
4
5
6
7
8
9
10
initial begin
check_trans(tr0); // 线程0,使用了动态线程中定义的一个任务
fork // 线程1
begin
check_trans(tr1); // 线程1.1,父线程为线程1
check_trans(tr2); // 线程1.2
end
#123 disable fork; // 除线程0外其余线程全部停止
join
end

停止指定线程

  • 为线程指定标签,使用带标签的disable语句
1
2
3
4
5
6
7
8
9
10
initial begin
check_trans(tr0); // 线程0
fork // 线程1
begin : thread1
check_trans(tr1); // 线程1.1
check_trans(tr2); // 线程1.2
end
#123 disable thread1; // 保留线程0
join
end
  • 停止指定fork-join块中所有线程
    • 在下例中,如果正确的总线来得足够早,则等wait结构先完成,fork-join_any得以执行,之后的disable结束剩余的线程
    • 如果正确的总线在TIME_OUT时延完成时没有到来,则会打印错误警告的信息,fork-join_any被执行,之后disalbe结束wait线程
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
parameter TIME_OUT = 1000ns;

task check_trans(input Transaction tr);
fork
begin
fork : timeout_block
begin // thread1 in timeout_block
wait (bus.cb.data == tr.data);
$display("@%0t: data match %d", $time, tr.data);
end
#TIME_OUT $display("@%0t: Error: timeout", $time); // thread2
join_any
disable timeout_block;
end
join_none // 产生非阻塞的线程
endtask

停止任务线程

  • 当从任务块内部停止该块时,将会停止所有由该任务启动的线程
    • 使用disable标签语句将停止所有所有使用这段代码的线程,不仅仅是当前线程
  • 在下例中,任务wait_for_time_out被调用三次,从而产生了三个线程
    • 线程0在#2ns延时后禁止了该任务,三个线程都启动了,但最终都停止了
    • 如果这个任务位于多次实例化的驱动器类中,则其中的disable标签语句将停止所有块
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
task wait_for_time_out (input int id);
if (id == 0)
fork
begin
#2ns;
$display("@%0t: disable wait_for_time_out", $time);
disabel wait_for_time_out;
end
join_none

fork : just_a_little
begin
$display("@%0t: %m: %0d entering thread", $time, id);
#TIME_OUT;
$display("@%0t: %m: %0d done", $time, id); // %m表示输出调用此语句的模块的层次路径
end
join_none
endtask

线程间通信

IPC概念

  • 每个线程都会和相邻的线程通信
    • 环境类需要知道发生器什么时候完成任务,以便及时终止测试平台还在运行的线程
    • 如下图测试平台环境中的发生器把激励传递给代理

image-20250724095858230

  • 所有数据交换和控制的同步被称为线程间的通信(Inter-Process Communication, IPC),本章介绍以下三种
    • 事件:用于控制多线程间的同步
    • 旗语:用于控制多线程对共享资源的访问
    • 信箱:用于控制多线程间的信息传递或者线程间通信

事件

事件操作

  • 触发事件(Trigger):通过->操作符通知事件发生
  • 等待事件(Wait):通过阻塞线程知道事件被触发
    • 边沿敏感型:@,检测事件的上升沿,从无触发到触发
    • 电平敏感型:SV引入wait(event_example.triggered),检测事件是否已经被触发过

事件竞争

  • 在同一仿真时间步内,多个线程因时间等待与触发顺序不确定而发生的竞争状态,导致在不同的仿真器中运行的结果可能不同
    • 下例中,先执行B?A永远阻塞
    • 先执行A?A需要捕获事件才继续
    • 同时执行?取决于仿真器
1
2
3
4
5
6
7
8
9
10
event my_event;
initial begin // 进程A
@my_event; // 事件等待
$display("Process A activated");
end

initial begin // 进程B
-> my_event; // 事件触发
$display("Process B triggered");
end
  • 若在循环中使用wait(handshake.triggered),需确保在下次等待所处的仿真时间步不能与本次等待的相同,否则代码进入一个零时延循环
    • 若时间步不变,.triggered不会重置,wait再次立即通过,形成无线循环
    • 可以使用边沿敏感型等待事件,避免.triggered不重置的问题
    • 或使用#0使进程挂起指导指定延时结束,当恢复执行,时间已推进
1
2
3
4
5
6
7
8
9
10
forever begin // 这是一个零时延循环,造成仿真的不确定
wait(handshake.triggered);
$display("Received next event");
end

forever begin // 使用边沿敏感或时延+电平敏感避免零时延循环
@handshake;
// #0 wait(handshake.triggered);
$display("Received next event");
end

事件传递

  • 在SV中,事件作为同步对象的句柄,可以传递给子程序
    • 允许在对象间共享事件,不用把事件定义成全局的,最常见的方式是把事件传递到一个对象的构造器中
    • 下例中,一个事件被事务处理器用来作为其执行完毕的标志信号
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
program automatic test;
class generator;
event done;
function new (input event done); // 从测试平台传来事件
this.done = done; // 同步句柄:测试平台中的gen_done与类中的done同步
endfunction

task run();
fork
begin
... // 创建事务
-> done; // 告知测试程序任务已完成
end
join_none
endtask
endclass

event gen_done;
generator gen;
initial begin
gen = new(gen_done); // 测试程序实例化
gen.run(); // 运行事务处理器
wait(gen_done.triggered); // 等待任务结束
end
endprogram

多事件等待

  • 上例中只有单个发生器释放单个时间,如果测试环境类有N个发生器,必须等待多个子线程完成
  • 传递事件句柄等待多事件
    • 使用wait fork来等待所有子线程约束
    • 也可以使用wait_order(event1, event2)来等待指定顺序的事件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
event done[N_GENERATORS];         // 定义N个发生器的阻塞事件

initial begin
foreach (gen[i]) begin
gen[i] = new(done[i]); // 创建N个发生器
gen[i].run(); // 发生器开始运行
end

foreach (gen[i])
fork
automatic int k = i;
wait (done[k].trriggered);
join_none

wait fork; // 等待所有触发事件完成
end
  • 通过对触发事件进行计数来等待多个线程
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
event done[N_GENERATORS];
int done_count;

initial begin
foreach (gen[i]) begin
gen[i] = new(done[i]); // 创建N个发生器
gen[i].run(); // 发生器开始运行
end

foreach (gen[i])
fork
automatic int k = i;
done_count++;
join_none

wait (done_count == N_GENERATORS); // 等待触发
end
  • 使用线程计数来等待多个线程
    • 使用类作用域分辨操作符:::用于明确指定访问某个类作用域内的成员
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Generator;
static int thread_count = 0;

task run();
thread_count++; // 启动另一个线程
fork
begin
... // 实际事务代码
thread_count--; // 事务完成时,线程数目减一
end
join_none
endtask
endclass

Generator gen[N_GENERATORS];

initial begin
foreach (gen[i])
gen[i] = new();

foreach (gen[i])
gen[i].run();

wait (Generator::thead_count == 0);
end

旗语

基本特性

  • 作用:一种同步机制,用于控制多线程对共享资源的访问
  • 类型:旗语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
2
3
4
5
6
7
8
9
10
semaphore sem = new(1); // 创建含1把钥匙的信号量
process_A: begin
sem.get(1); // 获取钥匙
shared_var = ...; // 安全访问共享变量
sem.put(1); // 归还钥匙
end
process_B: begin
sem.get(1); // 若钥匙被占用,则阻塞等待
//...
end

信箱

image-20250804105625565

基本特性

  • 作用:用于并发线程间的数据交换
  • 类型:信箱mailbox是SV内置类,维护一个有界或无界的消息队列
    • 有界:固定容量,满时阻塞;无界,理论上无限容量

操作方法

  • 创建信箱
    • 声明参数化信箱,可以强制邮箱使用一种数据类型
1
2
3
4
5
6
7
8
mailbox mbx;   // 声明信箱变量
mbx = new(); // 创建无界信箱
mbc = new(10); // 创建有界信箱,容量为10

mailbox #(int) int_mbx = new(); // 只能存放int类型的信箱
mailbox #(string) str_mbx = new(); // 只能存放string类型的信箱
int_mbx.put(42);
// int_mbx.put("hello"); // 编译错误,类型不匹配
  • 放入数据: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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
mailbox #(int) mbx = new(5);    // 容量为5的有界参数化信箱,指定存储整型

// 生产者进程
initial begin
for (int i=0; i<10; i++) begin
mbx.put(i); // 放入数据
$display("Produced: %0d", i);
end
end

// 消费者进程
initial begin
int received;
forever begin
mbx.get(received); // 获取数据
$display("Consumed: %0d", received);
end
end