In the documentation of ISerializationCallbackReceiver.OnBeforeSerialize, there is a simple example that explains how to serialize a Dictionary. The documentation says that OnBeforeSerialize is called on a different thread. It also says that the code is running interleaved with Unity’s serializing code, (the callbacks are not deferred).
Is the example supposed to be thread-safe? By what unexplained means or underlying logic would it be? What prevents some other MonoBehavior from modifying the dictionary from its Update callback, while it is (based on what we can assume from the current documentation) randomly being read/written by the serialization thread?
There are many serialization/deserialization contexts:
Assembly reloads after a recompilation in the editor
Copy of a MonoBehaviour or ScriptableObject
Instantiation of an asset (ScriptableObject or prefab)
Editor to Play Mode
Play Mode to Editor
Drawing Inspector
Take the “Drawing Inspector” case for exemple, the serialization of the inspected object is likely to happen at a very specific moment in the execution order. In fact, I strongly doubt that Unity is triggering Update and Render calls on the MonoBehaviours while it is serializing the data it will need to draw the inspector. Could anyone detail on these unexplained conditions and synchronization logic, if there is any?
Another example of conditions/rules that I’m looking for, is with the “Copy” case. The duplication (Object.Instantiate) is called from the Main thread, so any serialization/deserialization would be forced to execute right now, thus preventing the concurrency between the main and serialization thread.
Finally, take for example the reload of an assembly after a recompilation. All objects are disabled, serialized, the assembly is reloaded, the objects recreated, deserialized, and enabled again. In this case, it seems safe to assume that although the individual objects may be serialized/deserialized from different threads, they will ALL be disabled before serialization begins, and ALL be deserialized before any is enabled.
When ISerializationCallbackReceiver.OnAfterDeserialize is called, Unity deserialization is supposed be complete on the object. If the deserialized object has references to other serializable objects, they have to be, at this point, at least, instantiated. That implies some kind of untold synchronization barrier. Even if all objects are deserialized on different threads, they must all be at least instantiated before any call to OnAfterDeserialize is made.
Any precisions regarding this issue would be greatly appreciated.
Thanks
P.S.: For reference, here’s the example copied from the documentation
using UnityEngine;
using System.Collections.Generic;
using System;
public class SerializationCallbackScript : MonoBehaviour, ISerializationCallbackReceiver
{
public List<int> _keys = new List<int> { 3, 4, 5};
public List<string> _values = new List<string> { "I", "Love", "Unity"};
//Unity doesn't know how to serialize a Dictionary
public Dictionary<int,string> _myDictionary = new Dictionary<int,string>();
public void OnBeforeSerialize()
{
_keys.Clear();
_values.Clear();
foreach(var kvp in _myDictionary)
{
_keys.Add(kvp.Key);
_values.Add(kvp.Value);
}
}
public void OnAfterDeserialize()
{
_myDictionary = new Dictionary<int,string>();
for (int i=0; i!= Math.Min(_keys.Count,_values.Count); i++)
_myDictionary.Add(_keys[i],_values[i]);
}
void OnGUI()
{
foreach(var kvp in _myDictionary)
GUILayout.Label("Key: "+kvp.Key+ " value: "+kvp.Value);
}
}
Something being thread-safe has got to do with how you restrict access to mutable data. One of the fundamental ways of doing this starts with encapsulation.
Firstly you hold all the actual data behind some interface (the public members of a type) as private fields. Then with the public interface (getters/setters/methods) you can control if something can access something at the moment, or would have to wait. The simplest way of doing that is with the ‘lock’ keyword, if 2 threads call lock on an object, the first one gets to do its work until it releases the lock, while the second thread has to wait. There is problems with this since you could fall into a dead-lock… there is of course more complex designs to compensate for such things.
Most of the .Net framework that we use is NOT thread safe. This is because .net assumes all their data types are fundamental objects that would otherwise be consumed into your program where you control the flow to it.
Also… the example on that page is NOT thread safe. Case in point the lists that are the serialization data are public. They are not encapsulated. There is nothing stopping another thread from accessing it. Of course, the example probably has them as public since unity always has example code with public fields… game scripts don’t always follow encapsulation rules. They assume that you know better than to modify some other objects fields if it should be at that time. Creating one giant monolith of a project.
Of course, this isn’t a big deal… as long as you don’t access them. Unity ensures to you that they won’t touch them (because they use reflection, even if they were private, they could), that’s the point of the design of ISerializationCallbackReceiver. They’re giving you an event to react to that will run after the serialization/deserialization, but before any scheduled Unity events (like Awake/Start/Update/etc).
Because it’s happening at this weird time, you need to be careful what you do. It’s like with the Awake function, if you access another script, it may not have had its Awake function called yet… and therefore isn’t in a state to be accessed quite yet (if there’s critical code in Awake to be called). But moreso in this case than with Awake, in that you can’t even access the Unity API.
You should ONLY be working with the local scope of the object. Perform whatever actions you have to, to serialize or deserialize (unwrap a dictionary, create a dictionary).
furthermore, this line:
This is saying that your specific object has completed serialization, but its still got other stuff to serialize/deserialize. Just because the Unity API throws exceptions if you attempt to access it off the main thread, you still can access mono properties of some other object. For example this:
public class MyCustomScript : MonoBehaviour, ISerializationCallbackReceiver
{
public MyOtherScript someRef;
void ISerializationCallbackReceiver.OnBeforeSerialize()
{
}
void ISerializationCallbackReceiver.OnAfterDeserialize()
{
someRef.SomeData = Vector3.zero;
}
}
public MyOtherScript : MonoBehaviour
{
public Vector3 SomeData;
}
In this situation, the property ‘someRef’ is dealt with by unity, and not by you. Because OnAfterDeserialize occurs after all the serialized fields of the script have been set during initialization. That ‘MyOtherScript’ is technically available. And because that Vector3 on it is just some data member, there’s nothing stopping you from accessing it.
BUT… MyOtherScript may or may not have been deserialized yet. If you go and change that data it might put the state of that object into something the deserializer doesn’t expect. You could lose data. There’s no way to know the ramifications… because the order of the operation is unpredictable.
So serialization doesn’t necessarily happen off the main thread, it just can. Note:
It may run on the main thread. You just don’t have a way to know one way or the other. What can be said though is that the main thread will not continue its operation until the serialization has completed. If it allowed Update to fire while an object was in the middle of deserialization, your game could produce some errors.
An example could be how when you call ‘SerializedObject.Update’, what that actually is doing is reflecting all the serializable fields off of that object that SerializedObject deals with… and if it implements ISerializationCallbackReceiver, it will call ‘OnBeforeSerialize’ before actually doing that work.
The ‘SerializedObject.Update’ method will block for the duration of this.
Again, it blocks until the serialization and deserialization is done.
And yes… this is somewhere where you should be concerned about doing any heavy lifting during the event. You could cause that block to take a long time and hurt framerate when you are calling Object.Instatiate during gameplay.
Yes, it is safe to assume that. The order of the main thread is maintained.
All Unity is referring to in that documentation as that inter communication during a serialization is a big NO NO. Because there’s no way they can maintain thread safety for you.
Yeah, the .Net/Mono object has been constructed. This is part of the reason why unity suggests never using the ‘Constructor’ of a MonoBehaviour. The problems with ISerializationCallbackReceiver are even worse in that case…
Instantiated, in the context of “unity” though doesn’t mean constructed. Because unity does so much leg work in ‘instantiating’ an object:
constructing any of the C++ unity engine representation of the object if its a UnityEngine.Object
constructing the .net/mono objects for the mono side of things
setting all the fields based on serialized representation
scheduling for Awake if needed
The idea of ‘Instantiation’ is ALL of that. So to unity, it’s not instantiated until its all done. The ISerializationCallbackReceiver events are in the midst of all that… so it’s constructed in memory (it has to be), but it’s not instantiated to its fullest. And therefore certain ramifications must be considered.
And because during editor time, those things can happen at all sorts of weird times, as you yourself outlined. Just don’t expect to intercommunicate at all.
Stick to JUST playing with data that is local to that object.
During OnAfterDeserialize do NOT modify any field that is serialized, even if its local.
During OnBeforeSerialize, you can modify any local field that is serialized, as that’s how you hand over the serialized representation for unity.
Ran into this issue while crudely putting together a unity serializable dictionary. Rapid (de)serialization messed around with vectors stored inside the dict and I got messed up data.
I strongly recommend using:
Or include locks while writing custom (de)serialization.