IDisposable is becoming more and more prevalent in Unity. Previously, it was typically only used for I/O types like FileStream. Now it’s used for in-memory types like NativeArray<T> to avoid the garbage collector. Needing to call Dispose manually means we’re explicitly managing memory, just like we’d do in lower-level languages like C++. That comes with some challenges, especially with shared ownership, which we’ll deal with today.

Leaks

So-called “manual” memory management is notoriously error-prone. C and C++ programmers frequently forget to deallocate memory, resulting in a “leak.” Repeated leaking often leads to poor performance or a crash. This is what we get with IDisposable where we must write code to call Dispose ourselves.

Let’s try not calling Dispose at all, such as in this script:

using Unity.Collections;
using UnityEngine;
 
class TestScript : MonoBehaviour
{
    void Start()
    {
        NativeArray<int> array = new NativeArray<int>(1, Allocator.Temp);
    }
}

Pressing play in the Unity 2019.2.15f1 editor results in no log messages alerting us to the issue. The bug will likely go unnoticed.

Next, let’s try the same script with Allocator.TempJob instead. This time, Unity spams the following two warning messages:

Internal: JobTempAlloc has allocations that are more than 4 frames old - this is not allowed and likely a leak
To Debug, enable the define: TLA_DEBUG_STACK_LEAK in ThreadsafeLinearAllocator.cpp. This will output the callstacks of the leaked allocations

Even fixing the problem in the code won’t make these warnings go away, so we must restart the editor.

Now let’s try Allocator.Persistent. This time we don’t get any warning messages, but after a few seconds Unity prints the following error:

A Native Collection has not been disposed, resulting in a memory leak. Enable Full StackTraces to get more details.

Fixing the code and running again does not result in the same error message. So in two of the three cases we are at least notified by Unity about the problem.

Double-Dispose

The next challenge is to avoid calling Dispose more than once, such as with this script:

using Unity.Collections;
using UnityEngine;
 
class TestScript : MonoBehaviour
{
    void Start()
    {
        NativeArray<int> array = new NativeArray<int>(1, Allocator.Persistent);
        array.Dispose();
        array.Dispose();
    }
}

The second Dispose call throws this exception:

InvalidOperationException: The NativeArray has been deallocated, it is not allowed to access it
Unity.Collections.LowLevel.Unsafe.AtomicSafetyHandle.CheckDeallocateAndThrow (Unity.Collections.LowLevel.Unsafe.AtomicSafetyHandle handle) <0x14a91d9e0 + 0x00052> in <ad9beda1ab3247eeb4730364a1ba4085>:0
Unity.Collections.LowLevel.Unsafe.DisposeSentinel.Dispose (Unity.Collections.LowLevel.Unsafe.AtomicSafetyHandle& safety, Unity.Collections.LowLevel.Unsafe.DisposeSentinel& sentinel) (at /Users/builduser/buildslave/unity/build/Runtime/Export/NativeArray/DisposeSentinel.cs:73)
Unity.Collections.NativeArray`1[T].Dispose () (at /Users/builduser/buildslave/unity/build/Runtime/Export/NativeArray/NativeArray.cs:161)
TestScript.Start () (at Assets/TestScript.cs:10)

This case is reliably detected by Unity and surfaced to us so that we can fix the problem.

Sharing

Calling Dispose exactly once can be challenging. It is easy to forget to call it at all and sometimes difficult to write code that won’t call it more than once. It’s mostly the latter case that we’ll address today.

“Sharing” an IDisposable among many parts of the code raises the question of “ownership.” Which area of the code is responsible for calling Dispose? If a NativeArray is passed to a function, should that function call Dispose? If it’s passed to a struct or class constructor and stored in a field, should the methods of that struct or class be responsible for calling Dispose later on? What if it’s passed to 10 different structs (e.g. jobs) that all use it? These are just some examples of the difficult problems that arise when there is shared ownership.

Help

Various approaches have been derived over the years to address these problems. They range from naming conventions to garbage collectors, but today we’ll focus on a technique called “reference counting.” The core idea is to use an integer to store the number of “owners” of an object. Each time some area of the code wants to start referencing the IDisposable, the integer is incremented. When it’s done, the integer is decremented. When the integer reaches zero, Dispose is called.

This can be done ad-hoc, but today we’ll build and use a new type that helps us do this more easily: SharedDisposable<T>. It is a struct that contains an IDisposable of type T and a pointer to the reference count integer, allocated in native memory by Unity. The reference count is initially one and is incremented when users call Ref, which returns a copy of the SharedDisposable<T>. The SharedDisposable<T> itself is an IDisposable for three reasons:

  • It needs to decrement the reference count
  • It needs to free the reference count integer just like a NativeArray frees its array
  • It needs to call Dispose on the shared IDisposable

Here are the basics of how to use it:

// Create an IDisposable
NativeArray<int> array = new NativeArray<int>(1, Allocator.Temp);
 
// Prepare for sharing. Ref count = 1.
SharedDisposable<NativeArray<int>> shared = array.Share(Allocator.Temp);
 
// Share. Ref count = 2.
SharedDisposable<NativeArray<int>> shared2 = shared.Ref();
 
// Use a shared reference
print("Array len: " + shared2.Value.Length);
 
// Release a reference. Ref count = 1.
shared2.Dispose();
 
// Release the last reference. Ref count = 0.
// The NativeArray<int> is disposed.
shared.Dispose();

using blocks make this even more convenient because there's no longer any need to call Dispose. This includes times where an exception is thrown, as using blocks automatically include a try-catch-finally. Here's how this looks:

using (SharedDisposable<NativeArray<int>> used = array.Share(Allocator.Temp))
{
    using (SharedDisposable<NativeArray<int>> used2 = shared.Ref())
    {
        print("Array len: " + used2.Value.Length);
    }
}

Notice above that array.Share is used to create the SharedDisposable. This is allowed due to an extension method on IDisposable, but the longer manual version also works:

SharedDisposable<NativeArray<int>> shared = new SharedDisposable<NativeArray<int>>(
    array,
    Allocator.Temp);

The full MIT-licensed source code is available as part of the NativeCollections GitHub repo:

Conclusion

Manually managing memory can be a challenge, even with the present level of help from Unity. Calling Dispose exactly once can be tricky, especially writing code to do this when an IDisposable is shared among many owners. Reference counting is one way to make sharing ownership easier, especially with help from SharedDisposable<T>. It's not appropriate for every use case and still requires vigilance to avoid leaks and double Dispose calls, but it can be a handy tool in some cases.