天天看點

SystemVerilog學習筆記6——線程線程的使用線程的控制等待所有衍生線程線程間的通信

目錄

  • 線程的使用
    • 程式和子產品
    • 什麼是線程?
  • 線程的控制
    • fork并行線程語句塊
  • 等待所有衍生線程
  • 線程間的通信
    • event事件
    • semaphore旗語
    • mailbox信箱

線程的使用

程式和子產品

  • module作為RTL模型的外殼包裝和實作硬體行為,在更高層的內建層面,子產品之間也需要通信和同步;
  • 對于硬體的過程塊,它們之間的通信可了解為不同邏輯/時序塊間的通信或同步,是通過信号的變化來完成的;
  • 從硬體實作的角度來看,Verilog通過always、initial過程語句塊和信号資料連接配接實作程序間通信;
  • 可将不同module作為獨立程式塊,它們之間的同步通過信号的變化event觸發、等待特定事件(時鐘周期)或時間(固定延時)來完成;
  • 如果按軟體思維了解硬體仿真,仿真中的各個子產品首先是獨立運作的線程;
  • 子產品(線程)在仿真一開始便并行執行,除了每個線程會依照自身内部産生的事件觸發過程語句塊外,也同時依靠相鄰子產品間的信号變化完成子產品間的線程同步。
    SystemVerilog學習筆記6——線程線程的使用線程的控制等待所有衍生線程線程間的通信

什麼是線程?

  • 線程即獨立運作的程式;
  • 線程需要被觸發,可以結束或不結束;
  • 在module中的initial和always都可看做獨立的線程,它們會在仿真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,适用于線上程間做資料通信或内部資料緩存。

繼續閱讀