Why threads order yields different results for fork...join_none?

Hi, can somebody help me to understand why the threads order matter in the fork … join_none/join_any for this case?
If fork … join_none/join_any spawn multiple threads, why the order matters for the mailbox case?


program mailbox_ex;
  mailbox checker_data = new();
  
  initial begin
    fork
      //consumer();
      producer(0);
      producer(1);
      producer(2);
      consumer();
    join_none
    #500;
  end
        
  task automatic producer(input int tno);
    begin
      bit[7:0] data;
      for(int i=0; i<1; i++) begin
        data = $random();
        checker_data.put(data);
        $display($time,,"[Thread:%0d], Put data:%0h",tno,data);
        #1;
      end
    end
  endtask
        
  task automatic consumer();
    begin
      bit [7:0] data;
      while(1) begin
        if(checker_data.num() > 0) begin
          checker_data.get(data);
          $display($time,,"Got data:%0h",data);
        end
        else begin
          #1;
        end
      end
    end
  endtask
endprogram


case 1:
fork
consumer();
producer(0);
producer(1);
producer(2);
join_none

All the data producing at time 0, and consuming completed at time 1;

               0 [Thread:0], Put data:24
               0 [Thread:1], Put data:81
               0 [Thread:2], Put data:9
               1 Got data:24
               1 Got data:81
               1 Got data:9

case 2:
fork
//consumer();
producer(0);
producer(1);
producer(2);
consumer();
join_none

All the data producing and consuming completed at time 0;

               0 [Thread:0], Put data:24
               0 [Thread:1], Put data:81
               0 [Thread:2], Put data:9
               0 Got data:24
               0 Got data:81
               0 Got data:9

In reply to mlsxdx:

Because of the way you coded your consumer task, if it executes before any producer gets a chance to put anything it the mailbox, the loop has a #1 before it checks the mailbox num() method again.

There is no reason to have this check as the get() blocks until there is something in the mailbox. In general, it is never a good idea to add #1’s to your code.

In reply to dave_59:

Thanks for your reply, dave.
Actually one thing I want to understand is that


fork
consumer();
producer(0);
producer(1);
producer(2);
join_none

at #0, why checker_data.num() == 0?

fork
producer(0);
producer(1);
producer(2);
consumer();
join_none
at #0, why checker_data.num() == 3?

First, for the case that multiple threads share one mailbox, how to make sure that user can predict the result?
Second, based on your suggestion to avoid add #1, one problem is that the while-loop will never advance without delay. Can you give some more details on how to improve this consumer task?

Thanks.

In reply to mlsxdx:

For case 1 and case 2, you cannot rely on the ordering of the processes started by a fork/join block - these are race conditions. It just so happens that your simulator chooses to execute them in the order they appear in the source code. If you need a predictable ordering, then you need to use a begin/end instead of fork/join.

For case 1, the consumer executes first before any producer has done a put() into the mailbox, so checker_data.num() == 0. For case 2, the consumer executes last after all producers have done put()s into the mailbox, so checker_data.num() == 3.

As I said above, the get() is your blocking delay, there is no need for a #1.

 task automatic consumer();
      bit [7:0] data;
      while(1) begin // I would use 'forever begin'
          checker_data.get(data); // blocks until the get() succeeds 
          $display($time,,"Got data:%0h",data);
      end
  endtask

Also, use $urandom instead of $random. You get stable seeding of each process regardless of execution ordering of the threads.

In reply to dave_59:

Thanks for your excellent explanation.
The delay in while-loop is unnecessary because the checker_data.get() will block the process by itself as you pointed out.