Search Unity

Modifying the same element in a Job

Discussion in 'Entity Component System' started by Tazadar66, Sep 20, 2018.

  1. Tazadar66

    Tazadar66

    Joined:
    Aug 27, 2013
    Posts:
    57
    Hello,

    I would like to modify the same ComponentData several times in one single job.
    I would like to know if this is possible.
    Is there any Atomic function to add data to the same Component to be sure no addition is lost durring the parallel work?

    I have tried this but it does not work :

    Code (CSharp):
    1. using Unity.Collections;
    2. using Unity.Entities;
    3. using Unity.Jobs;
    4.  
    5. public class TestSystem : JobComponentSystem
    6. {
    7.     public struct Data
    8.     {
    9.         public readonly int Length;
    10.         public EntityArray Entities;
    11.         public ComponentDataArray<CellDataComponent> CellData;
    12.     }
    13.  
    14.     [Inject] Data m_Data;
    15.  
    16.     public struct MyJob : IJobParallelFor
    17.     {
    18.         [ReadOnly] public EntityArray Entities;
    19.         public ComponentDataArray<CellDataComponent> CellData;
    20.         public EntityCommandBuffer.Concurrent EntityCommandBuffer;
    21.  
    22.         public void Execute(int i)
    23.         {
    24.             CellDataComponent cdc = CellData[0];
    25.             cdc.cost = cdc.cost + 1;
    26.             EntityCommandBuffer.SetComponent(Entities[0], cdc);
    27.         }
    28.     }
    29.  
    30.     // Barrier for updating components
    31.     [Inject] private TestBarrier m_barrier;
    32.  
    33.     protected override JobHandle OnUpdate(JobHandle inputDeps)
    34.     {
    35.         return new MyJob
    36.         {
    37.             Entities = m_Data.Entities,
    38.             CellData = m_Data.CellData,
    39.             EntityCommandBuffer = m_barrier.CreateCommandBuffer()
    40.         }.Schedule(m_Data.Length, 32, inputDeps);
    41.     }
    42. }
    43.  
    44. public class TestBarrier : BarrierSystem
    45. { }
    46.  
    I got that error :
    IndexOutOfRangeException: Index 0 is out of restricted IJobParallelFor range [32...63] in ReadWriteBuffer.
    ReadWriteBuffers are restricted to only read & write the element at the job index. You can use double buffering strategies to avoid race conditions due to reading & writing in parallel to the same elements from a job.

    Thanks for your help.
     
  2. Antypodish

    Antypodish

    Joined:
    Apr 29, 2014
    Posts:
    10,779
    You are trying illegal index 0 of entity in parallel job. This work on single threading, but no for multi threading.
    Code (CSharp):
    1. EntityCommandBuffer.SetComponent(Entities[0], cdc);
    All injected data is split across thread. So if you got 64 entities, and 2 threads, that mean each thread takes 32 entities generally speaking. In your case would be 32 per each set, if I understand correctly.
    Code (CSharp):
    1. Schedule(m_Data.Length, 32, inputDeps);
    That means, you are trying to access index 0 on one of thread, which doesn't hold such index. First thread will have items 0 to 31, and second thread will have 32 to 63. See, no 0 index on second thread. Hence, if you need access index 0 for example, you may need use single threading. Or perhaps inject data, which is common for any threads.
     
    Tazadar66 likes this.
  3. Tazadar66

    Tazadar66

    Joined:
    Aug 27, 2013
    Posts:
    57
    Thanks Antypodish for your answer :)

    I would like to be able to modify any index (that's why I tested with index 0), is there any approach for this or should I use a single thread Job ?
     
  4. Antypodish

    Antypodish

    Joined:
    Apr 29, 2014
    Posts:
    10,779
    It is complex problem, for relational structure, executed on multi threads.

    To approach the case, you need to define, which data can be modified and which not. I.e. [ReadOnly].
    You can not expect one thread to modify something with index 3, while other thread is reading, or writing it too.
    If you need operation on the threads, on entities with relations, I think you need make a copy of entities, of which the copied entities are only to read. Not sure if that is approach right tho.

    I simply avoid multi threading data, which is relational. Too risky to break a part structure.
     
  5. julian-moschuering

    julian-moschuering

    Joined:
    Apr 15, 2014
    Posts:
    529
    Generally you can enable writing to other locations than the current work item index using [NativeDisableParallelForRestriction], but this is exactly the case the safety mechanism was build for as what your try to do will fail. Only use this attribute when you have to write to a differnt index but you are sure each index is only written by exactly one work item.

    It depends on your case. If you have a reduction filter where you combine the data into a single value you could build a native container that gives your atomic operations on a memory location. If the threads don't do ALOT more than the atomic write you will end up with alot of wasted performance, a single thread will be much more efficient and even faster as syncing the writes will take alot of time. You can use a ParallelFor to reduce subsets of your data each one in a single thread and then combine the smaller result in a single thread. Eg having 32 iterations on a 1024 array. Each item reading 32 values, combining them and writing to a single index of a 32 length output array. Then run a single Job that combines the remaining 32 values.

    Still the most efficient method will always be a single thread for this case. If you can run other jobs in parallel on other data this would be the preferred method.

    Another note: Your CellData is writeable. You don't need an EntityCommandBuffer, just write to the ComponentDataArray. EntityCommandBuffer is only required when doing structural changes, eg adding/removing components or setting a SharedComponentData:
    Code (CSharp):
    1.  public void Execute(int i)
    2.         {
    3.             CellDataComponent cdc = CellData[0];
    4.             cdc.cost = cdc.cost + 1;
    5.             CellData[0] = cdc;
    6.         }
     
    Tazadar66 likes this.
  6. Tazadar66

    Tazadar66

    Joined:
    Aug 27, 2013
    Posts:
    57
    Do you have any advice to achieve that?

    Thank you, I did not know that :)
     
  7. julian-moschuering

    julian-moschuering

    Joined:
    Apr 15, 2014
    Posts:
    529
    Actually unity has a NativeCounter sample exactly for this case:

    Docs (scroll down to custom container): https://github.com/Unity-Technologi...ter/Documentation/content/custom_job_types.md

    Source: https://github.com/Unity-Technologi.../tree/master/Samples/Assets/NativeCounterDemo

    It actually creates a counter for each thread in independent cache lines resulting in writes not having to do any CPU synchronization. (NativePerThreadCounter) Reading the result needs to sum up the values. Sounds pretty good for your case.
     
    Tazadar66 likes this.
  8. Tazadar66

    Tazadar66

    Joined:
    Aug 27, 2013
    Posts:
    57
    Thank you very much :)
    Will a NativeArray of NativeCounter.Concurrent work ?
     
  9. julian-moschuering

    julian-moschuering

    Joined:
    Apr 15, 2014
    Posts:
    529
    No. But NativePerThreadCounter could easily be extended to have multiple counters.