• 线程使用、控制、通信


    线程的使用

    • 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不会等待执行子线程,(点火后)直接执行后面线程外语句

    1. fork
    2. $display(); //执行这条后就会跳出,执行外面的display
    3. #10 $display(); //10个单位后会执行
    4. #20 $display();
    5. join_any
    6. $display();

    在sV中,当程序中的initial块全部执行完毕,仿真器就退出了。如果我们希望等待fork块中的所有线程执行完毕再退出结束initial块,我们可以使用wait fork语句来等待所有子线程结束。

    1. fork
    2. check_trans(tr1);
    3. check_trans(tr2);
    4. ……
    5. join_none
    6. ……
    7. wait fork;

    使用fork……join_any或者fork……join_none以后,可以使用disable来指定需要停止的线程。

    1. fork:time_block
    2. begin
    3. wait(bus.cb.addr==tr.addr);
    4. $display("@$0t:Addr match %d",$time,tr.addr);
    5. end
    6. #TIME_out $display();
    7. join_any
    8. disable timeout_block;

    disable fork可以停止从当前线程中衍生出来的所有子线程

    1. initial begin
    2. check_trans(tr0); //线程0
    3. //创建一个线程来限制disable fork的作用范围
    4. fork //线程1
    5. begin
    6. check_trans(tr1); //线程2
    7. fork //线程3
    8. check_trans(tr2); //线程4
    9. join
    10. //停止线程1-4,单独保留线程0
    11. #(TIME_OUT/2)disable fork; //停止所在的线程
    12. end
    13. join
    14. end

    如果给任务或者线程指明标号,那么线程被调用多次以后,如果通过disable去禁止这个线程标号,所有衍生的同名线程都将被禁止。

    1. task wait_for_time_out(int id);
    2. if(id==0)
    3. fork
    4. begin
    5. #2;
    6. disable wait_for_time_out;
    7. end
    8. join_none
    9. fork:just_a_little
    10. begin
    11. ……
    12. end
    13. join_none
    14. endtask
    15. //任务wait_for_time_out被调用了三次,从而衍生出三个线程
    16. //线程0#2延时之后禁止了该任务,而由于三个线程均是同名线程,因此这些线程都被禁止
    17. initial begin
    18. wait_for_time_out(0);
    19. wait_for_time_out(1);
    20. wait_for_time_out(2); //0时刻三个都会执行完,都为fork_join_none。任务退出,线程还在
    21. #(TIME_OUT)
    22. end

    线程间的通信

    • 测试平台中的所有线程都需要同步并交换数据。
    • 一个线程需要等待另一个。
    • 多个线程可能同时访问同一-个资源。
    • 线程之间可能需要交换数据。
    • 所有这些数据交换和同步称之为线程间的通信(IPC,Interprocess Communication)。

    event事件

    • Verilog中,一个线程总是要等待一个带@操作符的事件。这个操作符是边沿敏感的,所以它总是阻塞着、等待事件的变化。
    • 其它线程可以通过->操作符来触发事件,结束对第一个线程的阻塞。

    三个线程里唯一一个不用new的,可以将其当作一个对象,按类处理。

    1. event e1,e2;
    2. initial begin
    3. $display("@%0t:1:before trigger",$time);
    4. ->e1;
    5. @e2; //改为wait(e2.triggerd())
    6. $display("@%0t:1:after trigger",$time);
    7. end
    8. initial begin
    9. $display("@%0t:2:before trigger",$time);
    10. ->e2;
    11. @e1;
    12. $display("@%0t:2:after trigger",$time);
    13. end
    14. //第一个初始块启动,触发e1事件,然后阻塞在e2上。第二个初始化启动触发e2事件,然后阻塞在e1上。
    15. 总会有一个after trigger没有执行
    16. //e1和e2在同一时刻被触发,由于delta cycle的时间差使两个初始化模块无法等到e1或e2
    17. 更安全的方式是使用event的方法triggered(),那么全部都会打印出来
    • 可以使用电平敏感的wait (e1. triggered () )来替代边沿敏感的阻塞语句@e1。
    • 如果事件在当前时刻已经被触发,则不会引起阻塞。否则,会一直等到事件被触发为止。
    • 这个方法比起@而言,更有能力保证,只要event被触发过,就可以防止引起阻塞
    1. //通过wait进行线程通知
    2. class car;
    3. bit start=0; //event e_start;
    4. task launch();
    5. start=1; //->e_start
    6. $display();
    7. endtask
    8. task move();
    9. wait(start==1); //wait(e_start.triggerd) 可以实现同样的需求
    10. endtask;
    11. task driver();
    12. fork
    13. this.launch(); //虽然是并行语句,但由于wait会导致luanch先运行
    14. this.move();
    15. join
    16. endtask
    17. endclass
    1. //使用@的情况,不能使用wait triggered,因为边沿触发可以一直触发,而电平触发只能触发一次
    2. //uvm中的triggered不同,是可以擦除状态的
    3. class car;
    4. event speed;
    5. int speed=0;
    6. task speedup();
    7. #10ns;
    8. ->e_speedup;
    9. endtask
    10. task display();
    11. forever begin
    12. @e_speedup; //一直在等待
    13. speed++
    14. $display("speed is %0d";speed);
    15. end
    16. endtask
    17. module road;
    18. initial begin
    19. automatic car byd=new();
    20. byd.speedup(); //1
    21. byd.speedup(); //2
    22. byd.speedup(); //3
    23. end
    24. endmodule

    semaphore旗语

    • semaphore可以实现对同一资源的访问控制。
    • 对于初学者而言,无论线程之间在共享什么资源,都应该使用semaphore等资源访问控制的手段,以此避免可能出现的问题。
    • semaphore有三种基本操作。new( )方法可以创建一个带单个或者多个钥匙的semaphore,使用get( )可以获取一个或者多个钥匙,而put( )可以返回一个或者多个钥匙。
    • 如果你试图获取一个semaphore而希望不被阻塞,可以使用try_ get()函数。它返回1表示有足够多的钥匙,而返回0则表示钥匙不够。
    1. program automatic test(bus_ifc.TB bus);
    2. semaphore sem; //创建一个semphore
    3. initial begin
    4. sem=new(1); //分配一个钥匙
    5. fork
    6. sequencer(); //产生两个线程 这里用semphore是为了让只有一个能够进行,new参数为1
    7. sequencer();
    8. join
    9. end
    10. task sequencer;
    11. repeat($urandom%10) //随机等待0-9个周期
    12. @bus.cb;
    13. sendTrans(); //执行总线事务
    14. endtask
    15. task sendTrans;
    16. sem.get(1); //获取总线钥匙
    17. @bus.cb;
    18. bus.cb.addr<=t.addr; //信号驱动到总线
    19. ……
    20. sem.put(1); //处理完成后把钥匙返回,要不然会死锁
    21. endtask
    22. endprogram

    资源共享的需求

    • 对于线程间共享资源的使用方式,应该遵循互斥访问(mutexaccess)原则。
    • 控制共享资源的原因在于,如果不对其访问做控制,可能会出现多个线程对同一资源的访问,进而导致不可预期的数据损坏和线程的异常,这种现象称之为"线程不安全"。
    1. //与前面事件的例子相比,前面是顺序,通过event设置了哪个先哪个后。而在这不知道顺序,
    2. //虽然两者的结果都是顺序执行
    3. class car;
    4. semphore key;
    5. function new();
    6. key=new(1);
    7. endfunction
    8. task get_on(string p);
    9. $display("%s is waiting for the key",p);
    10. key.get();
    11. #1ns;
    12. $display("%s got on the car",p);
    13. endtask
    14. task get_off(string p);
    15. $display("%s is waiting for the key",p);
    16. key.put();
    17. #1ns;
    18. $display("%s returned the key",p);
    19. endtask
    20. endclass
    21. module family;
    22. car byd=new();
    23. string p1="husband";
    24. string p2="wife";
    25. initial begin
    26. fork
    27. begin
    28. byd.get_on(p1);
    29. byd.get_off(p1);
    30. end
    31. begin
    32. byd.get_on(p2);
    33. byd.get_off(p2);
    34. end
    35. join
    36. end
    37. endmodule
    • key在使用前必须要做初始化,即要告诉用户它原生自带几把钥匙。
    • 没有在semaphore: :get( )/ put( )函数中传递参数,即默认他们等待和归还的钥匙数量是1。
    • semaphore可以被初始化为多个钥匙,也可以支持每次支取和归还多把钥匙用来控制资源访问。
    • seamphore可以初始化为0,通过put可以放入钥匙。
    1. //通过类实现同样功能
    2. class carkeep;
    3. int key=1;
    4. string q[$];
    5. string user;
    6. task keep_user();
    7. fork
    8. forever begin; //管理分发钥匙
    9. wait(q.size()!=0 && key!=0);
    10. user=p.pop_front();
    11. key--;
    12. end
    13. join_none;
    14. endtask
    15. task get_key(string p);
    16. q.push(p);
    17. wait(user==p);
    18. endtask
    19. task put_key(string p);
    20. if(user==p)begin
    21. key++;
    22. user="none";
    23. end
    24. endtask
    25. endclass
    26. class car;
    27. carkeep keep;
    28. function new();
    29. keep=new();
    30. endfunction
    31. task driver();
    32. keep.keep_car();
    33. endtask
    34. task get_on(string p);
    35. $display("%s is waiting for the key",p);
    36. keep.get_key();
    37. #1ns;
    38. $display("%s got on the car",p);
    39. endtask
    40. task get_off(string p);
    41. $display("%s got off the car",p);
    42. keep.put_key(p)
    43. #1ns;
    44. $display("%s returned the key",p);
    45. endtask
    46. endclass

    maibox信箱

    • 线程之间如果传递信息,可以使用mailbox。mailbox和队列queue有相近之处。
    • mailbox是一种对象,因此也需要使用new( )来例化。例化时有一个可选的参数size来限定其存储的最大数量。如果size是0或者没有指定,则信箱是无限大的,可以容纳任意多的条目。
    • 使用put( )可以把数据放入mailbox,使用get( )可以从信箱移除数据。
    • 如果信箱为满,则put( )会阻塞;如果信箱为空,则get( )会阻塞
    • peek( )可以获取对信箱里数据的拷贝而不移除它。
    • 线程之间的同步方法需要注意,哪些是阻塞方法,哪些是非阻塞(try)方法,即哪些是立即返回的,而哪些可能需要等待时间的。
    1. program automatic bounded;
    2. mailbox mbx;
    3. initial begin
    4. mbx=new(1); //容量为1
    5. fork
    6. for(int i=1;i<4;i++)begin
    7. $display("Producer:before put(%0d)",i);
    8. mbx.put(i);
    9. $display("Producer:after pur(%0d)",i);
    10. end
    11. repeat(4)begin
    12. int j;
    13. #1ns mbx.get(j);
    14. $display("Consumer:after get(%0d)",j);
    15. end
    16. join
    17. end
    18. endprogram
    19. //结果
    20. Producer:before put(1)
    21. Producer:after put(1)
    22. Producer:before put(2)
    23. Consumer: after get(1)
    24. Producer:after put(2)
    25. Producer:before put(3)
    26. Consumer: after get(2)
    27. Producer:after put(3)
    28. Consumer: after get(3)

    数据通信的需求

            如果我们继续通过上面这辆BYD,来模拟不同传感器(线程)到车的中央显示的通信,可以利用SV的mailbox (信箱)来满足多个线程之间的数据通信。

    1. //分别用maibox和队列实现
    2. class car;
    3. mailbox tmp_mb,spd_mb; //int tmp_q[$],spd_q[$];
    4. int sample_period;
    5. function new();
    6. sample_period=10;
    7. tmp_mb=new();
    8. spd_mb=new();
    9. endfunction
    10. task sensor_tmp;
    11. int tmp;
    12. forever begin
    13. std::randomize(tmp) with {tmp>=80 && tmp<=100;}
    14. tmp_mb.put(tmp); //类里任务调用任务 //tmb_q.push_back(tmp)
    15. #sample_period;
    16. end
    17. endtask
    18. task sensor_spd;
    19. int spd;
    20. forever begin
    21. std::randomize(spd) with {spd>=50 && spd<=60};
    22. spd_mb.put(spd); //spd_q.push_back(spd);
    23. #sample_period;
    24. end
    25. endtask
    26. task driver();
    27. fork
    28. sensor_tmp();
    29. sensor_spd();
    30. display(tmp_tb,"temperature");
    31. display(spb_tb,"speed");
    32. join_none
    33. endtask
    34. task display(mailbox mb,string name="mb"); //(string name,ref int q[$])
    35. int val;
    36. forever begin
    37. mb.get(val); //wait(q.size()>0);
    38. $display("car::%s is %0d",name,val); //val=q.pop_front(); 使用队列记得加上wait
    39. end
    40. endtask
    41. endclass
    42. module road;
    43. car byd=new();
    44. initial begin
    45. byd.driver();
    46. end
    47. endmodule

    maibox与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来解决进程间的同步问题。

    1. //所谓的同步其实是握手
    2. class car;
    3. event e_stall;
    4. event e_park;
    5. task stall;
    6. #1ns;
    7. -> e_stall; //2
    8. @e_park; //3
    9. task park;
    10. @e_stall; //1
    11. #1ns;
    12. ->e_park //4
    13. endtask
    14. task driver();
    15. this.stall();
    16. this.park();
    17. endclass
    1. //semphore和maibox get\put顺序相同 两者都是为空时要先放数据
    2. class car;
    3. semphore key; //mailbox mb;
    4. function new();
    5. key=new(0); //mb=new(1);
    6. endfunction
    7. task stall; //int val=0;
    8. #1ns;
    9. key.put(); //1 //mb.put(val);
    10. key.get(); //4 //mb.get(val);
    11. endtask
    12. task park; //int val=0;
    13. key.get(); //2 //mb.get(val);
    14. #1ns;
    15. key.put(); //3 //mb.put(val);
    16. endtask
    17. task driver();
    18. fork
    19. this.stall();
    20. this.park();
    21. join_none
    22. endtask
    23. endclass

    正在进行的进程由于发生某事件而暂时无法继续执行时,便放弃处理机而处于暂停状态,亦即进程的执行受到阻塞,我们把这种暂停状态叫阻塞进程阻塞,有时也成为等待状态或封锁状态。通常这种处于阻塞状态的进程也排成一个队列。有的系统则根据阻塞原因的不同而处于阻塞状态进程排成多个队列。

    上面都是线程A请求同步线程B,线程B再响应线程A的同步方式

    如果要在同步(事件)的同时,完成一些数据传输,那么更合适的是mailbox,因为它可以用来存储一些数据; 而event和semaphore更偏向于小信息量的同步,即不包含更多的数据信息。

    通信要素的比较和应用

    event:最小信息量的触发,即单一的通知功能。可以用来做事件的触发,也可以多个event组合起来用来做线程之间的同步
    semaphore:共享资源的安全卫士。如果多线程间要对某一公共资源做访问,即可以使用这个要素。
    mailbox:精小的SV原生FIFO。在线程之间做数据通信或者内部数据缓存时可以考虑使用此元素。

  • 相关阅读:
    Python:实现logistic regression逻辑回归算法(附完整源码)
    RabbitMQ多线程配置和异常解决办法
    qemu中中断model虚拟化是如何实现的?
    JS EventListener
    (路透社数据集)新闻分类:多分类问题实战
    vue生命周期详解
    13链表-简单思路练习
    碳中和专题:碳足迹核算、碳中和顶刊论文、碳排放交易2022
    Redis原理 - 对象的数据结构(SDS、Inset、Dict、ZipList、QuickList、SkipList、RedisObject)
    SpringBoot内置工具类应有尽有,建议收藏!!
  • 原文地址:https://blog.csdn.net/qq_44455456/article/details/126115156