SystemVerilog之线程间通信

SystemVerilog 中的线程(Thread)是仿真过程中并发执行的基本单元,用于描述硬件或验证平台的并发行为。在复杂的数字系统验证中,多线程协同工作是实现高效验证的关键。SystemVerilog提供了强大的线程控制机制和多种线程间通信(Inter-Process Communication, IPC)方法,能够有效解决并发执行中的同步与数据交换问题。本文深入探讨SystemVerilog中的事件(Event)、旗语(Semaphore)、信箱(Mailbox)三个核心机制,并通过代码示例分析其应用场景。

1 线程基础与通信需求

SystemVerilog通过fork...join、fork...join_any和fork...join_none实现线程的动态创建。由于线程可能同时访问共享资源或需要协调执行顺序,通信机制成为避免竞争条件(Race Condition)和保证数据完整性的关键。

fork...join会阻塞父线程直到所有子线程执行完成,用于严格同步;

fork...join_any在任一子线程完成后即恢复父线程,其余子线程后台运行,适用于超时控制;

fork...join_none不阻塞父线程,直接启动子线程并行执行,常用于异步任务触发。三者分别对应“全等待”、“任一触发”和“无等待”的并发模式。SystemVerilog之线程间通信_第1张图片

2 核心通信机制详解

2.1事件(Event):硬件级同步触发器

Verilog事件可以实现线程的同步。就像在打电话时一个人等待另一个人的呼叫,在Verilog中,一个线程总是要等待一个带@操作符的事件。这个操作符是边沿敏感的,所以它总是阻塞着,等待事件的变化。其他的线程可以通过->操作符来触发事件,解除对第一个线程的阻塞。

SystemVerilog从几个方面对Verilog事件做了增强。事件现在成为了同步对象的句柄,可以传递给子程序。这个特点允许你在对象间共享事件,而不用把事件定义成全局的。最常见的方式是把事件传递到一个对象的构造器中。

在Verilog中,当一个线程在一个事件上发生阻塞的同时,正好另一个线程触发了这个事件,则竞争的可能性便出现了。如果触发线程先于阻塞线程执行,则触发无效SystemVerilog引人triggered()函数,可用于查询某个事件是否已被触发,包括在当前时刻。线程可以等待这个函数的结果,而不用在@操作符上阻塞。

用一段代码来说明:

event my_event;

// 线程1:触发事件两次
initial begin
  #10ns;
  -> my_event;  // 第一次触发(@ 会捕获,wait 也会捕获)
  #10ns;
  -> my_event;  // 第二次触发(@ 可能错过,wait 会捕获)
end

// 线程2:使用 @ 操作符(边沿敏感,可能错过事件)
initial begin
  #15ns;  // 延迟,使第一次触发已经发生
  @(my_event);  // 等待下一个事件跳变(可能错过第一次触发)
  $display("@ 捕获事件 at %0t", $time);
end

// 线程3:使用 wait(event.triggered)(电平敏感,不会错过事件)
initial begin
  #15ns;  // 延迟,使第一次触发已经发生
  wait(my_event.triggered);  // 检测事件是否被触发过(不会错过)
  $display("wait 捕获事件 at %0t", $time);
end

运行结果:

@ 捕获事件 at 20
wait 捕获事件 at 15

2.2旗语(Semaphore):资源访问控制锁

semaphore可以实现对同一资源的访问控制。对于初学者而言,无论线程之间在共享什么资源,都应该使用semaphore等资源访问控制的手段,以此避免可能出现的问题。semaphore有三种基本操作,new()方法可以创建一个带单个或者多个钥匙的semaphore,使用get()可以获取一个或者多个钥匙,而put()可以返回一个或者多个钥匙。如果你试图获取一个semaphore而希望不被阻塞,可以使用try_get()函数。它返回1表示有足够多的钥匙,而返回0则表示钥匙不够。

module semaphore_example;
  semaphore sem = new(2); // 初始化旗语,允许最多 2 个线程同时访问
  
  // 共享资源(模拟内存访问)
  logic [7:0] shared_memory [0:255];
  
  // 线程1:尝试访问共享资源
  initial begin : thread1
    $display("[%0t] Thread1: 尝试获取资源...", $time);
    sem.get(1); // 获取 1 个钥匙(如果无钥匙可用,则阻塞)
    $display("[%0t] Thread1: 获取资源成功,开始操作...", $time);
    #10ns; // 模拟操作耗时
    sem.put(1); // 释放钥匙
    $display("[%0t] Thread1: 操作完成,释放资源", $time);
  end
  
  // 线程2:尝试访问共享资源
  initial begin : thread2
    $display("[%0t] Thread2: 尝试获取资源...", $time);
    sem.get(1); // 获取 1 个钥匙
    $display("[%0t] Thread2: 获取资源成功,开始操作...", $time);
    #20ns; // 模拟操作耗时
    sem.put(1); // 释放钥匙
    $display("[%0t] Thread2: 操作完成,释放资源", $time);
  end
  
  // 线程3:尝试访问共享资源
  initial begin : thread3
    $display("[%0t] Thread3: 尝试获取资源...", $time);
    sem.get(1); // 获取 1 个钥匙
    $display("[%0t] Thread3: 获取资源成功,开始操作...", $time);
    #15ns; // 模拟操作耗时
    sem.put(1); // 释放钥匙
    $display("[%0t] Thread3: 操作完成,释放资源", $time);
  end
endmodule

运行结果

[0] Thread1: 尝试获取资源...
[0] Thread1: 获取资源成功,开始操作...
[0] Thread2: 尝试获取资源...
[0] Thread2: 获取资源成功,开始操作...
[0] Thread3: 尝试获取资源... (阻塞,因为 semaphore 已满)
[10] Thread1: 操作完成,释放资源
[10] Thread3: 获取资源成功,开始操作...
[15] Thread3: 操作完成,释放资源
[20] Thread2: 操作完成,释放资源

2.3信箱(Mailbox):线程安全通信管道

线程之间传递信息可以使用mailbox,它是一种类似于队列(queue)的对象,需要通过new()进行实例化,并可选择指定size参数来限制其最大存储容量;若size为0或未指定,则mailbox容量无限,能存储任意数量的数据。

使用put()方法可以向mailbox写入数据,若mailbox已满,该方法会阻塞,直到有空间可用;使用get()方法可以从中读取并移除数据,若mailbox为空,该方法也会阻塞;此外,peek()方法可以获取数据的拷贝而不移除它,同样会在mailbox为空时阻塞。由于这些方法可能涉及等待,因此在设计线程同步机制时,必须明确区分阻塞方法(如put()、get()、peek())和非阻塞方法(如try_put()、try_get()),以避免死锁或性能问题,确保线程间的高效通信和协调。

module mailbox_example;
  mailbox #(int) mbx = new(3); // 创建容量为 3 的邮箱(存储 int 类型数据)
  
  // 生产者线程:发送数据到邮箱
  initial begin : producer
    for (int i = 1; i <= 5; i++) begin
      #5ns; // 模拟数据生成延迟
      if (mbx.try_put(i)) begin
        $display("[%0t] Producer: 发送数据 %0d", $time, i);
      end else begin
        $display("[%0t] Producer: 邮箱已满,无法发送 %0d", $time, i);
      end
    end
  end
  
  // 消费者线程:从邮箱接收数据
  initial begin : consumer
    int received_data;
    #10ns; // 延迟启动,让生产者先发送一些数据
    forever begin
      #10ns; // 模拟处理延迟
      if (mbx.try_get(received_data)) begin
        $display("[%0t] Consumer: 接收数据 %0d", $time, received_data);
      end else begin
        $display("[%0t] Consumer: 邮箱为空,无数据可读", $time);
      end
    end
  end
  
  // 仿真结束条件(避免无限循环)
  initial begin
    #50ns;
    $display("[%0t] 仿真结束", $time);
    $finish;
  end
endmodule

运行结果

[5] Producer: 发送数据 1
[10] Producer: 发送数据 2
[10] Consumer: 接收数据 1
[15] Producer: 发送数据 3
[20] Producer: 发送数据 4
[20] Consumer: 接收数据 2
[25] Producer: 发送数据 5
[25] Producer: 邮箱已满,无法发送 5
[30] Consumer: 接收数据 3
[40] Consumer: 接收数据 4
[50] 仿真结束

在SystemVerilog中,event、semaphore和mailbox是三种关键的线程同步与通信机制,各自适用于不同的场景。

event是最轻量级的同步机制,主要用于简单的触发通知,比如标志某个事件的发生。它本身不携带数据,仅提供信号通知功能,适合线程间的简单同步。多个event可以组合使用,实现更复杂的同步逻辑。

semaphore用于管理共享资源的访问,确保多线程环境下对临界区的安全操作。它通过计数机制控制资源的使用权,比如限制同时访问某个资源的线程数量,防止数据竞争或冲突。

mailbox是SystemVerilog内置的线程间通信FIFO,支持数据的缓存和传递。相比event和semaphore,它不仅能同步线程,还能存储和传输数据,适用于生产者-消费者模型等需要数据交换的场景。

总结来说,event适用于简单通知,semaphore用于资源访问控制,而mailbox则适合数据通信。合理选择这些机制可以高效实现线程间的同步与数据交互。

你可能感兴趣的:(开发语言)