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,
1) constructing the object,
2) randomizing it,
3) putting it in the mailbox
1) constructing the object,
2) randomizing it,
3) 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.
Reference:
1) SystemVerilog for Verification 3rd edition, by Chris Spear
1) SystemVerilog for Verification 3rd edition, by Chris Spear
Hi Sagar,
ReplyDeleteThanks 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?
Thanks,
Jason
Hi Jason,
DeleteThanks for pointing out this. I had removed duplicate example.
great explanation ,it is a great help ....keep posting topics like this...
ReplyDeleteWow :)
ReplyDeleteThis is an incredible collection of ideas!
Waiting for more helpful pieces.
You would amazing to read a similar one here-
besttoolsbrand
Iamlinkfeeder
ReplyDeleteIamlinkfeeder
Iamlinkfeeder
Iamlinkfeeder
Iamlinkfeeder
Iamlinkfeeder
Iamlinkfeeder
Iamlinkfeeder
Iamlinkfeeder
Iamlinkfeeder