SV 线程
线程的使用
程序和模块
对于硬件的过程块,他们之间的通信可以理解为不同逻辑、时序块之间的通信或者同步,是通过信号的变化来完成;
从硬件实现角度看,verilog 通过always,initial过程语句块和信号数据连接实现进程间通信;可以将不同的module作为独立的程序块,他们之间的同步通过信号的变化(event触发)、等待待定事件(时钟周期)或者时间延时来完成;
从软件思维理解硬件仿真,仿真中的各个模块首先是独立运行的线程;模块在仿真一开始便并行执行,除了每个线程会按照自身内部产生的事件来触发过程语句块之外,也同时依靠相邻模块间的信号变化来完成模块之间的线程同步;
线程
线程是可以独立运行的程序;
线程需要被触发,可以结束或者不结束;在module中的 initial 和 always ,都可以看成独立的线程,他们在仿真 0 时刻开始,而选择结束或者不结束;
硬件模型中,由于都是 always 语句块,所以可以看成是多个独立运行的线程,而这些线程会一直占用仿真资源;
软件测试平台中的验证环境都需要有 initial 语句块去创建,而在仿真过程中,验证环境中的对象可以创建和销毁,因此,软件测试端的资源占用是动态的。
线程的执行轨迹呈树状结构,任何线程都有父线程:
- 当子线程终止时,父线程可以继续执行;
- 当父线程终止时,其所开辟的所有子线程都应当会终止;
软件环境中的 initial 块对语句有两种分组方式:
- begin ... end:内部语句以顺序方式执行
- fork ... join: 内部语句以并发方式执行,fork ... join_none,fork ... join_any
线程的控制
创建线程
fork...join/join_any/join_none:能够从它的每一个并行语句中产生并发进程。
- join: 父进程阻塞到这个分支产生的所有进程结束;
- join_any: 父进程阻塞到这个分支产生的任意一个进程结束;
- join_none:父进程会与其生成的所有子进程并发执行。在父进程执行一条阻塞语句或者结束之前,所生成的子进程不会启动执行。
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: after join", $time); end // 结果:fork...join开了4个线程(即并行运行程序,其中begin...end是顺序执行,算作一个线程)。 initial 块执行结束一共需要10+50+80=140ns。其中打印的顺序为下,在140ns 中全部执行结束。 @0: start fork...join example @10: sequential after #10 @10: parallel start @20: parallel after #10 @40: sequential after #30 @50: sequential after #10 @60: parallel after #50 @60: after join @140: finish after #80
fork ... join_any
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_any $display("@%0t: after join", $time); #80 $display("@%0t: after join", $time); end // 结果: fork...join开了4个线程(即并行运行程序,其中begin...end是顺序执行,算作一个线程)。 initial 块执行结束一共需要10+80=90ns。其中,fork...join_any语句执行完$display("@%0t: parallel start", $time);语句耗时为0,之后就跳出join_any语句。其中打印的顺序为下,在90ns 中全部执行结束。 @0: start fork...join 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
fork ... join_none
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_none $display("@%0t: after join", $time); #80 $display("@%0t: after join", $time); end // 结果:fork...join开了4个线程(即并行运行程序,其中begin...end是顺序执行,算作一个线程)。 initial 块执行结束一共需要10+80=90ns。其中,fork...join_none语句不执行完$display("@%0t: parallel start", $time)直接跳出join_any语句执行$display("@%0t: after join", $time);其中打印的顺序为下,在90ns 中全部执行结束。 @0: start fork...join example @10: sequential after #10 @10: after join_any @10: parallel start @20: parallel after #10 @40: sequential after #30 @50: sequential after #10 @60: parallel after #50 @90: finish after #80
wait fork
在SV中,当程序中的initial 块全部执行完毕,仿真器就退出了。
如果需要等待 fork 中的所有线程执行完毕再退出结束 initial 块,可以使用 wait fork 语句来等待所有子线程结束。
initial begin: fork_join_none $display("@%0t, fork_join_none_thread entered", $time); fork: fork_join_none_thread thread(0, 10); // 线程1 thread(1, 20); // 线程2 thread(2, 30); // 线程3 join_none $display("@%0t, fork_join_none_thread exited", $time); wait fork; // 等待所有线程退出 $display("@%0t, fork_join_none_thread's all sub-threads finished", $time); end
disable
在使用fork ... join 或者fork ... join_none 以后,可以使用disable 来制定需要停止的线程;
initial begin: fork_join_any $display("@%0t, fork_join_any_thread entered", $time); fork: fork_join_any_thread thread(0, 10); thread(1, 20); thread(2, 30); join_any $display("@%0t, fork_join_any_thread exited", $time); disable fork_join_any_thread; $display("@%0t, disabled fork_join_any_thread", $time); end
停止单个线程
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 // fork...join_any块中只要 满足begin...end或者#TIME_OUT中的一个,那么就会退出。这里只要其中一个线程执行完毕,自动关闭另一个线程,节约资源。
停止多个线程
disable fork 可以停止从当前线程中衍生出来的所有子线程;
initial begin check_trans(tr0); //线程0 //创建一个线程来限制disable fork的作用范围 fork //线程1 begin check_trans(tr1); //线程2 fork //线程3 check_tans(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 enteriing thread", $time,id); #TIME_OUT; $display("@%0t; %m: %0d done", $time,id); end join_none endtask // 该任务只有id=0的时候,等待2ns之后禁止任务线程。 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延时之后禁止了该任务,由于三个线程均是同名线程,因此这些线程都被禁止了,最终也没有完成。
线程间通信 IPC
概述
测试平台中所有的线程都需要同步和交换数据,称之为线程间的同步,比如:
一个线程等待另外一个线程
多个线程同时访问同一个资源
线程之间可能需要交换数据
event
verilog中,一个线程形式需要等待一个带@操作符的事件。这个操作符是边沿敏感的,总是阻塞着,等待事件的变化。
其他线程可以通过 -> 操作符来触发事件,结束对第一个线程的阻塞。
类比打电话,一人等待,一人呼叫
event 是边沿触发
可以使用电平敏感的 wait(e1.triggered()) , 来代替边沿敏感的阻塞语句 @e1;如果事件在当前时刻已经被触发,就不会引起阻塞,否则一直等到事件被触发;这个方法比起@而言,可以避免在相同时刻触发event而带来的竞争问题,但同样无法捕捉已经被触发,在后续才等待的事件。
明确什么时候使用wait:@ 和wait 都可以等待多次;如果event 已经触发,@ 和 wait 都依然会被阻塞; 如果在10ns同时触发和等待,只有wait 不会被阻塞。
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 // event创建了两个对象,不需要new。触发e1时等待e2,触发e2时等待e1。打印结果为: /* @0:1:before trigger @0:2:before trigger @0:1:after trigger */
e1 和 e2 在同一时刻被触发,但是由于delta cycle的时间差 使得两个初始化块可能无法等到e1或者e2。
更加安全的方式使用event的方法triggered()。
event e1,e2; initial begin $display("@%0t: 1:before trigger", $time); -> e1; wait (e2.tregger); $display("@%0t: 1:after trigger", $time); end initial begin $display("@%0t: 2:before trigger", $time); ->e2; wait (e1.tregger); $display("@%0t: 2:after trigger", $time); end /* @0:1:before trigger @0:2:before trigger @0:1:after trigger @0:2:after trigger */
使用wait(e1.trigger()) 电平敏感来代替 边沿敏感的 阻塞语句,@e1。类似打电话留言功能
module road; initial begin automatic car byd = new(); byd.drive(); end endmodule class car; bit static = 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
如果将上述公共变量修改为event,那么通过事件的触发和判断也可以实现一样的需求。
class car; event e_start; task launch(); -> e_start; $display("car is launched"); endtask task move(); wait(e_start.triggered()); $display("car is moving"); endtask task drive(); fork this.launch(); this.move(); join endtask endclass
如果汽车需要 加速的话,速度仪表盘的信息是如何显示的呢?
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); end endtask task drive(); fork this.launch(); this.move(); this.display(); join_none endtask endclass module road; initial begin automatic car byd = new(); byd.drive(); byd.speeduo(); byd.speeduo(); byd.speeduo(); end endmodule
semaphore
可以实现对同一资源的访问控制,互斥访问;
semaphore 的基本操作:
- new() :可以创建一个带单个或者多个钥匙的semaphore
- get() :可以获取一个或者多个钥匙
- put() :可以返回一个或者多个钥匙
- 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
拿了钥匙的人要还钥匙,对于线程之间共享资源的使用方式,应该遵循互斥访问(multex access)原则。
控制线程的原因在于,如果不对其进行访问 控制,可能会出现多个线程对同一资源的访问,进而导致不可预测的数据损坏和线程的异常,这种现象称之为“线程不安全”。
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 zhe car", p); endtask task get_off(string p); $display("%s is got off the car", p); key.put(); #1ns; $display("%s return the key", p); endtask endlass 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 jion end endmodule
一开始给车子配置一把钥匙new(1)。虽然丈夫和妻子在同一时间想要开这辆车子,然而只允许以为家庭成员来驾驶。直到丈夫归还了钥匙,妻子才可以上车。
对于semaphore key使用,key在使用前必须做初始化,即要告诉用户它原生自带几把钥匙。
semaphore初始化可以初始化0,即new()无参数,可以不停的换钥匙。
semaphore::get()/put()函数中没有传递参数,即 默认他们在等待和归还钥匙的数量为1。semaphore可以被初始化为多个钥匙,也可以支持每归还多把钥匙来控制资源访问。
class carkeep; int key = 1; string q[$]; //队列 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 car",p); keep.put_key(p); 1ns; $display("%s return the key", p); endtask endclass
mailbox信箱
线程间如果传递消息,可以使用mailbox;mailbox和队列有相近之处;
mailbox 是一种对象,需要使用 new() 例化;例化时有一个可选的参数 size 来限定其存储的最大数量;
如果size 是0 或者没有指定 new(),则信箱是无限大的,可以容纳任意多的条目;
new(N), 设置容量为 N;
put():可以把数据放入mailbox;如果信箱已满,则 put 会阻塞; try_put() 非阻塞方法;
get():可以从信箱移除数据;如果信箱为空,则 get 会阻塞; try_get() 非阻塞方法;
peek():可以获取对信箱里数据的拷贝而不移除它; try_peek 非阻塞方法;
线程之间的同步方法需要注意,哪些是阻塞方法,哪些是非阻塞方法;
如果要显式的限定mailbox 中的元素类型,可以通过mailbox #(type=T) 的方式来声明,例如 mailbox #(int)
mailbox | 队列 |
必须通过new()例化 | 只需要声明队列 |
可以同时存储不同的数据类型 | 内部存储的数据类型必须一致 |
put 和 get 是阻塞方法 调用阻塞方法只能在task中,因为阻塞是耗时的 |
push_back 和pop_front是非阻塞的 在使用queue 时,需要使用wait(queue.size()>0)才可以在其后对非空队列做取数据操作 |
mailbox只能用作FIFO | queue 既可以用作FIFO, 也可以用作LIFO |
mailbox变量的操作,在传递形式参数是,实际传递并拷贝的是mailbox指针 |
传递队列的形式参数默认是input方向,传递的是数组的拷贝; 考虑使用ref方向,考虑对队列是引用还是拷贝 |
program automatic bounded; mailbox mbx; initial begin mbx = new(1); //容量为1 fork //线程1 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 put(%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) */
信箱的容量只有1,当传递进去参数 之后,需要把信箱 中内容取出,再放入新的数据。
信箱设置为1的好处是,对于动态内存不占用资源。
class car; mailbox tmp_mb, spd_mb, 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 && fule <=35;); fuel_mb.put(fuel); #sample_period; end endtask task drive(); fork sensor_tmp(); sensor_spd(); semsor_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
对于mailbox的用法,与FIFO使用相似。 如果我们将上述的mailbox来代替用队列的话,则可以 修改为下面的例码。
class car; int tmp_q[$],spd_q[$],fuel_q[$]; 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_q.push_back(tmp); #sample_period; end endtask task sensor_spd; int spd; forever begin std::randomize(spd) with (spd >=50 && spd <= 60;); spd_q.push_back(spd); #sample_period; end endtask task drive(); fork sensor_tmp(); sensor_spd(); semsor_fuel(); display(tmp_mb,"temperature"); display(spd_mb,"speed"); display(fuel_mb,"feul"); join_none endtask task display(string name, ref int q[$]); // int val; forever begin wait (q.size()>0); //只有数据不为空时 val = q.pop_front(); $display("car::%s is % 0d", name ,val); end endtask endclass module road; car byd = new(); initial begin byd.drive(); end endmodule
总结
event:最小信息量的触发,即单一的通知功能。可以用来做事件的触发,也可以多个event组合起来做线程之间的同步
semaphore:共享资源的安全卫士。如果多线程之间要对某一公共资源做访问
mailbox:精小的SV原生FIFO。在线程之间做数据通信或者内部数据缓存是可以考虑这个
参考
https://www.freesion.com/article/5711873029/