Thursday, 17 December 2015

Mailbox in SystemVerilog

How do you pass information between two threads? Perhaps your generator needs to create many transactions and pass them to a driver. You might be tempted to just have the generator thread call a task in the driver. If you do that, the generator needs to know the hierarchical path to the driver task, making your code less reusable. Additionally, this style forces the generator to run at the same speed as the driver, which can cause synchronization problems if one generator needs to control multiple drivers. The channel must allow its driver and receiver to operate asynchronously. You may be tempted to just use a shared array or queue, but it can be difficult to create threads that read, write, and blocks safely.

The solution is a SystemVerilog mailbox. From a hardware point of view, the easiest way to think about a mailbox is that it is just a FIFO, with a source and sink. The source puts data into the mailbox, and the sink gets values from the mailbox.

A mailbox is a communication mechanism that allows messages to be exchanged between processes. Data can be sent to a mailbox by one process and retrieved by another.

Conceptually, mailboxes behave like real mailboxes. When a letter is delivered and put into the mailbox, a person can retrieve the letter (and any data stored within). However, if the letter has not been delivered when the mailbox is checked, the person must choose whether to wait for the letter or to retrieve the letter on a subsequent trip to the mailbox. Similarly, SystemVerilog’s mailboxes provide processes to transfer and retrieve data in a controlled manner. Mailboxes are created as having either a bounded or unbounded queue size. A bounded mailbox becomes full when it contains the bounded number of messages. A process that attempts to place a message into a full mailbox shall be suspended until enough room becomes available in the mailbox queue. Unbounded mailboxes never suspend a thread in a send operation.

Mailbox is a built-in class that provides the following methods:
— Create a mailbox: new()
— Place a message in a mailbox: put()
— Try to place a message in a mailbox without blocking: try_put()
— Retrieve a message from a mailbox: get() or peek()
— Try to retrieve a message from a mailbox without blocking: try_get() or try_peek()
— Retrieve the number of messages in the mailbox: num()

A put() blocks if the mailbox is full, and get() blocks if the mailbox is empty.
Use try_put() if you want to see if the mailbox is full. And try_get() to see if it is empty. Both are non-blocking methods.
If they are successful, they return a nonzero value; otherwise, they return 0. In other words,
If the mailbox is full, the method try_put() returns 0.
If the mailbox is empty, then the method try_get() or try_peek() returns 0.

These are more reliable than the num() function, as the number of entries can change between when you measure it and when you next access the mailbox.
The peek() task gets a copy of the data in the mailbox but does not remove it.

The data is a single value, such as an integer, or logic of any size or a handle. A mailbox never contains objects, only references to them.

The default mailbox is typeless, that is, a single mailbox can send and receive any type of data. This is a very powerful mechanism, which, unfortunately, can also result in run-time errors due to type mismatches (types not equivalent) between a message and the type of the variable used to retrieve the message. Frequently, a mailbox is used to transfer a particular message type, and, in that case, it is useful to detect type mismatches at compile time.

A classic mailbox bug:

A loop that randomizes objects and puts them in a mailbox, but the object is only constructed once, outside the loop. Since there is only one object, it is randomized over and over.
Below figure shows all the handles pointing to a single object. A mailbox only holds handles, not objects, so you end up with a mailbox containing multiple handles that all point to the single object. The code that gets the handles from the mailbox just sees the last set of random values.

The overcome this bug; make sure your loop has all three steps of constructing the object, randomizing it, and putting it in the mailbox.
The result, shown in below figure, is that every handle points to a unique object. This type of generator is known as the Blueprint Pattern.

Bounded Mailboxes:

By default, mailboxes are similar to an unlimited FIFO — a producer can put any number of objects into a mailbox before the consumer gets the objects out. However, you may want the two threads to operate in lockstep so that the producer blocks until the consumer is done with the object.

You can specify a maximum size for the mailbox when you construct it. The default mailbox size is 0 which creates an unbounded mailbox. Any size greater than 0 creates a bounded mailbox. If you attempt to put more objects than this limit, put() blocks until you get an object from the mailbox, creating a vacancy.

Synchronized Threads Using a Bounded Mailbox and a Peek:

In many cases, two threads that are connected by a mailbox should run in lockstep, so that the producer does not get ahead of the consumer. The benefit of this approach is that your entire chain of stimulus generation now runs in lock step.

To synchronize two threads, the Producer creates and puts a transaction into a mailbox, then blocks until the
Consumer finishes with it. This is done by having the Consumer remove the transaction from the mailbox only when it is finally done with it, not when the transaction is first detected.

Below example shows the first attempt to synchronize two threads, this time with a bounded mailbox. The Consumer uses the built-in mailbox method peek() to look at the data in the mailbox without removing. When the Consumer is done processing the data, it removes the data with get() . This frees up the Producer to generate a new value. If the Consumer loop started with a get() instead of the peek() , the transaction would be immediately removed from the mailbox, so the Producer could wake up before the Consumer finished with the transaction.

You can see that the Producer and Consumer are in lockstep, but the Producer is still one transaction ahead of the Consumer. This is because a bounded mailbox with size=1 only blocks when you try to do a put of the second transaction.

1 comment:

  1. Hi Sagar,

    Thanks for the detailed explanation, it's really helpful.
    BTW, it looks like that the example under the subsection "Bounded Mailboxes" is duplicated with the previous one, would you like to check it?