当前位置:网站首页>线程使用、控制、通信
线程使用、控制、通信
2022-08-08 05:40:00 【狮驼岭上的小钻风】
线程的使用
- module(模块)作为SV从Verilog继承过来的概念,自然地保持了它的特点,除了作为RTL模型的外壳包装和实现硬件行为,在更高层的集成层面,模块之间也需要通信和同步。
- 对于硬件的过程块,它们之间的通信可理解为不同逻辑/时序块之间的通信或者同步,是通过信号的变化来完成的。
- 从硬件实现的角度来看,Verilog通过always,initial过程语句块和信号数据连接实现进程间通信。
- 我们可以将不同的module作为独立的程序块,他们之间的同步通过信号的变化(event触发)、等待特定事件(时钟周期)或者时间(固定延时)来完成。
- 如果按照软件的思维理解硬件仿真,仿真中的各个模块首先是独立运行的线程(thread) 。
- 模块(线程)在仿真一开始便并行执行,除了每个线程会依照自身内部产生的事件来触发过程语句块之外,也同时依靠相邻模块间的信号变化来完成模块之间的线程同步。
线程的概念
- 线程即独立运行的程序。线程需要被触发,可以结束或者不结束。
- 在module中的initial和always,都可以看做独立的线程,它们会在仿真0时刻开始,而选择结束或者不结束。
- 硬件模型中由于都是always语句块,所以可以看成是多个独立运行的线程,而这些线程会一直占用仿真资源,因为它们并不会结束。
- 软件测试平台中的验证环境都需要由initial语句块去创建,而在仿真过程中,验证环境中的对象可以创建和销毁,因此软件测试端的资源占用是动态的。
- 软件环境中的initial块对语句有两种分组方式,使用begin...end或fork...join。begin...end中的语句以顺序方式执行,而fork...join中的语句则以并发方式执行。fork...join类似的并行方式语句还包括fork...join_any,fork...join_none。
- 线程的执行轨迹是呈树状结构的,即任何的线程都应该有父线程。父线程可以开辟若干个子线程,父线程可以暂停或者终止子线程。当子线程终止时,父线程可以继续执行。当父线程终止时,其所开辟的所有子线程都应当会终止。
线程的控制
fork join_any 某个子线程执行完退出后,剩下的子线程还会执行
fork join_none不会等待执行子线程,(点火后)直接执行后面线程外语句
fork
$display(); //执行这条后就会跳出,执行外面的display
#10 $display(); //10个单位后会执行
#20 $display();
join_any
$display();在sV中,当程序中的initial块全部执行完毕,仿真器就退出了。如果我们希望等待fork块中的所有线程执行完毕再退出结束initial块,我们可以使用wait fork语句来等待所有子线程结束。
fork
check_trans(tr1);
check_trans(tr2);
……
join_none
……
wait fork; 使用fork……join_any或者fork……join_none以后,可以使用disable来指定需要停止的线程。
fork:time_block
begin
wait(bus.cb.addr==tr.addr);
$display("@$0t:Addr match %d",$time,tr.addr);
end
#TIME_out $display();
join_any
disable timeout_block;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;
disable wait_for_time_out;
end
join_none
fork:just_a_little
begin
……
end
join_none
endtask
//任务wait_for_time_out被调用了三次,从而衍生出三个线程
//线程0在#2延时之后禁止了该任务,而由于三个线程均是同名线程,因此这些线程都被禁止
initial begin
wait_for_time_out(0);
wait_for_time_out(1);
wait_for_time_out(2); //0时刻三个都会执行完,都为fork_join_none。任务退出,线程还在
#(TIME_OUT)
end
线程间的通信
- 测试平台中的所有线程都需要同步并交换数据。
- 一个线程需要等待另一个。
- 多个线程可能同时访问同一-个资源。
- 线程之间可能需要交换数据。
- 所有这些数据交换和同步称之为线程间的通信(IPC,Interprocess Communication)。
event事件
- Verilog中,一个线程总是要等待一个带@操作符的事件。这个操作符是边沿敏感的,所以它总是阻塞着、等待事件的变化。
- 其它线程可以通过->操作符来触发事件,结束对第一个线程的阻塞。
三个线程里唯一一个不用new的,可以将其当作一个对象,按类处理。
event e1,e2;
initial begin
$display("@%0t:1:before trigger",$time);
->e1;
@e2; //改为wait(e2.triggerd())
$display("@%0t:1:after trigger",$time);
end
initial begin
$display("@%0t:2:before trigger",$time);
->e2;
@e1;
$display("@%0t:2:after trigger",$time);
end
//第一个初始块启动,触发e1事件,然后阻塞在e2上。第二个初始化启动触发e2事件,然后阻塞在e1上。
总会有一个after trigger没有执行
//e1和e2在同一时刻被触发,由于delta cycle的时间差使两个初始化模块无法等到e1或e2
更安全的方式是使用event的方法triggered(),那么全部都会打印出来- 可以使用电平敏感的wait (e1. triggered () )来替代边沿敏感的阻塞语句@e1。
- 如果事件在当前时刻已经被触发,则不会引起阻塞。否则,会一直等到事件被触发为止。
- 这个方法比起@而言,更有能力保证,只要event被触发过,就可以防止引起阻塞
//通过wait进行线程通知
class car;
bit start=0; //event e_start;
task launch();
start=1; //->e_start
$display();
endtask
task move();
wait(start==1); //wait(e_start.triggerd) 可以实现同样的需求
endtask;
task driver();
fork
this.launch(); //虽然是并行语句,但由于wait会导致luanch先运行
this.move();
join
endtask
endclass//使用@的情况,不能使用wait triggered,因为边沿触发可以一直触发,而电平触发只能触发一次
//uvm中的triggered不同,是可以擦除状态的
class car;
event speed;
int speed=0;
task speedup();
#10ns;
->e_speedup;
endtask
task display();
forever begin
@e_speedup; //一直在等待
speed++
$display("speed is %0d";speed);
end
endtask
module road;
initial begin
automatic car byd=new();
byd.speedup(); //1
byd.speedup(); //2
byd.speedup(); //3
end
endmodulesemaphore旗语
- semaphore可以实现对同一资源的访问控制。
- 对于初学者而言,无论线程之间在共享什么资源,都应该使用semaphore等资源访问控制的手段,以此避免可能出现的问题。
- semaphore有三种基本操作。new( )方法可以创建一个带单个或者多个钥匙的semaphore,使用get( )可以获取一个或者多个钥匙,而put( )可以返回一个或者多个钥匙。
- 如果你试图获取一个semaphore而希望不被阻塞,可以使用try_ get()函数。它返回1表示有足够多的钥匙,而返回0则表示钥匙不够。
program automatic test(bus_ifc.TB bus);
semaphore sem; //创建一个semphore
initial begin
sem=new(1); //分配一个钥匙
fork
sequencer(); //产生两个线程 这里用semphore是为了让只有一个能够进行,new参数为1
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资源共享的需求
- 对于线程间共享资源的使用方式,应该遵循互斥访问(mutexaccess)原则。
- 控制共享资源的原因在于,如果不对其访问做控制,可能会出现多个线程对同一资源的访问,进而导致不可预期的数据损坏和线程的异常,这种现象称之为"线程不安全"。
//与前面事件的例子相比,前面是顺序,通过event设置了哪个先哪个后。而在这不知道顺序,
//虽然两者的结果都是顺序执行
class car;
semphore 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 is waiting for the key",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
- key在使用前必须要做初始化,即要告诉用户它原生自带几把钥匙。
- 没有在semaphore: :get( )/ put( )函数中传递参数,即默认他们等待和归还的钥匙数量是1。
- semaphore可以被初始化为多个钥匙,也可以支持每次支取和归还多把钥匙用来控制资源访问。
- seamphore可以初始化为0,通过put可以放入钥匙。
//通过类实现同样功能
class carkeep;
int key=1;
string q[$];
string user;
task keep_user();
fork
forever begin; //管理分发钥匙
wait(q.size()!=0 && key!=0);
user=p.pop_front();
key--;
end
join_none;
endtask
task get_key(string p);
q.push(p);
wait(user==p);
endtask
task put_key(string p);
if(user==p)begin
key++;
user="none";
end
endtask
endclass
class car;
carkeep keep;
function new();
keep=new();
endfunction
task driver();
keep.keep_car();
endtask
task get_on(string p);
$display("%s is waiting for the key",p);
keep.get_key();
#1ns;
$display("%s got on the car",p);
endtask
task get_off(string p);
$display("%s got off the car",p);
keep.put_key(p)
#1ns;
$display("%s returned the key",p);
endtask
endclassmaibox信箱
- 线程之间如果传递信息,可以使用mailbox。mailbox和队列queue有相近之处。
- mailbox是一种对象,因此也需要使用new( )来例化。例化时有一个可选的参数size来限定其存储的最大数量。如果size是0或者没有指定,则信箱是无限大的,可以容纳任意多的条目。
- 使用put( )可以把数据放入mailbox,使用get( )可以从信箱移除数据。
- 如果信箱为满,则put( )会阻塞;如果信箱为空,则get( )会阻塞
- peek( )可以获取对信箱里数据的拷贝而不移除它。
- 线程之间的同步方法需要注意,哪些是阻塞方法,哪些是非阻塞(try)方法,即哪些是立即返回的,而哪些可能需要等待时间的。
program automatic bounded;
mailbox mbx;
initial begin
mbx=new(1); //容量为1
fork
for(int i=1;i<4;i++)begin
$display("Producer:before put(%0d)",i);
mbx.put(i);
$display("Producer:after pur(%0d)",i);
end
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)数据通信的需求
如果我们继续通过上面这辆BYD,来模拟不同传感器(线程)到车的中央显示的通信,可以利用SV的mailbox (信箱)来满足多个线程之间的数据通信。
//分别用maibox和队列实现
class car;
mailbox tmp_mb,spd_mb; //int tmp_q[$],spd_q[$];
int sample_period;
function new();
sample_period=10;
tmp_mb=new();
spd_mb=new();
endfunction
task sensor_tmp;
int tmp;
forever begin
std::randomize(tmp) with {tmp>=80 && tmp<=100;}
tmp_mb.put(tmp); //类里任务调用任务 //tmb_q.push_back(tmp)
#sample_period;
end
endtask
task sensor_spd;
int spd;
forever begin
std::randomize(spd) with {spd>=50 && spd<=60};
spd_mb.put(spd); //spd_q.push_back(spd);
#sample_period;
end
endtask
task driver();
fork
sensor_tmp();
sensor_spd();
display(tmp_tb,"temperature");
display(spb_tb,"speed");
join_none
endtask
task display(mailbox mb,string name="mb"); //(string name,ref int q[$])
int val;
forever begin
mb.get(val); //wait(q.size()>0);
$display("car::%s is %0d",name,val); //val=q.pop_front(); 使用队列记得加上wait
end
endtask
endclass
module road;
car byd=new();
initial begin
byd.driver();
end
endmodulemaibox与queue在使用时的差别:
- maibox必须通过new( )例化,而队列只需要声明,不用初始化。
- mailbox可以将不同的数据类型同时存储,不过这么做是不建议的;对于队列来讲,它内部存储的元素类型必须一致。
- maibox的存取方法put( )和get( )是阻塞方法(blocking method),即使用它们时,,方法不一定会立即返回,而队列所对应的存取方式,push_ back( )和pop_ front( )方法是非阻塞的,会立即返回。因此在使用queue取数时,需要额外填写wait(queue.size0) > 0)才 可以在其后对非空的queue做取数的操作。此外也应该注意,如果要调用阻塞方法,那么只可以在task中调用,因为阻塞方法是耗时的:而调用非阻塞方法,例如queue的push_ back( )和pop_ front(), 则既可以在task又可以在function中调用。
- mailbox只能够用作FIFO,而queue除了按照FIFO使用,还有其它应用的方式例如LIFO (Last In First Out)
- 对于mailbox变量的操作,在传递形式参数时,实际传递并拷贝的是mailbox的指针(所以这里没有标明方向或者使用ref);而在第二个例子中的task dispiay(),关于queue的形式参数声明是ref方向,因为如果采用默认的input方向,那么传递过程中发生的是数组的拷贝,以致于方法内部对queue的操作并不会影响外部的gueue本身。因此在传递数组时,读者需要考虑到,对数组做的是引用还是拷贝,进而考虑端口声明的方向。
mailbox的其它特性
- mailbox在例化时,通过new(N)的方式可以使其变为定长(fixed length)容器。这样在负载到长度N以后,无法再对其写入。如果用new()的方式,则表示信箱容量不限大小。
- 除了put()/get()/peek()这样的阻塞方法,用户也可以考虑使用try_ put()/try_ get()/try_peek()等非阻塞方法。
- 如果要显式地限定mailbox中元素的类型,可以通过mailbox #(type = T)的方式来声明。例如上面的三个mailbox存储的是int,则可以在声明时进一步限定其类型为mailbox # (int)。
将stall和park两个线程的同步视作,先由stall发起同步请求,再等待park线程完成并响应同步请求,最后由stall线程继续其余的程序,最终结束熄火的过程。用之前掌握的SV三种进程通信的方式event、semaphore和mailbox来解决进程间的同步问题。
//所谓的同步其实是握手
class car;
event e_stall;
event e_park;
task stall;
#1ns;
-> e_stall; //2
@e_park; //3
task park;
@e_stall; //1
#1ns;
->e_park //4
endtask
task driver();
this.stall();
this.park();
endclass//semphore和maibox get\put顺序相同 两者都是为空时要先放数据
class car;
semphore key; //mailbox mb;
function new();
key=new(0); //mb=new(1);
endfunction
task stall; //int val=0;
#1ns;
key.put(); //1 //mb.put(val);
key.get(); //4 //mb.get(val);
endtask
task park; //int val=0;
key.get(); //2 //mb.get(val);
#1ns;
key.put(); //3 //mb.put(val);
endtask
task driver();
fork
this.stall();
this.park();
join_none
endtask
endclass正在进行的进程由于发生某事件而暂时无法继续执行时,便放弃处理机而处于暂停状态,亦即进程的执行受到阻塞,我们把这种暂停状态叫阻塞进程阻塞,有时也成为等待状态或封锁状态。通常这种处于阻塞状态的进程也排成一个队列。有的系统则根据阻塞原因的不同而处于阻塞状态进程排成多个队列。
上面都是线程A请求同步线程B,线程B再响应线程A的同步方式
如果要在同步(事件)的同时,完成一些数据传输,那么更合适的是mailbox,因为它可以用来存储一些数据; 而event和semaphore更偏向于小信息量的同步,即不包含更多的数据信息。
通信要素的比较和应用
event:最小信息量的触发,即单一的通知功能。可以用来做事件的触发,也可以多个event组合起来用来做线程之间的同步。
semaphore:共享资源的安全卫士。如果多线程间要对某一公共资源做访问,即可以使用这个要素。
mailbox:精小的SV原生FIFO。在线程之间做数据通信或者内部数据缓存时可以考虑使用此元素。
边栏推荐
猜你喜欢

毕设——基于人脸表情的桌面交互精灵设计(分享一下成果,附上人脸表情的数据集和自己训练出来yolov5模型以及基于PYQT5运行yolov5的交互界面)

卷积神经网络 图像识别,卷积神经网络 图像处理

查询时间内用户分布的sql语句

Use of Filter

121道分布式面试题和答案

Object.prototype.toString()如何判断数据类型及注意点

Unity-CharacterController(角色控制器)

stack-queue

Leetcode sword 】 refers to the Offer (special commando) summary

Rust开发——Struct使用示例
随机推荐
The difference between classification, object detection, semantic segmentation, and instance segmentation
"Public Administration" exam key points and answers
温故知新—Activity的五种启动模式
cs软件ui构建办法
How to batch import files and rename them all to the same file name
分布式事务 :可靠消息最终一致性方案
【MySQL】——事务的基本概念
如何保存页面的当前的状态
LVS:NAT模式详解
apifox使用文档之环境变量 / 全局变量 / 临时变量附apifox学习路线图
C语言力扣第58题之最后一个单词的长度。从后往前遍历
说说Redis分布式锁的原理和实现蚂【蚁金服三面】
毕设——基于人脸表情的桌面交互精灵设计(分享一下成果,附上人脸表情的数据集和自己训练出来yolov5模型以及基于PYQT5运行yolov5的交互界面)
预处理笔记
线索二叉树
浅学软件逆向笔记(1)
C language - function
Week 9 10 Neural Networks
postman---postman parameterization
【猜拳游戏 基于Objective-C语言】