Search Unity

Resolved Can't get job dependencies to work how I expect

Discussion in 'Entity Component System' started by atr0phy, Jun 5, 2022.

  1. atr0phy

    atr0phy

    Joined:
    Nov 5, 2014
    Posts:
    43
    Having a lot of trouble wrapping my ahead around the dependency chain. It refuses to let me read from the data written in the FooJob from the BarJob. I'll simplify my actual code as much as possible to make it clear what I'm doing. Note: I'm calling .Complete() and disposing of the _results collection at the beginning of my Update loop, not shown here.

    Code (CSharp):
    1.      {
    2.           _barResults = new NativeList<int3>(Allocator.TempJob);
    3.           NativeList<int2> fooData = new NativeList<int2>(Allocator.TempJob);
    4.  
    5.           FooJob fooJob = new FooJob
    6.           {
    7.               fooData = fooData,
    8.           };
    9.  
    10.           BarJob barJob = new BarJob
    11.           {
    12.               fooDataIn = fooData,
    13.               barResults = _barResults,
    14.           };
    15.  
    16.           _dependency = fooJob.Schedule(_dependency);
    17.           _dependency = barJob.Schedule(fooData.Length, 128, _dependency);
    18.  
    19.           fooData.Dispose();
    20.      }
    21.  
    22.      public struct FooJob : IJob
    23.      {
    24.           [WriteOnly]
    25.           NativeList<int2> fooData;
    26.  
    27.           public void Execute() { /*Do stuff*/ }
    28.      }
    29.  
    30.      public struct BarJob : IJobParallelFor
    31.      {
    32.           [ReadOnly]
    33.           public NativeList<int2> fooDataIn;
    34.        
    35.           [WriteOnly]
    36.           public NativeList<int3> barResults;
    37.  
    38.           public void Execute(int index) { /*Do other stuff*/ }
    39.      }
    Which will throw the exception:
    I've tried passing the fooData into the BarJob as a deferred array, which yields no difference in results:
    Code (CSharp):
    1.      fooDataIn = fooData.AsDeferredJobArray(),
    I've also tried to defer disposal of the fooData via the JobHandle, which also yields no difference in results.
    Code (CSharp):
    1.      _dependency = fooJob.Schedule(_dependency);
    2.      _dependency = barJob.Schedule(fooData.Length, 128, _dependency);
    3.      fooData.Dispose(_dependency);
    So my questions are:
    1. Why is this dependency chain not working how I expect? Isn't this the purpose of the dependency chain, to make the access of data from one job dependent on the previous job?
    2. Why does using a deferred array and deferring disposal of the collection have no effect on my results? Isn't that their purpose, to tell the system that they need to hold off until my 1st job is done writing to it?
    I've read thru just about every thread I can find for help. And I feel like it should be working how I expect, but it's not. I can't tell if there's a dramatic gap in my understanding here or if it's just something silly I'm overlooking. Some guidance would really be appreciated.
     
  2. CodeSmile

    CodeSmile

    Joined:
    Apr 10, 2014
    Posts:
    5,870
    The key lies with fooData.Length which you use right after scheduling fooJob. At this point it is not guaranteed that fooData has its final length as it may currently be modified by the fooJob.
     
  3. atr0phy

    atr0phy

    Joined:
    Nov 5, 2014
    Posts:
    43
    I think this is where my gap in understanding is
    I think this is where my gap in understanding is. Because at that particular point, none of the data that I want to read from is guaranteed, is this correct? I assumed this is what the purpose of deferring or settings dependencies was, that under the hood Unity did some magic to work with whatever data it could when it could and deferred working on anything that wasn't yet reliably known.

    I had previously considered fooData.Length was the source of the problem, so I had replaced it with a magic number to test. This yields a different (but probably related) error which is thrown on the very first line of BarJob's Execute() method where I try to access the fooData:

    Code (CSharp):
    1. IndexOutOfRangeException: Index 0 is out of range of '0' Length.
    Which unfortunately just leads me right back to my original questions.
     
  4. CodeSmile

    CodeSmile

    Joined:
    Apr 10, 2014
    Posts:
    5,870
    What‘s important to understand here is that fooData.Length means reading from fooData. If fooData were fixed size you could assign fooData.Length to a local variable before scheduling both jobs and use the fixed length as the job array length. In that case dependencies will work as expected since you have no dependency on the array itself.

    The JobHandle dependency means that the latter job only starts executing after the first completes. However, here you use the Length already in the call to Schedule, not within the job itself, therefore it‘s essentially the same as writing:
    var x = fooData[0];
    right after scheduling fooJob. Which would be a race condition.

    It may be worthwhile to consider whether you can determine the list length ahead of time. Alternatively you can use a regular IJob and iterate over the fooData list in bar’s Execute though in that case it will run on a single background thread. Or simply call fooJob.Complete() before scheduling the bar job, in that case you don‘t need to use the dependency handle.
     
    bb8_1 and atr0phy like this.
  5. DreamingImLatios

    DreamingImLatios

    Joined:
    Jun 3, 2017
    Posts:
    4,262
    You might also want to use IJobParallelForDefer, which evaluates the length of the list when the job starts rather than on the main thread at schedule time.
     
    bb8_1, Razmot, atr0phy and 2 others like this.
  6. atr0phy

    atr0phy

    Joined:
    Nov 5, 2014
    Posts:
    43
    Thanks for these tips, and for your help in general. Knowing that a dependent job is simply waiting for the initial job to complete without any other "promises" regarding the data in contains helped make this click in my head. I do know that the simplest solution here would be to create a sync point, but I wanted to challenge my understanding since I know I'll be facing these as real world problems in the near future.

    Thank you as well for chiming in. This is the solution I ended up going with. After changing my BarJob to an IJobParallelForDefer and changing my _barResults into a ParallelWriter I'm now getting exactly the behavior I was expecting.

    Appreciate all your help!
     
    bb8_1 likes this.