不枉初心,砥砺前行

皮皮祥的博客

欢迎留言,评论

导航

线程的控制与同步

线程的控制与同步


相关: 线程间同步和通信,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。在线程之间做数据通信或者内部数据缓存时可以考虑使用此元素;

posted on 2022-10-04 23:36  皮皮祥  阅读(77)  评论(0编辑  收藏  举报