Inspired by ** this thread ** and this thread I’ve created what feels like a nice solution to job dependency management for NativeContainers. I haven’t seen any implementations shared so I thought I’d post it here to get feedback and hopefully help someone else on their journey.
What I like about this implementation is that
-
There’s minimal boilerplate needed to consume the NativeContainer
-
It’s much easier to manage than exposing the host system’s Dependency property directly with no risk of accidentally overwriting the existing dependencies.
-
It’s relatively easy to debug during development when you forget to call ReleaseAsync() after an AcquireAsync().
-
The last method to call Acquire or AcquireAsync is recorded so it can be included in the error message if needed.
-
No knowledge of other systems interacting with the container is required to use and parallel read access is granted when possible.
Of course, If you try to force complete the dependency (JobHandle.Complete()) you still run the risk of waiting on a substantial amount of work to complete synchronously up the chain. I don’t see any way around that issue unless Unity provides the ability to reorder and traverse a JobHandle’s dependency tree.
Implementation and sample use below. I’m interested to hear any questions, feedback or if this helps you out!
NativeArrayAccessController
/*
MIT License
Copyright (c) 2022 Decline Cookies
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
using System;
using Unity.Collections;
using Unity.Jobs;
using UnityEngine;
namespace Anvil.Unity.DOTS.Jobs
{
/// <summary>
/// A wrapper class for managing async access to a NativeArray{T}.
/// </summary>
public class NativeArrayAccessController<T> : IDisposable where T : struct
{
private enum AcquisitionState
{
Unacquired,
ReadOnly,
ReadWrite,
}
private readonly NativeArray<T> m_Value;
/// <summary>
/// The handle to wait on before <see cref="m_Value"> can be read.
/// </summary>
private JobHandle m_ReadAccessDependency = default;
/// <summary>
/// The handle to wait on before <see cref="m_Value"> can be written to.
/// </summary>
private JobHandle m_WriteAccessDependency = default;
private AcquisitionState m_State = AcquisitionState.Unacquired;
private bool m_IsDisposed = false;
/// <summary>
/// Creates a new <see cref="NativeArrayAccessController{T}"/> with a given length and init options.
/// </summary>
/// <param name="length">The length of the <see cref="NativeArray{T}".</param>
/// <param name="options">The init options for the <see cref="NativeArray{T}".</param>
public NativeArrayAccessController(int length, NativeArrayOptions options)
{
m_Value = new NativeArray<T>(length, Allocator.Persistent, options);
}
public void Dispose()
{
// NOTE: If these asserts trigger we should think about calling Complete() on these job handles.
Debug.Assert(m_ReadAccessDependency.IsCompleted, "The read access dependency is not completed");
Debug.Assert(m_WriteAccessDependency.IsCompleted, "The write access dependency is not completed");
(m_Value as IDisposable)?.Dispose();
}
/// <summary>
/// Get the <see cref="NativeArray{T}"/> returning a JobHandle to wait on before consuming.
/// After the work with the array is scheduled <see cref="ReleaseAsync"/> must be called before any other calls to
/// <see cref="AcquireAsync" /> or <see cref="Acquire" /> are made for this array.
/// </summary>
/// <param name="isReadOnly">The access level required for the value. Accessing readonly will tend to require less waiting.</param>
/// <param name="value">The array.</param>
/// <returns>A JobHandle to wait on before consuming the array.</returns>
public JobHandle AcquireAsync(bool isReadOnly, out NativeArray<T> value)
{
Debug.Assert(!m_IsDisposed);
#if ENABLE_UNITY_COLLECTIONS_CHECKS
ValidateAndUpdateAcquireCaller();
#endif
value = m_Value;
return GetAcquisitionDependency(isReadOnly);
}
/// <summary>
/// Schedule the release of the array's ownership after an asynchronous operation.
/// </summary>
/// <param name="releaseAccessDependency">The JobHandle that describes when work with the array will be complete.</param>
public void ReleaseAsync(JobHandle releaseAccessDependency)
{
Debug.Assert(!m_IsDisposed);
Debug.Assert(m_State != AcquisitionState.Unacquired, "There is no outstanding acquisition to release.");
switch (m_State)
{
case AcquisitionState.ReadOnly:
m_WriteAccessDependency = JobHandle.CombineDependencies(m_WriteAccessDependency, releaseAccessDependency);
break;
case AcquisitionState.ReadWrite:
m_ReadAccessDependency =
m_WriteAccessDependency = JobHandle.CombineDependencies(m_WriteAccessDependency, releaseAccessDependency);
break;
default:
throw new ArgumentOutOfRangeException(Enum.GetName(typeof(AcquisitionState), m_State));
}
m_State = AcquisitionState.Unacquired;
}
/// <summary>
/// Get the <see cref="NativeArray{T}"/> immediately.
/// This blocks the calling thread until the array is available.
/// <see cref="Release"/> must be called before any other calls to <see cref="AcquireAsync" />
/// or <see cref="Acquire" /> are made for this value.
/// </summary>
/// <remarks>
/// This method and its compliment <see cref="Release" /> are intended to be used for synchronous work
/// on the main thread.
/// </remarks>
public NativeArray<T> Acquire(bool isReadOnly)
{
Debug.Assert(!m_IsDisposed);
#if ENABLE_UNITY_COLLECTIONS_CHECKS
ValidateAndUpdateAcquireCaller();
#endif
GetAcquisitionDependency(isReadOnly)
.Complete();
return m_Value;
}
/// <summary>
/// Releases access to the array immediately.
/// Generally paired with the use of <see cref="Acquire"/>.
/// </summary>
/// <remarks>
/// This method can be called after <see cref="AcquireAsync"/> has been called but it will
/// block the calling thread until the array's access dependency is resolved. <see cref="ReleaseAsync" />
/// is typically the better option.
/// </remarks>
public void Release()
{
Debug.Assert(!m_IsDisposed);
Debug.Assert(m_State != AcquisitionState.Unacquired, "There is no outstanding acquisition to release.");
m_State = AcquisitionState.Unacquired;
m_ReadAccessDependency.Complete();
if (m_State == AcquisitionState.ReadWrite)
{
m_WriteAccessDependency.Complete();
}
}
private JobHandle GetAcquisitionDependency(bool isReadOnly)
{
if (isReadOnly)
{
m_State = AcquisitionState.ReadOnly;
return m_ReadAccessDependency;
}
m_State = AcquisitionState.ReadWrite;
return m_WriteAccessDependency;
}
#if ENABLE_UNITY_COLLECTIONS_CHECKS
private string m_AcquireCallerInfo;
private void ValidateAndUpdateAcquireCaller()
{
Debug.Assert(m_State == AcquisitionState.Unacquired, $"Release must be scheduled before scheduling acquisition again. Last ScheduleAcquire caller hasn't scheduled release yet. {m_AcquireCallerInfo}");
System.Diagnostics.StackFrame frame = new System.Diagnostics.StackFrame(2);
m_AcquireCallerInfo = $"{frame.GetMethod().Name} at {frame.GetFileName()}:{frame.GetFileLineNumber()}";
}
#endif
}
}
ExampleNativeArrayProviderSystem
public class ExampleNativeArrayProviderSystem : SystemBase
{
public NativeArrayAccessController<int> ExampleArray {get;}
protected override void OnCreate()
{
base.OnCreate();
Enabled = false;
ExampleArray = new NativeArrayAccessController<int>(500, NativeArrayOptions.UninitializedMemory);
}
protected override void OnDestroy()
{
ExampleArray.Dispose();
base.OnDestroy();
}
}
ExampleNativeArrayConsumerSystem
public class ExampleNativeArrayConsumerSystem : SystemBase
{
private ExampleNativeArrayProviderSystem m_ArrayProvider;
protected override void OnCreate()
{
base.OnCreate();
m_ArrayProvider = World.GetSystem<ExampleNativeArrayProviderSystem>();
}
protected override void OnUpdate()
{
Dependency = JobHandle.CombineDependencies(Dependency,
m_ArrayProvider.ExampleArray.AcquireAsync(false, out NativeArray<int> exampleArray));
Dependency = new ExampleJob()
{
ExampleArray = exampleArray
}.SheduleParallel(Dependency);
m_ArrayProvider.ExampleArray.ReleaseAsync(Dependency);
}
}