目录
一、线程的概念
- 线程的执行轨迹是呈树状结构的,即任何的线程都应该有父线程。
- 父线程可以开辟若干个子线程,父线程可以暂停或者终止子线程。
- 当子线程终止时,父线程可以继续执行。
- 当父线程终止时,其所开辟的所有子线程都会终止。
软件中的initial块对语句有两种分组方式:
(1)begin…end中的语句以顺序方式执行。
(2)fork…join中的语句以并发方式执行。
initial begin
$display("@%0t: start fork...join example", $time);
#10 $display("@%0t: sequential after #10", $time);
fork
$display("@%0t: parallel start", $time);
#50 $display("@%0t: parallel after #50", $time);
#10 $display("@%0t: parallel after #10", $time);
begin
#30 $display("@%0t: sequential after #30", $time);
#10 $display("@%0t: sequential after #10", $time);
end
join
$display("@%0t: after join", $time);
#80 $display("@%0t: finish after #80", $time);
end
//输出结果
@0: start fork...join example
@10: sequential after#10
@10: parallel start
@10: parallel after #10
@40: sequential after#30
@50: sequential after#10
@60: parallel after #50
@60: after join
@140:finish after #80
二、线程的控制
1.fork…join_any
在调度块内语句时,当并行块内的第一个语句完成后,父线程才继续执行,其他停顿的线程也得以继续。
initial begin
$display("@%0t: start fork...join_any example", $time);
#10 $display("@%0t: sequential after #10", $time);
fork
$display("@%0t: parallel start", $time);
#50 $display("@%0t: parallel after #50", $time);
#10 $display("@%0t: parallel after #10", $time);
begin
#30 $display("@%0t: sequential after #30", $time);
#10 $display("@%0t: sequential after #10", $time);
end
join_any
$display("@%0t: after join_any", $time);
#80 $display("@%0t: finish after #80", $time);
end
//输出结果
@0: start fork...join_any example
@10: sequential after#10
@10: parallel start
@10: after join_any
@20: parallel after#10
@40: sequential after#30
@50: sequential after#10
@60: parallel after #50
@90: finish after #80
2.fork…join_none
在调度块内语句时,父线程继续执行。
initial begin
$display("@%0t: start fork...join_none example", $time);
#10 $display("@%0t: sequential after #10", $time);
fork
$display("@%0t: parallel start", $time);
#50 $display("@%0t: parallel after #50", $time);
#10 $display("@%0t: parallel after #10", $time);
begin
#30 $display("@%0t: sequential after #30", $time);
#10 $display("@%0t: sequential after #10", $time);
end
join_none
$display("@%0t: after join_none", $time);
#80 $display("@%0t: finish after #80", $time);
end
//输出结果
@0: start fork...join_none example
@10: sequential after#10
@10: after join_none
@10: parallel start
@20: parallel after#10
@40: sequential after#30
@50: sequential after#10
@60: parallel after #50
@90: finish after #80
3.等待所有衍生线程
在SV中,当程序中的initial块全部执行完毕,仿真器就退出了。
如果我们希望等待fork块中的所有线程执行完毕再退出结束initial块,我们可以使用wait fork语句来等待所有子线程结束。
task run_theads;
...
fork
check_trans(tr1);//线程1
check_trans(tr2);//线程2
check_trans(tr3);//线程3
join_none
...
//等待所有fork中的线程结束再退出task
wait fork;
endtask
4.停止线程
停止单个线程
使用disable来指定需要停止的线程。
parameter TIME_OUT = 1000;
task check_trans(Transaction tr);
fork
begin
fork:timeout_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 timeout_block;
end
join_none
endtask
停止多个线程
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
停止被多次调用的任务
如果给某一个任务或者线程指明标号,那么当这个线程被调用多次以后,如果通过disable去禁止这个线程标号,所有衍生的同名线程都将被禁止。
三、线程间的通信
- event:单一的通知功能,可以用来做事件的触发,也可以多个event组合起来用来做线程之间的同步。
- semaphore:多线程间要对某一公共资源做访问时可以使用semaphore。
- mailbox:在线程之间做数据通信或者内部数据缓存时,可以考虑使用mailbox。
1.事件event
运行下方代码块时,第一个初始化块启动,触发e1事件,然后阻塞在另一个事件上;第二个初始化块启动,触发e2事件(唤醒第一个块),然后阻塞在第一个事件上。但是,因为第一个事件是一个零宽度的脉冲,所以第二个线程会因为错过第一个事件而被锁住。
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
使用电平敏感的wait(e1.triggered())来替代边沿敏感的阻塞语句@e1。 如果事件在当前时刻已经被触发,则不会引起阻塞,否则,会一直等到事件被触发为止。
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
@0: 1: before trigger
@0: 2: before trigger
@0: 1: after trigger
@0: 2: after trigger
在运行上述代码时,第一个块启动,触发e1事件,然后阻塞在另外一个事件上;第二个初始化块启动,触发e2事件(唤醒第一个块),然后阻塞在第一个事件上,从而得到上述的输出。
2.旗语semaphore
- semaphore可以实现对同一资源的访问控制。
- semaphore的三种基本操作
- new():创建一个带单个或多个钥匙的semaphore。
- get():获取一个或多个钥匙。
- put():返回一个或多个钥匙。
3.如果试图获取一个semaphore而希望不被阻塞,可以使用try_get()函数。其返回1表示有足够多的钥匙,而返回0则表示钥匙不够。
资源共享的需求:
对于线程间共享资源的使用方式,应该遵循互斥访问原则。原因在于如果不对其访问做控制,可能会出现线程不安全。
3.信箱mailbox
- 线程之间如果传递信息,可以使用mailbox。
- mailbox和队列queue有相似之处。
- mailbox是一种对象,因此也需要new()例化。例化时有一个可选的参数size来限定其存储的最大数量。如果size是0或者没有指定,则信箱是无限大的,可以容纳任意多的条目。
- 使用put()可以把数据放入mailbox,使用get()可以从信箱移除数据。
- 如果信箱为满,则put()会阻塞,如果信箱为空,则get()会阻塞。
- peek()可以获取对象信箱里数据的拷贝而不移除它。
- 线程之间的同步方法需要注意,哪些是阻塞方法,哪些是非阻塞方法,即哪些是立即返回的,而哪些是需要等待时间的。
对于mailbox的用法,和FIFO的使用很相似。而两者在使用时的差异在于:
-
mailbox必须通过new()例化,而队列只需要声明。
-
mailbox在传递形式参数的时候,实际传递并拷贝的是mailbox的指针;而对于队列,需要考虑做的是引用还是拷贝,进而考虑端口声明的方向。
-
mailbox只能够用作FIFO,而队列除了FIFO,还可以用作LIFO(Last In First Out)。
-
mailbox的存取方法put()和get()是阻塞方法,即使用它们时,方法不一定会立即返回。而队列对应的存取方式push_back()和pop_front()是非阻塞的,会立即返回。
因此在使用队列取数时,需额外填写wait(queue.size>0)才可以在其后对非空的queue做取数的操作。
此外,还有关于mailbox的特性为: -
mailbox在例化时,通过new(N)的方式可以使其变为定长容器。这样在负载到长度N之后,无法在对其写入,如果用new()的方式,则表示信箱容量不限大小。
-
如果要显式地限定mailbox中元素的类型,可以通过mailbox #(type = T)的方式。例如存储的是int,可以声明为mailbox #(int)。