Job Thread Safety, [readonly], Performance Questions

In all the documentation examples, the job system is reading from source data that never changes, and writes to destination buffers that are never in contention. This isn’t helpful in explaining what is thread-safe and what is not.

Example 1:
In C++ the sum would be an atomic operation, so this is thread-safe provided values never changes, but I’m not sure here.

struct SumArray: IJobParallelFor
{
    public NativeArray<float> values;
    public float sum = 0;

    public void Execute (int index)
    {
        sum += values[index];
    }
}

Example 2:
It isn’t clear what [ReadOnly] does or when the data is blitted
Case 1: The data is blitted under the hood in “job.values = values;” or “job.Schedule”. This would be threadsafe
Case 2: “NativeArray values” is only a pointer. This is not threadsafe, because over multiple frames SumArray2Behaviour.values is changing

struct SumArray2: IJobParallelFor
{
    [ReadOnly]
    public NativeArray<float> values;
    public float sum = 0;

    public void Execute (int index)
    {
        sum += values[index];
    }
}

public class SumArray2Behaviour : MonoBehaviour
{
    NativeArray<float> values;
    void Awake()
    {
        values = new NativeArray<Vector3>(20, Allocator.Persistent);
    }
    void Update()
    {
        for (int i = 0; i < 20; i++)
            values[i] = UnityEngine.Random.value;
    }
    public SumArray2 Schedule()
    {
        SumArray2 job = new SumArray2();
        job.values = values;
        job.Schedule();
        return job;
    }
}

Related to Example 2, what does [ReadOnly] do?

  1. NativeArray is a pointer to somewhere else. This means that [ReadOnly] means the source data does not change
  2. NativeArray is an automatically (?) blitted copy of data. This means that [ReadOnly] indicates the thread does not change the blitted copy of data.

Assuming the data is not blitted, do I need to blit the values manually if using [ReadOnly]?
Assume Unity does not blit the data in “job.values = values;” or “job.Schedule”.
By manual blit I mean:: On every usage of job.Schedule() I need to allocate a new NativeArray(), copy the source data to the new NativeArray(), and then schedule the job to work on the new NativeArray(). This ensure the thread is working on a copy of my data

Should the source data in MonoBehaviour be a NativeArray?
Lets say in game code every Update() I want to write an array values, which I know I will pass to the job system.
Option 1. NativeArray instead of Vector3 [ ]
NativeArray samples;
samples = new NativeArray(20, Allocator.Persistent);
Option 2: Use Vector3[ ], but blit to NativeArray every time I process the data
Vector3 [ ] samples;
samples = new Vector3 [20];

Both examples are “thread safe” they each worker thread writes to its own sum variable. But they also have no output. Because they simply write to their own thread local sum variable…

If you want to output data you need to write to a Native container. Like a NativeArray.

You have an example of how to create a custom native container here.
Look for NativeCounter example as it pretty much does what you want.

After some research here are what I believe are the answers to my questions:

With NativeArray, does blitting occur?
Assuming the data is not blitted, do I need to blit the values manually if using [ReadOnly]
No, and Yes. I attempted to modify a NativeArray immediately after calling Schedule() to see what would happen - in C++ this would lead to undetermined behavior. However, Unity threw an exception upon doing so. Effectively, this means I can’t change the array after calling Schedule(), which forces me to copy the values to the NativeArray.

Related to Example 2, what does [ReadOnly] do?
Given the additional context of not user-code not being able to modify values, I believe this means the thread won’t modify the data in the NativeArray

Should the source data in MonoBehaviour be a NativeArray?
If it is never modified then yes, otherwise no, it needs to be copied.

Additional question:
Paraphrasing the documentation, IJobParallel needs to run on each element of the array independently. However, the following job seems to work when used with IJobParallelFor, despite not being independent. Can I get confirmation if this is OK to do?

public struct WeightedVectorSumJob : IJobParallelFor
    {
        [ReadOnly]
        NativeArray<Vector3> samples;
        NativeArray<Vector3> sum;

        static public readonly int sampleCount = 20;

        int writeIdx;

        public void Setup(int _writeIdx, NativeArray<Vector3> _samples, NativeArray<Vector3> _sum)
        {
            sum = _sum;
            writeIdx = _writeIdx;
            samples = _samples;
        }

        // IJob version
        public void Execute()
        {
            for (int i = 0; i < sampleCount; i++)
            {
                int weight = (sampleCount - (writeIdx - i)) % sampleCount;
                sum[0] += samples[i] * weight;
            }
        }

        // IJobParallelFor version
        public void Execute(int i)
        {
            //for (int i = 0; i < sampleCount; i++)
            {
                int weight = (sampleCount - (writeIdx - i)) % sampleCount;
                sum[0] += samples[i] * weight;
            }
        }
    }

I am quite certain that your sample code using parallel for will throw an exception from the safety system. (Writing to sum[0] out of the allowed range, because there is in fact a race condition)

I just tried it, both in a sample and in-game, and it did not throw an exception. But I’ll change it to not use Parallel.