Can't write concurrently to two different DynamicBuffers

Hi,
I was playing around with ECS and I’ve stumbled upon a puzzling problem. This is the code sample

    public partial class TestSystem : SystemBase {
        protected override void OnStartRunning() {
            Entity e1 = EntityManager.CreateEntity( typeof( Block ) );
            Entity e2 = EntityManager.CreateEntity( typeof( Block ) );

            Test( e1, e2 );
        }

        private void Test(Entity e1, Entity e2) {
            TestBufferJob job1 = new() {
                blocks = EntityManager.GetBuffer<Block>( e1 )
            };

            JobHandle handle1 = job1.Schedule();

            TestBufferJob job2 = new() {
                blocks = EntityManager.GetBuffer<Block>( e2 )
            };

            JobHandle handle2 = job2.Schedule();        

            handle2.Complete();
            handle1.Complete();
        }

        protected override void OnUpdate() { }
    }

    public struct TestBufferJob : IJob {
        public DynamicBuffer<Block> blocks;

        public void Execute() {
            blocks.Add( new() );
            blocks.Add( new() );
            blocks.Add( new() );
            blocks.Add( new() );
        }
    }

The Block is just an IBufferElementData with its InternalBufferCapacity = 0 and one uint field.
My question is: Why does this crash? Why can’t I write to those two buffers concurrently? My thinking is, those buffers have internal buffer capacity of zero so it should be the same as writing to some NativeArrays.

You can’t Add in parallel because if the underlying array needs to resize it’s not thread safe anymore

Few options here

  1. Pass dependency of a job to another, so they won’t run in parallel
  2. Allocate whole required capacity up front, and increment the count atomic (Interlocked.Add), then write to the index. You can check the implementation of NativeList.ParallelWriter for this, AddNoResize method
  3. If you’re using an Array, set-up the Jobs so that they won’t work on overlapping indices

Okay - I need to use the dynamic buffer like I would use an array anyway (AFAIK you shouldn’t use Native Arrays in IComponentData), so its not a problem to allocate the whole capacity up from, though even doing this:

public partial class TestSystem : SystemBase {
    protected override void OnStartRunning() {
        Entity e1 = EntityManager.CreateEntity( typeof( Block ) );
        Entity e2 = EntityManager.CreateEntity( typeof( Block ) );

        DynamicBuffer<Block> blocks1 = EntityManager.GetBuffer<Block>( e1 );
        blocks1.Resize(c.cs * c.cs * c.cs, NativeArrayOptions.UninitializedMemory);

        DynamicBuffer<Block> blocks2 = EntityManager.GetBuffer<Block>( e1 );
        blocks2.Resize(c.cs * c.cs * c.cs, NativeArrayOptions.UninitializedMemory);
      
        Test( e1, e2 );
    }

    private void Test(Entity e1, Entity e2) {
        TestBufferJob job1 = new() {
            blocks = EntityManager.GetBuffer<Block>( e1 )
        };

        JobHandle handle1 = job1.Schedule();

        TestBufferJob job2 = new() {
            blocks = EntityManager.GetBuffer<Block>( e2 )
        };

        JobHandle handle2 = job2.Schedule();      

        handle2.Complete();
        handle1.Complete();
    }

    protected override void OnUpdate() { }
}

public struct TestBufferJob : IJob {
    public DynamicBuffer<Block> blocks;

    public void Execute() {
        blocks[0] = new(){value = 1u};
        blocks[1] = new(){value = 4u};
        blocks[2] = new(){value = 7u};
        blocks[3] = new(){value = 9u};
    }
}

doesn’t seem to help, but apart from that, I really don’t get it - why can two different Native Lists resize in parallel but two different Dynamic Buffers can’t resize in parallel? I would expect a Dynamic Buffer of buffer capacity of 0 to behave similarly to a Native List.
Those are two different buffers so such behavior seems unintuitive.

Sorry I didn’t realize you were using two different Entities in your code. In that case your initial code should just work.

I believe that this is not the “standard” way of working with DynamicBuffers (but could be wrong). The standard way is to use a query and access the DynamicBuffer from there. One thing I’ve learned when working with DOTS is to don’t stray away from the usual usage if you don’t know what you’re doing. If you’re worried about parallelism, there are ways to make it work when using queries.

Well, yeah, I was previously using jobs with GameObjects so my usual approach was to just use NativeArrays. I wanted a system where I could schedule a lot of very heavy jobs on entities without flooding all of my CPU’s threads (because some of them are needed for Unity’s render jobs and its other elements) but its seems like now I will have to figure out another way of doing that.