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/

 

 


 

posted @ 2021-04-06 19:55  徘徊的游鱼  阅读(1330)  评论(0编辑  收藏  举报