SystemVerilog学习笔记6——线程

目录

  • 线程的使用
    • 程序和模块
    • 什么是线程?
  • 线程的控制
    • fork并行线程语句块
  • 等待所有衍生线程
  • 线程间的通信
    • event事件
    • semaphore旗语
    • mailbox信箱


线程的使用

程序和模块

  • module作为RTL模型的外壳包装和实现硬件行为,在更高层的集成层面,模块之间也需要通信和同步;
  • 对于硬件的过程块,它们之间的通信可理解为不同逻辑/时序块间的通信或同步,是通过信号的变化来完成的;
  • 从硬件实现的角度来看,Verilog通过always、initial过程语句块和信号数据连接实现进程间通信;
  • 可将不同module作为独立程序块,它们之间的同步通过信号的变化event触发、等待特定事件(时钟周期)或时间(固定延时)来完成;
  • 如果按软件思维理解硬件仿真,仿真中的各个模块首先是独立运行的线程;
  • 模块(线程)在仿真一开始便并行执行,除了每个线程会依照自身内部产生的事件触发过程语句块外,也同时依靠相邻模块间的信号变化完成模块间的线程同步。
    SystemVerilog学习笔记6——线程_第1张图片

什么是线程?

  • 线程即独立运行的程序
  • 线程需要被触发,可以结束不结束
  • 在module中的initialalways都可看做独立的线程,它们会在仿真0时刻开始,而选择结束或不结束;
  • 硬件模型中由于都是always语句块,所以可看成是多个独立运行的线程,而这些线程会一直占用仿真资源,因为它们并不会结束
  • 软件测试平台中的验证环境都需要由initial语句块去创建,仿真过程验证环境中的对象可创建和销毁,因此资源占用是动态的;
  • 软件环境中的initial块对语句有两种分组方式,使用begin…end或fork…join。begin…end中的语句以顺序方式执行,而fork…join中的语句则以并发方式执行。并行方式还包括fork…join_any、fork…join_none;
  • 线程的执行轨迹是呈树状结构的,任何线程都应该有父线程;
  • 父线程可以开辟若干个子线程,父线程可以暂停或终止子线程。当子线程终止时,父线程可继续执行,当父线程终止时,其子线程都应当终止。

多选题:如果需要降低仿真时的内存负载,哪些措施是合理的:ABCD
A、降低模块间的信号跳变频率;B、只在必要时创建软件对象;
C、在不需要时钟时关闭时钟; D、在数据带宽需求低时降低时钟频率。

注:动态消耗取决于单位时刻(delta-cycle)里有多少事件发生,事件越多触发的时序/组合逻辑越多,从而增加动态运算量;

线程的控制

fork并行线程语句块

  • fork...join需要所有线程执行完,才能继续执行
  • fork...join_any只需要任何其中一个线程执行完,就可以继续执行
  • fork...join_none无需等待正在执行的线程,直接继续执行接下来的程序

等待所有衍生线程

  • 在SV中,当程序中的initial块全部执行完毕,仿真器就退出了;
  • 如果要等待fork块中的所有线程执行完毕再退出结束initial块,可以使用wait fork语句来等待所有子线程结束。
task run_threads;
	...
	fork
		check_trans(tr1);
		check_trans(tr2);
		check_trans(tr3);//三个线程
	join_none //虽然直接跳过了所有子线程,但它们会在后台执行
	...
	wait fork; //等待所有fork中的线程结束再退出task
endtask
  • 使用了fork…join_any或fork…join_none,可使用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; //fork块里有两个并行线程,但只希望有一个执行完就可以退出
		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去禁止这个线程标号,所有衍生的同名线程都将被禁止。
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);
	//在#2延时之后禁止了该任务,而由于三个线程均是同名线程
	//因此这些线程都被禁止了,最终都没有完成
	wait_for_time_out(1);
	wait_for_time_out(2);
	#(TIME_OUT * 2) $display("@%0t: All done", $time);
end

线程间的通信

  • 测试平台中的所有线程之间都需要同步交换数据
  • 一个线程需要等待另一个线程;
  • 多个线程可能同时访问同一个资源;
  • 所有数据交换和同步称为线程间的通信(IPC,Interprocess Communication)
  • 线程间共享资源的使用方式,应该遵循互斥访问原则;
  • 控制共享资源的原因在于如果不对其访问做控制,可能出现多个线程对同一资源的访问,进而导致不可预期的数据损坏和线程异常,即“线程不安全”。

event事件

  • Verilog中一个线程总是要等待一个带@操作符的事件。这个操作符是边沿敏感的,所以总是阻塞着、等待事件的变化;
  • 其它线程可通过->操作符触发事件,结束对第一个线程的阻塞
  • 可以使用电平敏感的wait(e1.triggered()) 替代边沿敏感的阻塞语句@e1。如果事件在当前时刻已经被触发则不会引起阻塞,否则会一直等到事件被触发为止。比起@而言可以避免在相同时刻触发event而带来的竞争问题,但同样无法捕捉已经被触发,但后续才等待的事件的阻塞情况
  • 当线程A要给线程B传递超过一次的事件时,利用公共变量就不再是个好选择了;
  • 多次通知某个线程,用@或wait(event.triggered)都可以,两者均需先等待然后event触发才起作用
  • 适用于最小信息量的触发,即单一的通知功能
event e1, e2; //注意event不需要new
initial begin //第一个初始化块启动,触发e1事件,然后阻塞在e2上
	$display("@%0t: 1: before trigger", $time);
	-> e1;
	@e2;
	$display("@%0t: 1: after trigger", $time);
end
initial begin //第二个初始化块启动,触发e2事件,然后阻塞在e1上
	$display("@%0t: 2: before trigger", $time);
	-> e2;
	@e1;
	$display("@%0t: 2: after trigger", $time);
end //e1和e2在同一时刻被触发
//但由于delta cycle的时间差使得两个初始化块可能无法等到e1或e2
输出结果:
@0: 1: before trigger
@0: 2: before trigger
@0: 1: after trigger
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
module road;
initial begin
	automatic car byd = new();
	byd.drive();
end
endmodule
输出结果:
# car is launched
# car is moving
class car;
	bit start = 0;
	task launch();
		start = 1;
		$display("car is launched");
	endtask
	task move();
		wait(start == 1);
		$display("car is moving");
	endtask
	task drive();
		fork
			this.launch();
			this.move();
		join
	endtask
endclass //在car::drive()中同时启动两个线程move和launch。
//move通过两个线程之间的共享信号car::start判断什么时候可以行驶
//即利用了wait语句完成线程launch通知线程move
class car;
	event e_start;
	bit start = 0;
	task launch();
		-> e_start;
		$display("car is launched");
	endtask
	task move();
		wait(e_start.triggerd);
		$display("car is moving");
	endtask
	task drive();
		fork
			this.launch();
			this.move();
		join
	endtask
endclass
module road;
initial begin
	automatic car byd = new();
	byd.drive();
	byd.speedup();
	byd.speedup();
	byd.speedup();
end
endmodule
输出结果:
# car is launched
# car is moving
# speed is 1
# speed is 2
# speed is 3
class car;
	event e_start;
	event e_speedup;
	int speed = 0;
	...
	task speedup();
		#10ns;
		-> e_speedup;
	endtask
	task display();
		forever begin
			@e_speedup;
			speed++;
		$display("speed is %0d", speed);
	endtask
	task drive();
		fork
			this.launch();
			this.move();
			this.display();
		join—_none
	endtask
endclass

semaphore旗语

  • semaphore可以实现对同一资源访问控制
  • semephore有三种基本操作。new()方法可以创建一个带单个或多个钥匙的semaphore,使用get()方法可以获取一个或多个钥匙,而put()方法可以返回一个或多个钥匙(没有传递参数默认钥匙数量为1);
  • 如果想要获取一个semaphore而不希望被阻塞,可使用try_get()函数,返回1表示有足够多的钥匙,返回0则表示钥匙不够,钥匙遵循“先到先得”的原则;
  • 钥匙必须在使用前要做初始化,即告诉用户它原生自带几把钥匙。
program automatic test(bus_ifc.TB bus);
	semaphore sem; //创建一个semaphore
	initial begin
		sem = new(1); //分配一个钥匙
		fork
			sequencer();
			sequencer(); //产生两个总线事务线程
		join
	end
	task sequencer;
		repeat($urandom%10) //随机等待0-9个周期
		@bus.cb;
		sendTrans(); //执行总显示无
	endtask
	task sendTrans;
		sem.get(1); //获取总线钥匙
		@bus.cb;
		bus.cb.addr <= t.addr; //把信号驱动到总线上
		...
		sem.put(1); //处理完成返回钥匙,不返回会程序会死锁
	endtask
endprogram
//夫妻取钥匙开车示例——资源共享
class car;
	semaphore key;
	function new();
		key = new(1);
	endfunction
	task get_on(string p);
		$display("%s is waiting for the key", p);
		key.get();
		#1ns;
		$display*("%s got on the car", p);
	endtask
	task get_off(string p);
		$display("%s got off the car". p);
		key.put();
		#1ns;
		$display("%s returned the key", p);
	endtask
endclass
module family;
	car byd = new();
	string p1 = "husband";
	string p2 = "wife";
	initial begin
		fork
			begin //丈夫开车
				byd.get_on(p1);
				byd.get_off(p1);
			end
			begin //妻子开车
				byd.get_on(p2);
				byd.get_off(p2);
			end
		join //不知道谁先谁后
	end
endmodule
输出结果:
# husband is waiting for the key
# wife is waiting for the key
# husband got on the car
# husband got off the car
# husband returned the key
#wife got on the car
#wife got off the car
# wife returned the key
//车管家保管车钥匙(但没有优先级)
class carkeep;
	int key = 1;
	string q[s];
	string user;
	task keep_car();
		fork
			forever begin //管理钥匙和分发
				wait(q.size()!=0 && key!=0);
				user = q.pop_front();
				key--;
			end
		join_none;
	endtask
	task get_key(string p); //拿钥匙
		q.push_back(p);
		wait(user == p);
	endtask
	task put_key(string p); //还钥匙
		if(user == p) begin
			user = "none";
			key++;
		end
	endtask
endclass
class car;
	carkeep keep;
	function new();
		keep = new();
	endfunction
	task drive();
		keep.keep_car();
	endtask
	task get_on(string p);
		$display("%s is waiting for the key", p);
		keep.get_key(p);
		#1ns;
		$display("%s got on the car", p);
	endtask
	task get_off(string p);
		$display("%s got off the key", p);
		keep.put_key(p);
		#1ns;
		$display("%s returned the key", p);
	endtask
endclass
module family;
	car byd = new();
	string p1 = "husband";
	string p2 = "wife";
	initial begin
		byd.drive();
		fork
			begin //丈夫开车
				byd.get_on(p1);
				byd.get_off(p1);
			end
			begin //妻子开车
				byd.get_on(p2);
				byd.get_off(p2);
			end
		join
	end
endmodule
输出结果:
# husband is waiting for the key
# wife is waiting for the key
# husband got on the car
# husband got off the car
# husband returned the key
# wife got on the car
# wife got off the car
# wife returned the key

mailbox信箱

  • 线程之间如果传递信息,可使用mailbox信箱。mailbox和队列queue相似;
  • mailbox是一种对象,因此需要使用new()来例化,参数size为0没有指定时,信箱则表示无限大
  • 使用put()把数据放入mailbox,get()从信箱移除数据,peek()可以获取对信箱里数据的拷贝而不移除它(若信箱为满,则put()阻塞;为空则get()阻塞);
  • 线程之间的同步方法需注意哪些是阻塞方法,哪些是非阻塞方法,即哪些是立即返回,哪些可能需要等待时间;

mailbox与queue的区别

  • mailbox必须通过new()例化,而队列只需要声明;
  • mailbox可以将不同的数据类型同时存储(但不建议这么做),队列存储的元素类型必须一致;
  • mailbox的存取方法put()get()阻塞方法,队列的push_back()pop_front()方法是非阻塞的,故使用queue取数时需wait(queue.size()>0)才可以在其后对非空的queue做取数操作。调用阻塞方法只能在task中调用,因为阻塞方法是耗时的;调用非阻塞方法在task和function中都可以;
  • mailbox只能用作FIFO,而queue还可用作LIFO

mailbox使用细节:

  • 对于mailbox变量的操作,在传递形参时实际传递并拷贝的是mailbox的指针;
  • 例化mailbox时通过new(N)的方式使其变为定长容器,这样在负载到长度N以后便无法再写入,若用new()的方式,则表示信箱容量不限大小;
  • put()/get()/peek()等阻塞方法,也可考虑用try_put()/try_get()/try_peek()等非阻塞方法;
  • 如果要显式地限定mailbox中元素的类型,可通过mailbox #(type = T)的方式声明;
  • 精小的SV原生FIFO,适用于在线程之间做数据通信或者内部数据缓存。
program automatic bounded;
	mailbox mbx;
	initial begin
		mbx = new(1); //容量为1
		fork
			//Producer线程
			for(int i=1; i<4; i++) begin
				$display("Producer: before put(%0d)", i);
				mbx.put(i);
				$display("Producer: after put(%0d)", i);
			end
			//Consumer线程
			repeat(4) begin
				int j;
				#1ns mbx.get(j);
				$display("Consumer: after get(%0d)", j);
			end
		join
	end
endprogram
输出结果:
Producer:before put(1)
Producer:after put(1)
Producer:before put(2)
Consumer:after get(1)
Producer:after put(2)
Producer:before put(3)
Consumer:after get(2)
Producer:after put(3)
Consumer:after get(3)
//实时显示车子的状态(包括车速、油量和温度),利用mailbox满足多个线程间的数据通信
class car;
	mailbox tmp_mb;
	mailbox spd_mb;
	mailbox fuel_mb;
	int sample_period;
	function new();
		sample_period = 10;
		tmp_mb = new();
		spd_mb = new();
		fuel_mb = new();
	endfunction
	task sensor_tmp;
		int tmp;
		forever begin
			std::randomize(tmp) with {tmp >= 80 && tmp <= 100;};
			tmp_mb.put(tmp);
			#sample_period;
		end
	endtask
	task sensor_spd;
		int spd;
		forever begin
			std::randomize(spd) with {spd>= 50 && spd <= 60;};
			spd_mb.put(spd);
			#sample_period;
		end
	endtask
	task sensor_fuel;
		int fuel;
		forever begin
			std::randomize(fuel) with {fuel>= 30 && fuel <= 35;};
			fuel_mb.put(fuel);
			#sample_period;
		end
	endtask
	task drive();
		fork
			sensor_tmp();
			sensor_spd();
			sensor_fuel();
			display(tmp_mb, "temperature");
			display(spd_mb, "speed");
			display(fuel_mb, "feul");
		join_none
	endtask
	task display(mailbox mb, string name="mb");
		int val;
		forever begin
			mb.get(val);
			$display("car::%s is %0d", name, val);
		end
	endtask
endclass
module road;
	car byd = new();
	initial begin
		byd.drive();
	end
endmodule
输出结果
# car::temperature is 100
# car::speed is 50
# car::feul is 30
# car::temperature is 96
# car::speed is 55
# car::feul is 33
...

下面对于mailbox的描述正确的有:ABCD
A、信箱在使用时需要初始化;
B、信箱的容量可以固定也可以不固定;
C、信箱可通过参数化指定其存储的数据类型;
D、多个对象可同时使用信箱存储或获取数据;

下面对于信箱和队列的说法哪些是错误的:BC
A、信箱和队列的容量都可以不固定;(队列一定不固定)
B、信箱和队列在使用前都应初始化;
C、信箱和队列都可通过赋值传递他们的指针;
D、信箱和队列都可被用作FIFO;

event、semaphore和mailbox总结

  • 均可实现线程A请求同步线程B,线程B再响应线程A的同步方式;
  • 如果要在同步时完成一些数据传输,mailbox更合适(因为它可以存储数据);而event和semaphore更偏向于小信息量的同步,即不包含更多的数据信息;
  • event:最小信息类的触发,即单一的通知功能。可用做事件的触发,也可多个event组合起来用做线程之间的同步;
  • semaphore:共享资源的安全卫士,如果多线程间要对某一公共资源做访问,即可使用它;
  • mailbox:精小的SV原生FIFO,适用于在线程间做数据通信或内部数据缓存。

你可能感兴趣的:(SystemVerilog,学习笔记,功能测试,测试用例,测试覆盖率,模块测试)