tim tregubov

tell me a story

Wrapping Unity C# Coroutines for Exception Handling, Value Retrieval, and Locking

Firstly, coroutines are awesome. If you aren’t familiar with them (particularly in the context of Unity3d) then you should be. In short, coroutines are methods that can suspend and resume execution. In the context of Unity what this means is that you can have methods that appear to run concurrently. The Unity documentation has some examples.

Coroutines are the way to script a lot of things in Unity, however there are a few problems that you may run into if you use them heavily: exception handling, return values, and locking. Especially if you use nested coroutines!

Return Values

Coroutines in Unity, although built on iterators, don’t handle return values. Even though it appears as if you can return a value, internally return values are used to keep track of where to resume. But, “I want a coroutine to return something!”, you say. So you work around that using a callback:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
IEnumerator FooBar()
{
  yield return StartCoroutine(DoSomethingEverySecond(success=> {
      if (success)
          doMoreStuff;
  }));
}

IEnumerator DoSomethingEverySecond(System.Action<bool> success)
{
  bool result = false;
  foreach (Something s in somethings)
  {
      if (s.doStuff())
          result = true;
      yield return new WaitForSeconds(1f);
  }
  if (success != null) success(result);
}

Ok so this helps with getting a return value out of a coroutine. It works well enough. Only caveat is in the callback code you can’t do another yield, not a big problem, and solveable by setting some variables. We’ll return to this topic.

Exception Handling

There is no exception handling at the coroutine level. You can’t put a yield statement in a try…catch block. This in particular can cause a problem if you have nested coroutines:

1
2
3
4
5
6
7
8
9
10
11
12
13
void Start()
{
  try { StartCoroutine(ParentCoroutine()); } //can try...catch if there's no yielding
  catch (Exception e) { Debug.LogError("oh noes we had a problem: " + e.Message); }
}

IEnumerator ParentCoroutine()
{
  yield return StartCoroutine(NestedCoroutineDoSomeStuff());  //can't try...catch these
  yield return StartCoroutine(NestedCoroutineFoo()); //if this has an exception nothing below executes
  if (stuff)
      yield return StartCoroutine(NestedCoroutineBar());
}

Logically, if any of the nested coroutines throw an exception then the parent will stop execution. Now, this is only fair, but since you can’t try…catch the nested coroutines it forces you to have to do error handling at a much more obnoxious nested level. You have to make sure to handle all exceptions in each subroutine, even if it doesn’t really make sense to do that. For instance you have a MovePiece coroutine, if it fails (say the piece was attacked mid move) what you’d like to do in the parent coroutine (which is something like AttackState) is notice the failed move and handle it there. If the MovePiece coroutine does a bunch of stuff (set some flags, run a few different animations) the parent probably only cares whether it was successfull or not. You could use a callback like above to indicate success but, this doesn’t help if MovePiece threw an exception. In that case the parent coroutine exits and we’re stuck handling the exception at too high a level (say in a non-coroutine above the parent). Not ideal.

Ideally we’d be able to catch any exception per coroutine (nested or not) allowing us to think of each coroutine as a logical unit as it should be. A great solution for this exists. Part of this solution is also a better way to handle return values without having to use a callback. This uses a parametrized wrapper to Coroutine and allows us to handle exceptions and return values like so:

1
2
3
4
5
6
7
8
9
10
11
Coroutine<bool> dostuff = StartCoroutine<bool>(DoStuff()); // declare a return value type
yield return dostuff.coroutine;
try
{
  if (dostuff.Value)  //attempt to access the return value
      NextStuff();
}
catch
{
  //and handle any exceptions here
}

That is much better! Now the child exits without interrupting the parent and the parent can deal with it appropriately (exceptions and return values). Now, what’s this about locking?

Locking

The other problem is locking. Coroutines (although technically not multi-threaded in Unity) do cause concurrency problems! What if, based on user input, you start a Coroutine multiple times and it operates on the same objects? A mess can ensue. One solution in Unity is that you can start a coroutine using a string name of the IEnumerator method. This uses reflection and is yucky. Who want to use strings when you’re using a nice strongly typed language like C#? But, if you use strings you can do StopCoroutine("DoStuff"); StartCoroutine("DoStuff"). Another option is to just have some instance variable bool and appropriate logic for every coroutine. This too gets messy quickly with lots of coroutines.

These techniques get you part of the way but what if you want a coroutine to actually just block/wait for a previous instance to finish running like traditional locking but without any undue messiness? Well using lock(someobject) doesn’t work, because it would only be evaluated once and isn’t checked on the coroutine resume. Some people have written large coroutine managers that help with all these problems but those seemed like overkill so I built on the exception handling stuff and added locking support. The idea is that you start the coroutine with a string identifier (if none is given no locking is performed) and an optional timeout period. With the string identifier each MonoBehavior keeps the currently running coroutines in a queue and either yields (analogous to blocking on a lock) until it’s turn comes up or bails on a timeout. Like so:

1
2
StartCoroutine(DoStuff(), "DoStuff", 10f); // will yield null
                                           // if any previous "DoStuff"s are running up to 10seconds

The easiest way to use this is to extend MonoBehaviour and use the new class as the base class for all your MonoBehaviours. Here is mine:

My MonoBehaviour Base Class (TTMonoBehaviour.cs) download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
using UnityEngine;
using System;
using System.Collections;
using System.Collections.Generic;

/// <summary>
/// Extending MonoBehaviour to add some extra functionality
/// Exception handling from: http://twistedoakstudios.com/blog/Post83_coroutines-more-than-you-want-to-know
/// 
/// 2013 Tim Tregubov
/// </summary>
public class TTMonoBehaviour : MonoBehaviour
{
  private LockQueue LockedCoroutineQueue { get; set; }
          
  /// <summary>
  /// Coroutine with return value AND exception handling on the return value. 
  /// </summary>
  public Coroutine<T> StartCoroutine<T>(IEnumerator coroutine)
  {
      Coroutine<T> coroutineObj = new Coroutine<T>();
      coroutineObj.coroutine = base.StartCoroutine(coroutineObj.InternalRoutine(coroutine));
      return coroutineObj;
  }
  
  /// <summary>
  /// Lockable coroutine. Can either wait for a previous coroutine to finish or a timeout or just bail if previous one isn't done.
  /// Caution: the default timeout is 10 seconds. Coroutines that timeout just drop so if its essential increase this timeout.
  /// Set waitTime to 0 for no wait
  /// </summary>
  public Coroutine<T> StartCoroutine<T>(IEnumerator coroutine, string lockID, float waitTime = 10f)
  {
      if (LockedCoroutineQueue == null) LockedCoroutineQueue = new LockQueue();
      Coroutine<T> coroutineObj = new Coroutine<T>(lockID, waitTime, LockedCoroutineQueue);
      coroutineObj.coroutine = base.StartCoroutine(coroutineObj.InternalRoutine(coroutine));
      return coroutineObj;
  }
  
  /// <summary>
  /// Coroutine with return value AND exception handling AND lockable
  /// </summary>
  public class Coroutine<T>
  {
      private T returnVal;
      private Exception e;
      private string lockID;
      private float waitTime;
      
      private LockQueue lockedCoroutines; //reference to objects lockdict
      private bool lockable;
      
      public Coroutine coroutine;
      public T Value
      {
          get
          {
              if (e != null)
              {
                  throw e;
              }
              return returnVal;
          }
      }
      
      public Coroutine() { lockable = false; }
      public Coroutine(string lockID, float waitTime, LockQueue lockedCoroutines)
      {
          this.lockable = true;
          this.lockID = lockID;
          this.lockedCoroutines = lockedCoroutines;
          this.waitTime = waitTime;
      }
      
      public IEnumerator InternalRoutine(IEnumerator coroutine)
      {
          if (lockable && lockedCoroutines != null)
          {        
              if (lockedCoroutines.Contains(lockID))
              {
                  if (waitTime == 0f)
                  {
                      //Debug.Log(this.GetType().Name + ": coroutine already running and wait not requested so exiting: " + lockID);
                      yield break;
                  }
                  else
                  {
                      //Debug.Log(this.GetType().Name + ": previous coroutine already running waiting max " + waitTime + " for my turn: " + lockID);
                      float starttime = Time.time;
                      float counter = 0f;
                      lockedCoroutines.Add(lockID, coroutine);
                      while (!lockedCoroutines.First(lockID, coroutine) && (Time.time - starttime) < waitTime)
                      {
                          yield return null;
                          counter += Time.deltaTime;
                      }
                      if (counter >= waitTime)
                      {
                          string error = this.GetType().Name + ": coroutine " + lockID + " bailing! due to timeout: " + counter;
                          Debug.LogError(error);
                          this.e = new Exception(error);
                          lockedCoroutines.Remove(lockID, coroutine);
                          yield break;
                      }
                  }
              }
              else
              {
                  lockedCoroutines.Add(lockID, coroutine);
              }
          }
          
          while (true)
          {
              try
              {
                  if (!coroutine.MoveNext())
                  {
                      if (lockable) lockedCoroutines.Remove(lockID, coroutine);
                      yield break;
                  }
              }
              catch (Exception e)
              {
                  this.e = e;
                  Debug.LogError(this.GetType().Name + ": caught Coroutine exception! " + e.Message + "\n" + e.StackTrace);
                  if (lockable) lockedCoroutines.Remove(lockID, coroutine);
                  yield break;
              }
              
              object yielded = coroutine.Current;
              if (yielded != null && yielded.GetType() == typeof(T))
              {
                  returnVal = (T)yielded;
                  if (lockable) lockedCoroutines.Remove(lockID, coroutine);
                  yield break;
              }
              else
              {
                  yield return coroutine.Current;
              }
          }
      }
  }
  
  
  /// <summary>
  /// coroutine lock and queue
  /// </summary>
  public class LockQueue
  {
      private Dictionary<string, List<IEnumerator>> LockedCoroutines { get; set; }
      
      public LockQueue()
      {
          LockedCoroutines = new Dictionary<string, List<IEnumerator>>();
      }
      
      /// <summary>
      /// check if LockID is locked
      /// </summary>
      public bool Contains(string lockID)
      {
          return LockedCoroutines.ContainsKey(lockID);
      }
      
      /// <summary>
      /// check if given coroutine is first in the queue
      /// </summary>
      public bool First(string lockID, IEnumerator coroutine)
      {
          bool ret = false;
          if (Contains(lockID))
          {
              if (LockedCoroutines[lockID].Count > 0)
              {
                  ret = LockedCoroutines[lockID][0] == coroutine;
              }
          }
          return ret;
      }
      
      /// <summary>
      /// Add the specified lockID and coroutine to the coroutine lockqueue
      /// </summary>
      public void Add(string lockID, IEnumerator coroutine)
      {
          if (!LockedCoroutines.ContainsKey(lockID))
          {
              LockedCoroutines.Add(lockID, new List<IEnumerator>());
          }
          
          if (!LockedCoroutines[lockID].Contains(coroutine))
          {
              LockedCoroutines[lockID].Add(coroutine);
          }
      }
      
      /// <summary>
      /// Remove the specified coroutine and queue if empty
      /// </summary>
      public bool Remove(string lockID, IEnumerator coroutine)
      {
          bool ret = false;
          if (LockedCoroutines.ContainsKey(lockID))
          {
              if (LockedCoroutines[lockID].Contains(coroutine))
              {
                  ret = LockedCoroutines[lockID].Remove(coroutine);
              }
              
              if (LockedCoroutines[lockID].Count == 0)
              {
                  ret = LockedCoroutines.Remove(lockID);
              }
          }
          return ret;
      }
      
  }

}

What do you think?

Comments