线程的控制与同步
相关: 线程间同步和通信,event semaphore mailbox
1. 线程的使用
1.1. 什么是线程 ?
- 线程即独立运行的程序;
- 线程需要被触发,可以结束或者不结束;
- 在module中的initial和always,都可以看做独立的线程,它们会在仿真0时刻开始,而选择结束或者不结束;
- 硬件模型中由于都是always语句块,所以可以看成是多个独立运行的线程,而这些线程会一直占用仿真资源,因为他们并不会结束;
- 软件测试平台中的验证环境都需要有initial语句块去创建,而在仿真过程中,验证环境中的对象可以创建和销毁,因此在软件测试端的资源占用是动态的;
- 选择题1:下面关于仿真时程序和模块的说法哪些是正确的?
A:硬件的模块可以看作独立的程序块
B:initial和always可以看作独立的线程
C:always线程不会结束
D:initial线程一定会结束
- 选择题2:如果需要降低仿真时的内存负载,那么下面哪些措施是合理的?
A:降低模块之间的信号跳变频率
B:只在必要的时候创建软件对象
C:在不需要时钟的时候关闭时钟
D:在数据带宽需求低的时候降低时钟频率
- 软件环境中的initial块对语句有两种分组方式,使用begin-end或fork-join;
- begin-end中的语句顺序执行,而fork-join中的语句并发执行;
1.2. 线程的概念澄清
- 线程的执行轨迹是呈树状结构的,即任何的线程都应该有父线程;
- 父线程可以开辟若干个子线程,父线程可以暂停或者终止子线程;
- 当父线程终止时,其所开辟的所有子线程都应当会终止;
2. 线程的控制
2.1. fork并行线程语句块
2.2. 等待所有衍生线程
- 在SV中,当程序中的initial块全部执行完毕,仿真器就退出了;
- 如果希望等待fork块中的所有线程执行完毕再退出结束initial块,可以使用
wait fork
语句来等待所有子线程结束
task run_threads;
fork
check_trans(tr1); // 线程1
check_trans(tr2); // 线程2
check_trans(tr3); // 线程3
join_none
...
// 等待所有fork中的线程结束再退出task
wait fork;
endtask
注意:
- 上述task结束的情况下,SV中父线程结束了,并不会主动回收它开辟的子线程,僵尸线程
- 建议在fork-join_none、fork-join_any开辟的线程,一旦认为它没有必要了,一定要给他打上名字,标记记号,方便后期主动disable
2.3. 停止单个线程
- 在使用了fork-join_any或者fork-join_none以后,可以使用disable来指令需要停止的线程
parameter TIME_OUT = 1000;
task check_trans(Transaction tr);
fork
begin
// 等待回应,或者达到某个最大时延
fork: time_block
begin
wait (bus.cb.addr == tr.addr);
$display("@%0t: Addr match %d", $time, tr.addr);
end
#TIME_OUT $display("@%0t: Error: timeout", $time);
join_any
disable time_block
end
join_none
endtask
2.4. 停止多个线程
- disable fork可以停止从当前线程中衍生出来的所有子线程
initial begin
check_trans(tr0); // 线程0
// 创建一个线程来限制disable fork的作用范围
fork // 线程1
begin
check_trans(tr1); // 线程2
fork // 线程3
check_trans(tr2); // 线程4
join
// 停止线程1-4,单独保留线程0
#(TIME_OUT/2) disable fork;
end
join
end
`timescale 1ns/1ns
module tb;
task automatic child_t(int t, string name);
forever #(t*1ns) $display("@%0t: child thread [%s] say hello", $time, name);
endtask
task automatic parent_t(int t = 3, string name = "parent_thread", int loop = 10);
fork: child_thread
child_t(4, "child_thread_A");
child_t(5, "child_thread_B");
child_t(6, "child_thread_C");
join_none
repeat(loop) #(t*1ns) $display("@%0t: parent thread [%s] say hello", $time, name);
$display("@%0t: finish %s", $time, name);
endtask
initial begin: parent_thread
parent_t();
end
initial begin
#25ns;
disable parent_thread;
// $display("@%0t: disable parent_thread", $time);
#50ns;
$display("@%0t: finish current test", $time);
$finish;
end
endmodule
2.5. 停止被多次调用的任务
- 如果给一个任务或者线程指明标号,那么当这个线程被调用多次以后,如果通过disabe去禁止这个线程标号,所有衍生的同名线程都将被禁止
task wait_for_time_out(int id);
if(id == 0)
fork
begin
#2;
$display("@%0t: disable wait_for_time_out", $time);
disable 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);
end
join_none
endtask
initial begin
wait_for_time_out(0); // Spawn thread 0
wait_for_time_out(1); // Spawn thread 1
wait_for_time_out(2); // Spawn thread 2
#(TIME_OUT*2) $display("@%0t: All done", $time);
end
- 任务wait_for_time_out被调用三次,从而衍生了三个线程
- 线程0在#2延时之后禁止了该任务,而由于三个线程均是“同名”线程,因此这些线程都被禁止了,最终也都没有完成
3. 线程间的通信
3.1. 概述
- 测试平台中的所有线程都需要同步并交换数据
- 一个线程需要等待一个线程
- 多个线程可能同时访问同一个资源
- 线程之间可能需要交换数据
- 所有这些数据交换和同步称之为线程间的通信(IPC,Interprocess Communication)
3.2. event事件
- Verilog中,一个线程总是要等待一个带@操作符的事件。这个操作符是边沿敏感的,所以它总是阻塞着、等待着事件的变化
- 其他线程可以通过
->
操作符来触发事件,结束对第一个线程的阻塞
3.3. 在event的边沿阻塞
event e1, e2;
initial begin
$display("@%0t: 1: before trigger", $time);
-> e1;
@e2;
$display("@%0t: 1: after trigger", $time);
end
initial begin
$display("@%0t: 2: before trigger", $time);
-> e2;
@e1;
$display("@%0t: 2: after trigger", $time);
end
@0: 1: before trigger
@0: 2: before trigger
@0: 1: after trigger
- 第一个初始化块启动,触发e1事件,然后阻塞在e2上
- 第二个初始化块启动,触发e2事件,然后阻塞在e1上
- e1和e2在同一个时刻被触发,但由于detla cycle的时间差使得两个初始化块可能无法等到e1或者e2
- 所以,更安全的方式可以使用event的方法triggered()
3.4. 等待事件的触发
event e1, e2;
initial begin
$display("@%0t: 1: before trigger", $time);
-> e1;
wait(e2.triggered());
$display("@%0t: 1: after trigger", $time);
end
initial begin
$display("@%0t: 2: before trigger", $time);
-> e2;
wait(e1.triggered());
$display("@%0t: 2: after trigger", $time);
end
- 可以使用电平敏感的
wait(e1.triggered())
来代替边沿敏感的阻塞语句@e1 - 如果事件在当前时刻已经被触发,则不会引起阻塞。否则,会一直等到事件被触发为止
- 这个方法比起@而言,可以避免在相同时刻触发event而带来的竞争问题,但同样无法捕捉已经被触发,但后续才等待的事件;
3.5. 资源共享的需求
- 对于线程之间共享资源的使用方式,应该遵循互斥访问(mutex access)原则
- 控制共享资源的原因在于,如果不对其访问做控制,可能会出现多个线程对同一资源的访问,进而导致不可预期的数据损坏和线程的异常,这种现象成为之“线程不安全”
- 选择题1:下面关于semaphore的描述哪些是正确的?
A:使用之前应该使用new()对其进行初始化
B:如果semaphore初始化只有1个钥匙,那么2个对象同时请求时,只有1个对象可以获取
C:如果semaphore初始化只有2个钥匙,那么2个对象同时请求时,2个对象都可以获取
D:semaphore初始化时可以初始化为0个钥匙
- semaphore没有管家,如果只定义了一把钥匙,如果还了一把,再还一把,这是不合理的,但是再代码上时允许了,会继续累加
3.6. 通信要素的比较和应用
- event:最小信息量的触发。即单一的通知功能。可以用来做事件的触发,也可以多个event组合起来用作线程之间的同步;
- semaphore:共享资源的安全卫士。如果多线程间要对某一公共资源做访问,即可以使用这个要素;
- mailbox:精小的SV原生FIFO。在线程之间做数据通信或者内部数据缓存时可以考虑使用此元素;