• 线程的控制与同步



    相关: 线程间同步和通信,event semaphore mailbox


    1. 线程的使用

    1.1. 什么是线程 ?

    • 线程即独立运行的程序;
    • 线程需要被触发,可以结束或者不结束
    • 在module中的initial和always,都可以看做独立的线程,它们会在仿真0时刻开始,而选择结束或者不结束;
    • 硬件模型中由于都是always语句块,所以可以看成是多个独立运行的线程,而这些线程会一直占用仿真资源,因为他们并不会结束;
    • 软件测试平台中的验证环境都需要有initial语句块去创建,而在仿真过程中,验证环境中的对象可以创建和销毁,因此在软件测试端的资源占用是动态的;

    • 选择题1:下面关于仿真时程序和模块的说法哪些是正确的?
      A:硬件的模块可以看作独立的程序块
      B:initial和always可以看作独立的线程
      C:always线程不会结束
      D:initial线程一定会结束

    • 选择题2:如果需要降低仿真时的内存负载,那么下面哪些措施是合理的?
      A:降低模块之间的信号跳变频率
      B:只在必要的时候创建软件对象
      C:在不需要时钟的时候关闭时钟
      D:在数据带宽需求低的时候降低时钟频率

    • 软件环境中的initial块对语句有两种分组方式,使用begin-end或fork-join;
    • begin-end中的语句顺序执行,而fork-join中的语句并发执行;

    1.2. 线程的概念澄清

    • 线程的执行轨迹是呈树状结构的,即任何的线程都应该有父线程
    • 父线程可以开辟若干个子线程,父线程可以暂停或者终止子线程;
    • 当父线程终止时,其所开辟的所有子线程都应当会终止;

    2. 线程的控制

    2.1. fork并行线程语句块

    在这里插入图片描述

    2.2. 等待所有衍生线程

    • 在SV中,当程序中的initial块全部执行完毕,仿真器就退出了;
    • 如果希望等待fork块中的所有线程执行完毕再退出结束initial块,可以使用wait fork语句来等待所有子线程结束
    task run_threads;
    	fork
    		check_trans(tr1);  // 线程1
    		check_trans(tr2);  // 线程2
    		check_trans(tr3);  // 线程3	
    	
    	join_none
    	...
    	// 等待所有fork中的线程结束再退出task
    	wait fork;
    endtask
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    注意:

    • 上述task结束的情况下,SV中父线程结束了,并不会主动回收它开辟的子线程,僵尸线程
    • 建议在fork-join_none、fork-join_any开辟的线程,一旦认为它没有必要了,一定要给他打上名字,标记记号,方便后期主动disable

    2.3. 停止单个线程

    • 在使用了fork-join_any或者fork-join_none以后,可以使用disable来指令需要停止的线程
    parameter TIME_OUT = 1000;
    task check_trans(Transaction tr);
    	fork
    		begin
    			// 等待回应,或者达到某个最大时延
    			fork: time_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 time_block
    		end
    	join_none	
    endtask
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    2.4. 停止多个线程

    • 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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    `timescale 1ns/1ns
    
    module tb;
      task automatic child_t(int t, string name);
        forever #(t*1ns) $display("@%0t: child thread [%s] say hello", $time, name);
      endtask
      
      task automatic parent_t(int t = 3, string name = "parent_thread", int loop = 10);
        fork: child_thread
          child_t(4, "child_thread_A");
          child_t(5, "child_thread_B");
          child_t(6, "child_thread_C");
        join_none
        repeat(loop) #(t*1ns) $display("@%0t: parent thread [%s] say hello", $time, name);
        $display("@%0t: finish %s", $time, name);
      endtask
    
      initial begin: parent_thread
        parent_t();
      end
    
      initial begin
        #25ns;
         disable parent_thread;
        // $display("@%0t: disable parent_thread", $time);
        #50ns;
        $display("@%0t: finish current test", $time);
        $finish;
      end
    
    endmodule
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32

    在这里插入图片描述

    2.5. 停止被多次调用的任务

    • 如果给一个任务或者线程指明标号,那么当这个线程被调用多次以后,如果通过disabe去禁止这个线程标号,所有衍生的同名线程都将被禁止
    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);  // 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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 任务wait_for_time_out被调用三次,从而衍生了三个线程
    • 线程0在#2延时之后禁止了该任务,而由于三个线程均是“同名”线程,因此这些线程都被禁止了,最终也都没有完成

    3. 线程间的通信

    3.1. 概述

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

    3.2. event事件

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

    3.3. 在event的边沿阻塞

    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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    @0: 1: before trigger
    @0: 2: before trigger
    @0: 1: after trigger

    • 第一个初始化块启动,触发e1事件,然后阻塞在e2上
    • 第二个初始化块启动,触发e2事件,然后阻塞在e1上
    • e1和e2在同一个时刻被触发,但由于detla cycle的时间差使得两个初始化块可能无法等到e1或者e2
    • 所以,更安全的方式可以使用event的方法triggered()

    3.4. 等待事件的触发

    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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 可以使用电平敏感wait(e1.triggered())来代替边沿敏感的阻塞语句@e1
    • 如果事件在当前时刻已经被触发,则不会引起阻塞。否则,会一直等到事件被触发为止
    • 这个方法比起@而言,可以避免在相同时刻触发event而带来的竞争问题,但同样无法捕捉已经被触发,但后续才等待的事件

    3.5. 资源共享的需求

    • 对于线程之间共享资源的使用方式,应该遵循互斥访问(mutex access)原则
    • 控制共享资源的原因在于,如果不对其访问做控制,可能会出现多个线程对同一资源的访问,进而导致不可预期的数据损坏和线程的异常,这种现象成为之“线程不安全

    • 选择题1:下面关于semaphore的描述哪些是正确的?
      A:使用之前应该使用new()对其进行初始化
      B:如果semaphore初始化只有1个钥匙,那么2个对象同时请求时,只有1个对象可以获取
      C:如果semaphore初始化只有2个钥匙,那么2个对象同时请求时,2个对象都可以获取
      D:semaphore初始化时可以初始化为0个钥匙

    • semaphore没有管家,如果只定义了一把钥匙,如果还了一把,再还一把,这是不合理的,但是再代码上时允许了,会继续累加

    3.6. 通信要素的比较和应用

    • event:最小信息量的触发。即单一的通知功能。可以用来做事件的触发,也可以多个event组合起来用作线程之间的同步;
    • semaphore:共享资源的安全卫士。如果多线程间要对某一公共资源做访问,即可以使用这个要素;
    • mailbox:精小的SV原生FIFO。在线程之间做数据通信或者内部数据缓存时可以考虑使用此元素;
  • 相关阅读:
    使用EISeg自动标注数据,yolov5训练模型(保姆教程)
    初步了解 RabbitMQ
    Nginx “upstream sent no valid HTTP/1.0 header“错误,以及http status 009分析
    深入学习Linux中的“文件系统与日志分析”
    华为无线设备配置同一业务VLAN的AP间快速漫游
    Linux基础命令[15]-less
    「项目管理」甘特图制定项目计划的方法
    shell脚本实现Mysql分库分表备份
    振芯GM7123C:功能RGB转VGA芯片方案简介
    有哪些挣钱软件一天能赚几十元?盘点十个能长期做下去的挣钱软件
  • 原文地址:https://blog.csdn.net/Bunny9__/article/details/126200749