ISerializationCallbackReceiver threading concerns

I was needing a serializeable dictionary, and after searching I have found this thread

In that thread there are links to other threads of people who had weird issues with ISerializationCallbackReceiver that can be threading issues.

The documents

warn about thread problems and states “You’re strongly recommended to only use this interface for the example use case mentioned above: serializing something yourself that Unity doesn’t know how to.”
Then below they gave a example script of a serializeable dictionary, which leads me to believe that there should be no issues with doing things similar to how they are doing it. (However, they are inheriting MonoBehaviour, which I do not want to do. Is that very important?)

In the thread at the top of the post where the user vexe posted a script of a serializeable dictionary, while it may or may not work, it looks far to scary for me and the user lordofduct posted an alternative which is much easier for me to digest, and is also done in a similar way to the unity document above (except lordofduct is using arrays, which I am not sure if that is needed or not? I replaced it with lists so I can use Clear to avoid garbage as well as changed a few other things in the OnBeforeDeserialize and OnAfterDeserialize).

I end up with a script like this
Click for code

using System;
using UnityEngine;
using System.Collections.Generic;

namespace Custom.Collections
{
    [Serializable]
    public class SerializeableDictionary<TKey, TValue> : DrawableDictionary, IDictionary<TKey, TValue>, ISerializationCallbackReceiver
    {
        [SerializeField] List<TKey> keys = new List<TKey>();
        [SerializeField] List<TValue> values = new List<TValue>();
        [NonSerialized] Dictionary<TKey, TValue> dictionary = new Dictionary<TKey, TValue>();

        void ISerializationCallbackReceiver.OnBeforeSerialize()
        {
            if(dictionary == null) return;

            CheckForNullList();
            keys.Clear();
            values.Clear();
    
            Dictionary<TKey, TValue>.Enumerator enumerator = dictionary.GetEnumerator();
            while(enumerator.MoveNext())
            {
                keys.Add(enumerator.Current.Key);
                values.Add(enumerator.Current.Value);
            }
        }

        void ISerializationCallbackReceiver.OnAfterDeserialize()
        {
            CheckForNullList();
            if(dictionary == null)
            {
                dictionary = new Dictionary<TKey, TValue>();
            }else{
                dictionary.Clear();
            }
 
            if(keys.Count != values.Count)
            {
                int difference = keys.Count - values.Count;
                for(int i = 0; i < difference; i++)
                {
                    values.Add(default(TValue));
                }
            }
 
            for(int i = 0; i < keys.Count; i++)
            {
                dictionary.Add(keys[i], values[i]);
            }

            keys.Clear();
            values.Clear();
        }

        void CheckForNullList()
        {
            if(keys.IsNull()) keys = new List<TKey>();
            if(values.IsNull()) values = new List<TValue>();
        }

        #region IDictionary Interface
        public int Count
        {
            get { return (dictionary != null) ? dictionary.Count : 0; }
        }

        public void Add(TKey key, TValue value)
        {
            if (dictionary == null) dictionary = new Dictionary<TKey, TValue>();
            dictionary.Add(key, value);
        }

        public bool ContainsKey(TKey key)
        {
            if (dictionary == null) return false;
            return dictionary.ContainsKey(key);
        }

        public ICollection<TKey> Keys
        {
            get
            {
                if (dictionary == null) dictionary = new Dictionary<TKey, TValue>();
                return dictionary.Keys;
            }
        }

        public bool Remove(TKey key)
        {
            if (dictionary == null) return false;
            return dictionary.Remove(key);
        }

        public bool TryGetValue(TKey key, out TValue value)
        {
            if(dictionary == null)
            {
                value = default(TValue);
                return false;
            }
            return dictionary.TryGetValue(key, out value);
        }

        public ICollection<TValue> Values
        {
            get
            {
                if (dictionary == null) dictionary = new Dictionary<TKey, TValue>();
                return dictionary.Values;
            }
        }

        public TValue this[TKey key]
        {
            get
            {
                if (dictionary == null) throw new KeyNotFoundException();
                return dictionary[key];
            }
            set
            {
                if (dictionary == null) dictionary = new Dictionary<TKey, TValue>();
                dictionary[key] = value;
            }
        }

        public void Clear()
        {
            if (dictionary != null) dictionary.Clear();
        }

        void ICollection<KeyValuePair<TKey, TValue>>.Add(KeyValuePair<TKey, TValue> item)
        {
            if (dictionary == null) dictionary = new Dictionary<TKey, TValue>();
            (dictionary as ICollection<KeyValuePair<TKey, TValue>>).Add(item);
        }

        bool ICollection<KeyValuePair<TKey, TValue>>.Contains(KeyValuePair<TKey, TValue> item)
        {
            if (dictionary == null) return false;
            return (dictionary as ICollection<KeyValuePair<TKey, TValue>>).Contains(item);
        }

        void ICollection<KeyValuePair<TKey, TValue>>.CopyTo(KeyValuePair<TKey, TValue>[] array, int arrayIndex)
        {
            if (dictionary == null) return;
            (dictionary as ICollection<KeyValuePair<TKey, TValue>>).CopyTo(array, arrayIndex);
        }

        bool ICollection<KeyValuePair<TKey, TValue>>.Remove(KeyValuePair<TKey, TValue> item)
        {
            if (dictionary == null) return false;
            return (dictionary as ICollection<KeyValuePair<TKey, TValue>>).Remove(item);
        }

        bool ICollection<KeyValuePair<TKey, TValue>>.IsReadOnly
        {
            get { return false; }
        }

        public Dictionary<TKey, TValue>.Enumerator GetEnumerator()
        {
            if (dictionary == null) return default(Dictionary<TKey, TValue>.Enumerator);
            return dictionary.GetEnumerator();
        }

        System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
        {
            if (dictionary == null) return System.Linq.Enumerable.Empty<KeyValuePair<TKey, TValue>>().GetEnumerator();
            return dictionary.GetEnumerator();
        }

        IEnumerator<KeyValuePair<TKey, TValue>> IEnumerable<KeyValuePair<TKey, TValue>>.GetEnumerator()
        {
            if (dictionary == null) return System.Linq.Enumerable.Empty<KeyValuePair<TKey, TValue>>().GetEnumerator();
            return dictionary.GetEnumerator();
        }
        #endregion
    }

    public abstract class DrawableDictionary{}
}

So my concern is, is this all really safe?
We can just focus on the unity docs. We see that in
OnBeforeSerialize we are clearing the lists, taking whats currently in our dictionary and putting it in the lists.
Since OnBeforeSerialize and OnAfterSerialize are in a different thread, is it not possible that as the dictionary elements are being added to the lists, somewhere else there is a method in Update that has decided to clear the dictionary, but the serializer already stored lots of what was already in the dictionary and on OnAfterSerialize it will add it back. Now when I thought I cleared the dictionary, it actually still has stuff in it.

The docs say “Another caveat is that serialization happens more often than you might think. When a MonoBehaviour gets cloned through Instantiate() at runtime, it gets deserialized and serialized.”. Is that saying that at runtime, the only time serialization happens is when we call Instantiate()? Or is it saying that is just one of many others…
Is it that there would be a problem, however since serialization only happens at things like Instantiate, its going to maybe run the serialization in the main thread or something?
Is the only concern for when doing editor stuff?

@lordofduct you mentioned in this post Finally, a serializable dictionary for Unity! (extracted from System.Collections.Generic) - Unity Engine - Unity Discussions “…Oh, and I have ran into these same exceptions as well. So in the cases where it can’t be avoided, you have to use it and you don’t have full control of everything…”
Are you basically saying we will just have to hope for the best? Also, Why did you use arrays and not lists?

Any help is appreciated!

No, inheriting MonoBehaviour is not important when implement ISerializationCallbackReceiver.

the callbacks don’t occur on a thread that is considered thread safe to access the unity API. BUT that doeesn’t mean that it might happen parrallel to game code running. Serialization still blocks the main thread. The threading comes in so that when deserializing multiple gameobjects, 1 object isn’t waiting on another to deserialize when they’re unrelated.

Or when loading a scene asynchronously. In which case the objects being loaded won’t have Start/Update called on them until after all of them have been loaded.

Instantiate is one time it might happen.

It might happen when modified in the editor. It might happen when touched in the editor. It might happen the first time the prefab of an object is loaded into memory. There’s many things that might cause it to be called… and of course some are editor specific.

I’m not 100% if it might run on main thread or not. But that doesn’t matter. Since you can not be sure to know if it’s on the main thread or not, it’s in consequential to you.

If you had a job to write your name on a piece of paper. And you elected to be woken up in the middle of the night to write your name on the piece of paper. The possibility that it MIGHT be 3:34am when you get woken up is inconsequential to that.

To be brutally I do not know what that specific comment was a response to. Reading it I get the feel someone said something that I was responding to that I didn’t bother to quote, and may have been edited out or deleted.

From what I can tell I’m talking about using ISerializationCallbackReceiver, and the fact that some people may get weird exceptions when using them (and that I’ve received them as well), doesn’t mean you can just avoid using it. And then go on to list in a practical sense what I do to avoid those errors.

The biggest issue here is that during the ISerializationCallbackReceiver callbacks, you’re modifying objects in the middle of the serialization process. The serialization process is NOT documented for us, and can change at any point on the whim of Unity engineers. So your best bet is to just treat the data as volatile, and explicitly inform the serializer your expectations (flagging what is serializable and not with attributes explicitly).

First, and my primary influence when I use certain types. I use a type that best suits my needs at that moment, not necessarily avoids GarbageCollection. Both Lists and Arrays generate garbage. So avoiding garbage by using one over the other doesn’t necessarily avoid garbage collection.

I’m describing a collection of items that are static in length for the supposed representation of their life (these are SUPPOSED to just be fields for representing the state of the object when serialized… not be active fields).

Note, a List is really just a wrapper for an array. But it doesn’t know how long to make that array. So it just creates an array at some interval (usually a prime number), as you near the size of the array, the List will create a new array of the correct size.

So when you create your keys and values Lists, and then enumerate the dict and add them to the lists. You’re actually generating more garbage as more and more items are added. If you want to continue using lists, you may want to pass in the size of the list you expect it to be. This will tell the List to create its array at some set size (reflected by the Capacity property).

Furthermore, garbage generated at Editor time is superfluous to me. I honestly don’t care. I won’t write optimized code just to eek that tiny bit out at editor time. Since the majority of serialization callbacks are editor callbacks, I use that in any optimization assumption.

I personally don’t like maintaining state information for the life of the object either. This is actually why I complain in that thread that they don’t give us a state object to update like .Net serialization does… but rather expects us to make the state information part of the class itself. Ugh. For me to maintain that ‘List’ for the life of the object means I’m taking up unnecessary memory. My dictionary has grown to take up 2x the memory… sure, it might be insignificant if you wanted to compare it to the GC cost that might incur. But I go back to my initial point… I don’t initially pick based on optimization, but on conveying intent. And my intent is that this is state information for passing to the serializer… not something that persists.

So with these assumptions in place, in the final build the only times it runs aren’t that many.

  1. When that specific object is first created. This is only a call to OnAfterDeserialize, and unity will have initialized our array/list anyways, same work either way (well technically more for List, but a negligible more).

  2. When the object is cloned. In this scenario there’s 2 objects involved. The existing one has OnBeforeSerialize called to create a state, and OnAfterDeserialize on the new object so to copy that state. During OnBefore this is the only time we might actually create new garbage, and OnAfter has the same scenario as (1).

I mean sure, there’s some potential to save some possible memory allocations in here. I never really ran any optimization tests on it. But in the end, since in a final build, since any significant difference in cost only occurs when cloning, an action that doesn’t happen frequently anyways… and generates a whole lot of garbage anyhow already. I consider it inconsequential in total.

Garbage piling up is something that is painful when it’s happening constantly.

Creating those strings every frame, and throwing them out.
Foreaching every frame.
Boxing every frame.

That sort of stuff.

But if my load screen creates a bunch of garbage. Well yeah… of course it is! It’s a load screen!

I mean, what else? Something like FX or temporary objects like bullets that get created frequently and destroyed? Again, a lot of garbage is going to be created here anyways (unless pooled, in which case, the disparity again disapears). And I’m probably not going to have a serialized dictionary on that bullet or FX.

Honestly, I’m not going to have many serialized dictionaries.

I have that class in my framework, yet I honestly can’t think of ANY time I’ve actually used it. I think I wrote it specifically for vexe.

So the general feel Im getting is that the times when objects are being deserialized during runtime will not mess things up. Seeing how instantiated objects need to be accessible right after the instantiation, it wouldnt make sense for them to not block the main thread.
I was wondering if being inherited to monobehaviour had any big affect because of how unity handles unity objects differently from system object. Maybe it was handling threading better or something.

Also, I mainly wanted to know why you used an array instead of a list in case there was some special must have reason or something. I saw you avoid foreach and it made me think you were trying to avoid garbage, and then I saw you creating arrays which threw me off ^^.

So to recap, the reason this person here

was getting threading issues was due to them inheriting the hashset and marking it as serializeable, which caused a race condition between .net and unity serializer? If we instead make the hashset private and nonserializeable within the class itself then we are in the clear?

Edit - I just noticed on this unity blog here

it states “…(Serialization happening as part of loading a scene happens on a loading thread. Serialization happening as part of you invoking Instantiate() from script happens on the main thread)…”
So I guess all is safe as long as Update and stuff doesnt run as the loading thread is running.
However Im still curious as to why people had issues when inheriting from the hashshets / dictionaries. Is it really due to .net serializer?

Edit 2 - heh, I also just realized theres almost an exact copy of my thread that was made a while ago about this issue… you are even there too lol

Weird, when googling “unity iserializationcallbackreceiver threading concerns” or other variations, only my thread shows…

It seems the serializable hashset that inherits hashset would throw an indexoutofrangeexception
at the start of play mode if there were values already in the hashset during edit time (we set it in Reset). Even if you dont run Setup in Awake, the error still occurs.
Not only that, but if you caused a assembly reload (such as editing a script) after you added the component, it would then spam indexoutofrangeexception. Whats weird is, at this point since it erroring out, the hashset is empty and at the next assembly reload the errors will go away and the hashset will remain empty.
Whats interesting is that if I put a debug log in the OnAfterDeserialize before where the error is being called at the UnionWith(elements); part, that debug only runs once and then the errors take over. It seems the errors are being caused by something internal at this point as OnAfterDeserialize isnt being called anymore.

If there were no values during edit time, then no errors seem to be thrown.

If my hashset happens to be on a gameobject I am instantiating and it had default values, each instantiation would throw the indexoutofrangeexception.

The serializable hashset that does not inherit hashset seems to throw no errors either way.

Here was the test code
Click for code

using System;
using UnityEngine;
using System.Collections.Generic;

public class TestSerialize : MonoBehaviour
{
    [Serializable]
    public class H : SerializableHashSet<int>{}
    public H h = new H();

    //[Serializable]
    //public class H2 : SerializableHashSetNoInherit<int>{}
    //public H2 h2 = new H2();

    void Reset()
    {
        Setup();
    }

    void Awake()
    {
        //Setup();
    }

    void Update()
    {
        h.Add(200000 + Time.frameCount);
        //h2.hashSet.Add(200000 + Time.frameCount);
    }

    void Setup()
    {
        h = new H();
        //h2 = new H2();

        for(int i = 0; i < 1000; i++)
        {
            h.Add(i);
            //h2.hashSet.Add(i);
        }
    }
}

[Serializable]
public class SerializableHashSet<T> : HashSet<T>, ISerializationCallbackReceiver
{
    [SerializeField] private List<T> elements = new List<T>();

    public void OnBeforeSerialize()
    {
        elements.Clear();
        elements.AddRange(this);
    }

    public void OnAfterDeserialize()
    {
        Clear();
        UnionWith(elements);
    }
}

[Serializable]
public class SerializableHashSetNoInherit<T> : ISerializationCallbackReceiver
{
    [SerializeField] private List<T> elements = new List<T>();
    [NonSerialized] public HashSet<T> hashSet = new HashSet<T>();

    public void OnBeforeSerialize()
    {
        elements.Clear();
        elements.AddRange(hashSet);
    }

    public void OnAfterDeserialize()
    {
        if(hashSet == null) hashSet = new HashSet<T>();
        else hashSet.Clear();

        hashSet.UnionWith(elements);
    }
}

The code was based on this thread complaining about such errors

However, that guy said
“…this is constantly throwing “index out of range” when going into play mode:… I only use one instance of this class in my whole project, which is instantiated in Awake(), and used in Update()…”
I only got spammed in the editor or once in playmode, so I am not sure what he was doing to get spammed.

Im still a bit confused as to why this all is, but I guess it might just be putting Serializable on the Hashset. Its like when the assembly reloads, unity cant save whats in the hashset class, but at the same time since its marked serializable, unity cant leave it null.
Maybe unity is calling the hashsets constructor on serialize and causing issues since the constructor gets called before serialization.

So, Unity may have fixed this.

But I witnessed in the past that the serializer would touch private fields if they weren’t flagged as System.NonSerialized. This included those fields in an inherited class.

This could cause this issue, because it may have touched the array after the constructor created it, messing up the hashset.