Sharing IDisposables
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 sharedIDisposable
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.
#1 by Alexander Yevsyeyev on December 16th, 2019 ·
Good day Jackson,
Thank you for the post. A small mistake you did saying that “The bug will likely go unnoticed” with Temp allocation. It is not a bug, Temp memory gets cleared automatically. It was introduced in 2019.1 or 2 to allow allocating Temp memory in Burstable jobs to support automatic deallocation without relying on Dispose reference.
Best regards,
Alexander.
#2 by jackson on December 16th, 2019 ·
I suppose it’s debatable whether this is a bug or not. You’re right that it’ll be automatically deallocated so there’s no need to call
Dispose
. However, there are reasons why you might want to anyhow. Here are some I can think of off the top of my head:NativeArray
after the implicit deallocation will still throw an exceptionTemp
allocators#3 by Todd Ogrin on December 16th, 2019 ·
What we do in Microsoft DCOM echoes through eternity. ;)
#4 by Chris Ochs on December 17th, 2019 ·
Ya Temp is annoying because they won’t tell us what the scope actually is. We know what it’s not, it’s not code block based. So likely time/frame based.
Unity has a lot of their own code that never explicitly disposes Temp allocations. My bet is a lot of Unity’s own developers don’t know more then we do, which isn’t really good.
It could be that disposing Temp just marks it, it doesn’t actually get disposed immediately. Probably not, but I think Unity needs to document it better whatever the actual behavior is.
#5 by Marcelo Oliveira on December 31st, 2019 ·
As per “C# Standards”
“To help ensure that resources are always cleaned up appropriately, a Dispose method should be callable multiple times without throwing an exception.”
https://docs.microsoft.com/en-us/dotnet/standard/garbage-collection/implementing-dispose
Mostly C# programmers will rely on this, so I think we should treat the exception ou double dispose a bug.
#6 by jackson on January 2nd, 2020 ·
It seems that Unity disagrees with Microsoft’s advice since they throw an exception on double
Dispose
.SharedDisposable
does not throw an exception on doubleDispose
because it only callsDispose
on the underlyingIDisposable
exactly once, it’s more in line with Microsoft’s advice. Depending on whether you prefer the exception, this may or may not be a good thing.#7 by Baggers on January 14th, 2020 ·
I’d love to see a follow up to this talking about Dipose(JobHandle). I’ve really enjoyed using this to delay disposal until after jobs complete. It’s very handy in combination with JobHandle.CombineDependencies(NativeArray) as then you can schedule a native collection with TempJob allocation to be automatically disposed once the jobs have all completed.
Great article as always, thanks for writing so many!
#8 by jackson on January 14th, 2020 ·
I’m glad you liked the article. Thanks for the idea for another one about
Dipose(JobHandle)
!