9
\$\begingroup\$

I had a coroutine in Unity3D that downloaded a zip from a server, extracted it to the persistent data path, and loaded its contents into memory. The flow looked something like this:

IEnumerator LongCoroutine()
{
    yield return StartCoroutine(DownloadZip());
    ExtractZip();
    yield return StartCoroutine(LoadZipContent());
}

But the ExtractZip() method (which uses the DotNetZip library), is synchronous, takes too long, and leaves me no place to yield during the process.

This resulted in the application being killed (on mobile devices) whenever I tried to extract a large zip, which I assume is due to the main thread becoming unresponsive for too long.

Is this something that mobile OS are known to do (kill the app if a frame takes too long)?

So, I assumed that extracting the zip on a separate thread might solve the problem, and it seems to have worked. I created this class:

public class ThreadedAction
{
    public ThreadedAction(Action action)
    {
        var thread = new Thread(() => {
            if(action != null)
                action();
            _isDone = true;
        });
        thread.Start();
    }

    public IEnumerator WaitForComplete()
    {
        while (!_isDone)
            yield return null;
    }

    private bool _isDone = false;
}

And I use it like this:

IEnumerator LongCoroutine()
{
    yield return StartCoroutine(DownloadZip());
    var extractAction = new ThreadedAction(ExtractZip);
    yield return StartCoroutine(extractAction.WaitForComplete());
    yield return StartCoroutine(LoadZipContent());
}

But I'm still not sure if this is the best way to implement it, or if I need to lock _isDone (not too used to multithreading).

Can something go wrong with this / am I missing something?

\$\endgroup\$

4 Answers 4

4
\$\begingroup\$

This is a really elegant solution for wrapping up multithreaded tasks in a coroutine, well done :)

Mixing coroutines and threads is perfectly safe, provided you correctly lock access to resources shared between your main thread (which the coroutine executes on) and the worker thread(s) you create. You shouldn't need to lock _isDone in any way, since it's only ever written to by the worker thread and there's no intermediate state that could cause the main thread to misbehave.

Where you do need to look out for potential problems, is if any resources are written to by ExtractZip and are either

  1. simultaneously written to by a function being called from your main thread or
  2. being read by a function on the main thread and expected to be in a safe state before ExtractZip completes.

In this particular case, my worry would be that if you don't check that you're not trying to download the same file to the same location twice, you could have two threads simultaneously running ExtractZip that interfere with one another.

\$\endgroup\$
2
\$\begingroup\$

There is a free solution for this on the Asset Store:

Thread Ninja

With it you can switch between main thread & background thread easily, which likes:

void Awake() {
    this.StartCoroutineAsync(AsyncCouroutine());
}

IEnumerator AsyncCoroutine() {
    // won't block
    Thread.Sleep(10000);

    yield return Ninja.JumpToUnity;

    // we're now on Unity's main thread
    var time = Time.time;

    yield return Ninja.JumpBack;

    // now on background thread again
    // ...
\$\endgroup\$
0
-1
\$\begingroup\$

Well, I'm not a major expert either, but I think in your 2nd example, the WaitForComplete() function will still block the calling thread until the ThreadedAction thread completes. What you can do is pass a callback function to your zip processing Thread, and have it call that function when it's done. That way your main thread starts the zip thread and then continues on doing his stuff (updating GUI, what have you), and then the call back function will set something in the main thread when it's done (like boolean zipDone=true), and at this point the main thread can react to that (display "Zip extracted" in the GUI etc).

\$\endgroup\$
1
  • 2
    \$\begingroup\$ It would appear so, but WaitForComplete is a Unity3D coroutine, so it only will run one iteration of the while loop every frame, then yields to let everything else in the game update. It's not blocking the thread :) \$\endgroup\$ Mar 13, 2013 at 12:55
-2
\$\begingroup\$

You don't need to lock _isDone, but it does need to be marked volatile.

\$\endgroup\$

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .