Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Unity3d and Threading

I want to do some stuff in my app that is time consuming but which does not have an impact on the actual game. Therefore I am trying to use a thread to run that stuff separately. Here is how I create my thread:

Thread thread = new Thread(() => WriteCheckpointTextFile (myFileName, checkpoint));
thread.Start();

Here is the code I am running in WriteCheckpointTextFile:

void WriteCheckpointTextFile (string fileName, Checkpoint checkpoint)
{
            string checkpointToText = "";
            checkpointToText += CheckpointDataNames.nextMarker + Environment.NewLine;
            checkpointToText += checkpoint.nextMarker + Environment.NewLine;
            checkpointToText += "%" + Environment.NewLine;

            checkpointToText += CheckpointDataNames.difficulty + Environment.NewLine;
            checkpointToText += checkpoint.difficulty.ToString () + Environment.NewLine;
            checkpointToText += "%" + Environment.NewLine;

            checkpointToText += CheckpointDataNames.musicObject + Environment.NewLine;
            checkpointToText += checkpoint.musicObject + Environment.NewLine;
            checkpointToText += "%" + Environment.NewLine;

            checkpointToText += CheckpointDataNames.gameTimePassed + Environment.NewLine;
            checkpointToText += checkpoint.gameTimePassed.ToString () + Environment.NewLine;
            checkpointToText += "%" + Environment.NewLine;


            checkpointToText += CheckpointDataNames.storedStats + Environment.NewLine;
            checkpointToText += EndRaceDataString (checkpoint.storedStats);
            checkpointToText += "%" + Environment.NewLine;

            checkpointToText += CheckpointDataNames.pursuers + Environment.NewLine;
            foreach (PursuerData p in checkpoint.pursuers) {
                    checkpointToText += p.pursuerName + "," + p.pursuerNextMarker + "," + p.pursuerPosition.x.ToString () + "," + p.pursuerPosition.y.ToString () + "," + p.pursuerPosition.z.ToString () + Environment.NewLine;

            }
            checkpointToText += "%" + Environment.NewLine;
            #if !UNITY_WEBPLAYER

            System.IO.File.WriteAllText (fileName, checkpointToText);

            #endif
            Debug.Log("Finished writing text to file");
}

Now for some reason that last debug doesn't always get printed and my file is not always written. What could be causing this?

like image 258
kUr4m4 Avatar asked Oct 21 '22 09:10

kUr4m4


1 Answers

The issue is that your collection checkpoint.storedStats is being modified from another thread (likely you're main thread) while you're iterating on it in your background thread. This throws an InvalidOperationException. Because your code is executing on a background thread, the exception is being silently thrown, unhandled, and ignored by the Unity player/engine.

Unfortunately, this means you'll have to synchronize or restrict access and changes to your checkpoint.storedStats collection so that only 1 thread may make modifications to it and while doing so, other threads must not be iterating on it.

It's difficult to comment on the best or simplest method for you to implement because I don't know how you use the storedStats collection throughout your application. One possible, and fairly typical solution would be to wrap all places access the collection with a lock statement. This will ensure that only 1 thread enters the locked block(s) at any given time. Then when accessing the storedStats for reading (not writing) you can provide a method on your GameObject's script to lock the collection, make a copy of it, and return that copy. Since it's a copy, you can safely iterate on it:

public class Checkpoint : MonoBehaviour
{
    //used for locking
    private readonly object ThreadLocker = new Object();

    private List<Stat> storedStats = new List<Stat>();

    public List<State> GetStoredStatsCopy()
    {
        lock(ThreadLocker)
        {
            //makes a copy/snapshot of the list
            return new List<Stat>(storedStats);
        }
    }

    //sample code that changes storedStats
    public void AddStat(Stat newStat)
    {
        lock(ThreadLocker)
        {
            storedStats.Add(newStat);
        }
    }
}

Notice that the Checkpoint script now makes storedStats private. I've also added some sample method that modifies or writes to that underlying storedStats. So all access to that storedStats is controlled and threads are synchronized. This way your background thread can't be iterating on the storedStats collection (because it has a copy, and the creation of the copy is controlled/thread-safe) and any writes to the collection cannot occur at the same time as another thread is accessing it.

EDIT: And with the above API change, your calling code might look like:

checkpointToText += EndRaceDataString (checkpoint.GetStoredStatsCopy());
like image 113
Chris Sinclair Avatar answered Oct 23 '22 11:10

Chris Sinclair