Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

Best practices: Async vs. coroutines - Unite Copenhagen 2019

9,229 views

Published on

Before async was introduced in Unity 2017, asynchronous routines were implemented using coroutines and/or callbacks. This video covers the benefits of async over coroutines. You'll see how one example problem – building an asynchronous prompt popup – can be solved using async vs coroutines.

Speaker:
Johannes Ahvenniemi - Seriously Digital Entertainment

Watch the session here: https://youtu.be/7eKi6NKri6I

Published in: Technology
  • My personal experience with research paper writing services was highly positive. I sent a request to ⇒ www.HelpWriting.net ⇐ and found a writer within a few minutes. Because I had to move house and I literally didn’t have any time to sit on a computer for many hours every evening. Thankfully, the writer I chose followed my instructions to the letter. I know we can all write essays ourselves. For those in the same situation I was in, I recommend ⇒ www.HelpWriting.net ⇐.
       Reply 
    Are you sure you want to  Yes  No
    Your message goes here
  • You can try to use this service ⇒ www.WritePaper.info ⇐ I have used it several times in college and was absolutely satisfied with the result.
       Reply 
    Are you sure you want to  Yes  No
    Your message goes here
  • Would you like to earn extra cash ✤✤✤ https://dwz1.cc/v5Fcq3Qr
       Reply 
    Are you sure you want to  Yes  No
    Your message goes here
  • If you’re looking for a great essay service then you should check out ⇒ www.HelpWriting.net ⇐. A friend of mine asked them to write a whole dissertation for him and he said it turned out great! Afterwards I also ordered an essay from them and I was very happy with the work I got too.
       Reply 
    Are you sure you want to  Yes  No
    Your message goes here
  • My only statement is "WOW"...I thought your other systems were special but this is going to turn out to be the "Holy Grail" of all MLB systems, no doubt! ■■■ http://t.cn/A6zP24pL
       Reply 
    Are you sure you want to  Yes  No
    Your message goes here

Best practices: Async vs. coroutines - Unite Copenhagen 2019

  1. 1. Async vs Coroutines: Best Practices Johannes Ahvenniemi Lead Programmer, Seriously Digital Entertainment
  2. 2. About me 3 — Started with: BASIC — First published “indie” game: Java — First professional job: ActionScript 3 (Flash) — Favourite language: C++ — More tools in the box: C#, Clojure, Lua, JavaScript, OpenGL… — Unity developer since 2014 — Struggling with “callback hell”
  3. 3. Coroutines 5 — Useful for spanning logic across multiple frames — Cooperative multitasking (Single threaded) — Clever utilisation of C# generators (IEnumerators) — This talk is about Unity’s Coroutines, not generators — Familiar from Go, Lua, Python and many other languages
  4. 4. async/await 6 — Not intended for replacing Coroutines — Useful for handling non-blocking IO (and long lasting routines, i.e. threads) — Old C# feature (originating from F# (originating from Haskell)) — Now also in JavaScript, Python, Dart, C++, Rust… — Finally introduced in Unity 2017
  5. 5. Comparison
  6. 6. Coroutine Async Doesn’t have return values Has return values Can’t run synchronously Can run synchronously Doesn’t work without the engine Works without the engine Doesn’t support try/catch Supports try/catch Doesn’t preserve call stack Preserves call stack Always shows exceptions Can hide exceptions Doesn’t always exit Always exits Lifetime tied to a MonoBehaviour Lifetime handled manually Familiar to most Unity developers Unfamiliar to many Unity developers Coroutine Async Doesn’t have return values Has return values Can’t run synchronously Can run synchronously Doesn’t work without the engine Works without the engine Doesn’t support try/catch Supports try/catch Doesn’t preserve call stack Preserves call stack Always shows exceptions Can hide exceptions Doesn’t always exit Always exits Lifetime tied to a MonoBehaviour Lifetime handled manually Familiar to most Unity developers Unfamiliar to many Unity developers
  7. 7. Coroutine Async Doesn’t have return values Has return values Can’t run synchronously Can run synchronously Doesn’t work without the engine Works without the engine Doesn’t support try/catch Supports try/catch Doesn’t preserve call stack Preserves call stack Always shows exceptions Can hide exceptions Doesn’t always exit Always exits Lifetime tied to a MonoBehaviour Lifetime handled manually Familiar to most Unity developers Unfamiliar to many Unity developers Coroutine Async Doesn’t have return values Has return values Can’t run synchronously Can run synchronously Doesn’t work without the engine Works without the engine Doesn’t support try/catch Supports try/catch Doesn’t preserve call stack Preserves call stack Always shows exceptions Can hide exceptions Doesn’t always exit Always exits Lifetime tied to a MonoBehaviour Lifetime handled manually Familiar to most Unity developers Unfamiliar to many Unity developers
  8. 8. 10 Disclaimer: The following examples aren’t production quality. They are full of memory leaks and bad practices. Don’t just copy these examples into your product!
  9. 9. Coroutine Doesn’t have return values IEnumerator Start() { IEnumerator enumerator = SelectButton(); yield return enumerator; Button selectedButton = (Button)enumerator.Current; Assert.IsTrue(selectedButton != null); Debug.Log(selectedButton.name); } static IEnumerator SelectButton() { Button selectedButton = null; Button[] buttons = FindObjectsOfType<Button>(); foreach (Button button in buttons) { Button b = button; b.onClick.AddListener(() => selectedButton = b); } while (selectedButton == null) { yield return null; } yield return selectedButton; } 11 Async Has return values async void Start() { Button selectedButton = await SelectButton(); Debug.Log(selectedButton.name); } static async Task<Button> SelectButton() { Button[] buttons = FindObjectsOfType<Button>(); var tasks = buttons.Select(PressButton); Task<Button> finishedTask = await Task.WhenAny(tasks); return finishedTask.Result; } static async Task<Button> PressButton(Button button) { bool isPressed = false; button.onClick.AddListener(() => isPressed = true); while (!isPressed) { await Task.Yield(); } return button; }
  10. 10. Coroutine Async Doesn’t have return values Has return values Can’t run synchronously Can run synchronously Doesn’t work without the engine Works without the engine Doesn’t support try/catch Supports try/catch Doesn’t preserve call stack Preserves call stack Always shows exceptions Can hide exceptions Doesn’t always exit Always exits Lifetime tied to a MonoBehaviour Lifetime handled manually Familiar to most Unity developers Unfamiliar to many Unity developers Coroutine Async Doesn’t have return values Has return values Can’t run synchronously Can run synchronously Doesn’t work without the engine Works without the engine Doesn’t support try/catch Supports try/catch Doesn’t preserve call stack Preserves call stack Always shows exceptions Can hide exceptions Doesn’t always exit Always exits Lifetime tied to a MonoBehaviour Lifetime handled manually Familiar to most Unity developers Unfamiliar to many Unity developers
  11. 11. Coroutine Can’t run synchronously IEnumerator Start() { Debug.Log($"Frame: {Time.frameCount}"); yield return Function(); Debug.Log($"Frame: {Time.frameCount}"); } static IEnumerator Function() { if (false) yield return null; } 13 Async Can run synchronously async void Start() { Debug.Log($"Frame: {Time.frameCount}"); await Function(); Debug.Log($"Frame: {Time.frameCount}"); } static async Task Function() { if (false) await Task.Yield(); } Outputs: Frame: 1 Frame: 2 Outputs: Frame: 1 Frame: 1
  12. 12. Coroutine Async Doesn’t have return values Has return values Can’t run synchronously Can run synchronously Doesn’t work without the engine Works without the engine Doesn’t support try/catch Supports try/catch Doesn’t preserve call stack Preserves call stack Always shows exceptions Can hide exceptions Doesn’t always exit Always exits Lifetime tied to a MonoBehaviour Lifetime handled manually Familiar to most Unity developers Unfamiliar to many Unity developers Coroutine Async Doesn’t have return values Has return values Can’t run synchronously Can run synchronously Doesn’t work without the engine Works without the engine Doesn’t support try/catch Supports try/catch Doesn’t preserve call stack Preserves call stack Always shows exceptions Can hide exceptions Doesn’t always exit Always exits Lifetime tied to a MonoBehaviour Lifetime handled manually Familiar to most Unity developers Unfamiliar to many Unity developers
  13. 13. 15
  14. 14. Coroutine Async Doesn’t have return values Has return values Can’t run synchronously Can run synchronously Doesn’t work without the engine Works without the engine Doesn’t support try/catch Supports try/catch Doesn’t preserve call stack Preserves call stack Always shows exceptions Can hide exceptions Doesn’t always exit Always exits Lifetime tied to a MonoBehaviour Lifetime handled manually Familiar to most Unity developers Unfamiliar to many Unity developers Coroutine Async Doesn’t have return values Has return values Can’t run synchronously Can run synchronously Doesn’t work without the engine Works without the engine Doesn’t support try/catch Supports try/catch Doesn’t preserve call stack Preserves call stack Always shows exceptions Can hide exceptions Doesn’t always exit Always exits Lifetime tied to a MonoBehaviour Lifetime handled manually Familiar to most Unity developers Unfamiliar to many Unity developers
  15. 15. Coroutine Doesn’t support try/catch public class Root : MonoBehaviour { IEnumerator Start() { try { yield return MainMenu(); } catch (Exception) { SceneManager.LoadScene("Error"); throw; } } } 17 Async Supports try/catch public class Root : MonoBehaviour { async void Start() { try { await MainMenu(); } catch (Exception) { SceneManager.LoadScene("Error"); throw; } } } Compiler error! Works!
  16. 16. Coroutine Async Doesn’t have return values Has return values Can’t run synchronously Can run synchronously Doesn’t work without the engine Works without the engine Doesn’t support try/catch Supports try/catch Doesn’t preserve call stack Preserves call stack Always shows exceptions Can hide exceptions Doesn’t always exit Always exits Lifetime tied to a MonoBehaviour Lifetime handled manually Familiar to most Unity developers Unfamiliar to many Unity developers Coroutine Async Doesn’t have return values Has return values Can’t run synchronously Can run synchronously Doesn’t work without the engine Works without the engine Doesn’t support try/catch Supports try/catch Doesn’t preserve call stack Preserves call stack Always shows exceptions Can hide exceptions Doesn’t always exit Always exits Lifetime tied to a MonoBehaviour Lifetime handled manually Familiar to most Unity developers Unfamiliar to many Unity developers
  17. 17. 19 Coroutine Doesn’t preserve call stack Async Preserves call stack
  18. 18. 20 Coroutine Doesn’t preserve call stack Async Preserves call stack
  19. 19. 21 Coroutine Doesn’t preserve call stack Async Preserves call stack
  20. 20. 22 Async call stack in the profiler :(
  21. 21. 23 Mind the stack! Watch out for asset references in the stack! UnloadUnusedAssets will unload the assets but might cause surprises My suggestion: Don’t await non-additive scenes from non-additive scenes
  22. 22. Coroutine Async Doesn’t have return values Has return values Can’t run synchronously Can run synchronously Doesn’t work without the engine Works without the engine Doesn’t support try/catch Supports try/catch Doesn’t preserve call stack Preserves call stack Always shows exceptions Can hide exceptions Doesn’t always exit Always exits Lifetime tied to a MonoBehaviour Lifetime handled manually Familiar to most Unity developers Unfamiliar to many Unity developers Coroutine Async Doesn’t have return values Has return values Can’t run synchronously Can run synchronously Doesn’t work without the engine Works without the engine Doesn’t support try/catch Supports try/catch Doesn’t preserve call stack Preserves call stack Always shows exceptions Can hide exceptions Doesn’t always exit Always exits Lifetime tied to a MonoBehaviour Lifetime handled manually Familiar to most Unity developers Unfamiliar to many Unity developers
  23. 23. Coroutine Always shows exceptions void Start() { this.StartCoroutine(Function()); } static IEnumerator Function() { yield return null; Debug.Log("About to throw exception"); throw new Exception(); } 25 Async Can hide exceptions void Start() { Function(); } static async Task Function() { await Task.Yield(); Debug.Log("About to throw exception"); throw new Exception(); }
  24. 24. Coroutine Async Doesn’t have return values Has return values Can’t run synchronously Can run synchronously Doesn’t work without the engine Works without the engine Doesn’t support try/catch Supports try/catch Doesn’t preserve call stack Preserves call stack Always shows exceptions Can hide exceptions Doesn’t always exit Always exits Lifetime tied to a MonoBehaviour Lifetime handled manually Familiar to most Unity developers Unfamiliar to many Unity developers Coroutine Async Doesn’t have return values Has return values Can’t run synchronously Can run synchronously Doesn’t work without the engine Works without the engine Doesn’t support try/catch Supports try/catch Doesn’t preserve call stack Preserves call stack Always shows exceptions Can hide exceptions Doesn’t always exit Always exits Lifetime tied to a MonoBehaviour Lifetime handled manually Familiar to most Unity developers Unfamiliar to many Unity developers
  25. 25. Coroutine Doesn’t always exit IEnumerator ShowEffect(RawImage container) { var texture = new RenderTexture(512, 512, 0); try { container.texture = texture; for (int i = 0; i < 100; ++i) { /* * Update effect */ yield return null; } } finally { texture.Release(); } } 27 Async Always exits async Task ShowEffect(RawImage container) { var texture = new RenderTexture(512, 512, 0); try { container.texture = texture; for (int i = 0; i < 100; ++i) { /* * Update effect */ await Task.Yield(); } } finally { texture.Release(); } } Leaks if owning MonoBehaviour is destroyed! Never leaks!
  26. 26. Coroutine Async Doesn’t have return values Has return values Can’t run synchronously Can run synchronously Doesn’t work without the engine Works without the engine Doesn’t support try/catch Supports try/catch Doesn’t preserve call stack Preserves call stack Always shows exceptions Can hide exceptions Doesn’t always exit Always exits Lifetime tied to a MonoBehaviour Lifetime handled manually Familiar to most Unity developers Unfamiliar to many Unity developers Coroutine Async Doesn’t have return values Has return values Can’t run synchronously Can run synchronously Doesn’t work without the engine Works without the engine Doesn’t support try/catch Supports try/catch Doesn’t preserve call stack Preserves call stack Always shows exceptions Can hide exceptions Doesn’t always exit Always exits Lifetime tied to a MonoBehaviour Lifetime handled manually Familiar to most Unity developers Unfamiliar to many Unity developers
  27. 27. Coroutine Lifetime tied to a MonoBehaviour IEnumerator Start() { this.StartCoroutine(this.Rotate()); yield return new WaitForSeconds(1.0f); Destroy(gameObject); } IEnumerator Rotate() { while (true) { transform.Rotate(Vector3.forward, 1.0f); yield return null; } } 29 Async Lifetime handled manually async void Start() { this.Rotate(); await Task.Delay(1000); Destroy(gameObject); } async void Rotate() { while (true) { transform.Rotate(Vector3.forward, 1.0f); await Task.Yield(); } } Throws NullRef Exception!Stops silently!
  28. 28. 30 Async Lifetime handled manually Fixed? No. async void Start() { var cts = new CancellationTokenSource(); this.Rotate(cts.Token); await Task.Delay(1000); cts.Cancel(); Destroy(this.gameObject); } async void Rotate(CancellationToken ct) { while (!ct.IsCancellationRequested) { this.transform.Rotate(Vector3.forward, 1.0f); await Task.Yield(); } }
  29. 29. Coroutine Async Doesn’t have return values Has return values Can’t run synchronously Can run synchronously Doesn’t work without the engine Works without the engine Doesn’t support try/catch Supports try/catch Doesn’t preserve call stack Preserves call stack Always shows exceptions Can hide exceptions Doesn’t always exit Always exits Lifetime tied to a MonoBehaviour Lifetime handled manually Familiar to most Unity developers Unfamiliar to many Unity developers Coroutine Async Doesn’t have return values Has return values Can’t run synchronously Can run synchronously Doesn’t work without the engine Works without the engine Doesn’t support try/catch Supports try/catch Doesn’t preserve call stack Preserves call stack Always shows exceptions Can hide exceptions Doesn’t always exit Always exits Lifetime tied to a MonoBehaviour Lifetime handled manually Familiar to most Unity developers Unfamiliar to many Unity developers
  30. 30. Example: Creating a prompt popup 32
  31. 31. 33
  32. 32. public class PromptPopup1 : MonoBehaviour { public Button buttonAccept; public Button buttonCancel; public Button buttonClose; public static async Task<bool> Spawn() { PromptPopup1 p = await Utils.LoadScene<PromptPopup1>("Popup", LoadSceneMode.Additive); Button selected = await Utils.SelectButton(p.buttonAccept, p.buttonCancel, p.buttonClose); return selectedButton == p.buttonAccept; } } 34 Simple prompt popup
  33. 33. public class PromptPopup2 : MonoBehaviour { public Button buttonAccept; public Button buttonCancel; public Button buttonClose; public static async Task<bool> Spawn() { PromptPopup2 p = await Utils.LoadScene<PromptPopup2>("Popup", LoadSceneMode.Additive); Task<Button> selectButtonTask = Utils.SelectButton(p.buttonAccept, p.buttonCancel, p.buttonClose); Task pressBackButtonTask = Utils.WaitForPressBackButton(); Task finishedTask = await Task.WhenAny(selectButtonTask, pressBackButtonTask); await finishedTask; // Propagate exception if the task finished because of exception return finishedTask == selectButtonTask && selectButtonTask.Result == p.buttonAccept; } } 35 Implement Android back button
  34. 34. 36 Implement Android back button public class PromptPopup2 : MonoBehaviour { public Button buttonAccept; public Button buttonCancel; public Button buttonClose; public static async Task<bool> Spawn() { PromptPopup2 p = await Utils.LoadScene<PromptPopup2>("Popup", LoadSceneMode.Additive); Task<Button> selectButtonTask = Utils.SelectButton(p.buttonAccept, p.buttonCancel, p.buttonClose); Task pressBackButtonTask = Utils.WaitForPressBackButton(); Task finishedTask = await Task.WhenAny(selectButtonTask, pressBackButtonTask); await finishedTask; // Propagate exception if the task finished because of exception return finishedTask == selectButtonTask && selectButtonTask.Result == p.buttonAccept; } }
  35. 35. public class PromptPopup3 : MonoBehaviour { public Button buttonAccept; public Button buttonCancel; public Button buttonClose; public static async Task<bool> Spawn(CancellationToken ct) { PromptPopup3 p = await Utils.LoadScene<PromptPopup3>("Popup", LoadSceneMode.Additive, ct); var selectButtonTask = Utils.SelectButton(ct, p.buttonAccept, p.buttonCancel, p.buttonClose); var pressBackButtonTask = Utils.WaitForPressBackButton(ct); var finishedTask = await Task.WhenAny(selectButtonTask, pressBackButtonTask); await finishedTask; // Propagate exception if the task finished because of exception return finishedTask == selectButtonTask && selectButtonTask.Result == p.buttonAccept; } } 37 Implement manual cancellation
  36. 36. public class PromptPopup4 : MonoBehaviour { public Button buttonAccept; public Button buttonCancel; public Button buttonClose; public static async Task<bool> Spawn(CancellationToken ct) { PromptPopup4 p = await Utils.LoadScene<PromptPopup4>("Popup", LoadSceneMode.Additive, ct); var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(ct); var linkedCt = linkedCts.Token; var buttonTask = Utils.SelectButton(linkedCt, p.buttonAccept, p.buttonCancel, p.buttonClose); var pressBackButtonTask = Utils.WaitForPressBackButton(linkedCt); var finishedTask = await Task.WhenAny(buttonTask, pressBackButtonTask); await finishedTask; // Propagate exception if the task finished because of exception linkedCts.Cancel(); return finishedTask == buttonTask && buttonTask.Result == p.buttonAccept; } } 38 Clean up remaining tasks
  37. 37. public class PromptPopup5 : MonoBehaviour { public Button buttonAccept; public Button buttonCancel; public Button buttonClose; public static async Task<bool> Spawn(CancellationToken ct) { PromptPopup5 p = await Utils.LoadScene<PromptPopup5>("Popup", LoadSceneMode.Additive, ct); using (var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(ct)) { var linkedCt = linkedCts.Token; var buttonTask = Utils.SelectButton(linkedCt, p.buttonAccept, p.buttonCancel, p.buttonClose); var pressBackButtonTask = Utils.WaitForPressBackButton(linkedCt); var finishedTask = await Task.WhenAny(buttonTask, pressBackButtonTask); await finishedTask; // Propagate exception if the task finished because of exception linkedCts.Cancel(); return finishedTask == buttonTask && buttonTask.Result == p.buttonAccept; } } } 39 Dispose linked CancellationTokenSource
  38. 38. public static async Task<bool> Spawn(CancellationToken ct) { PromptPopup6 p = await Utils.LoadScene<PromptPopup6>("Popup", LoadSceneMode.Additive, ct); var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(ct); try { var linkedCt = linkedCts.Token; var buttonTask = Utils.SelectButton(linkedCt, p.buttonAccept, p.buttonCancel, p.buttonClose); var pressBackButtonTask = Utils.WaitForPressBackButton(linkedCt); var finishedTask = await Task.WhenAny(buttonTask, pressBackButtonTask); await finishedTask; // Propagate exception if the task finished because of exception linkedCts.Cancel(); return finishedTask == buttonTask && buttonTask.Result == popup.buttonAccept; } finally { linkedCts.Dispose(); popup.StartCoroutine(Utils.PlayPopupCloseAnimation(p.gameObject)); } } 40 Implement closing the popup
  39. 39. public static async Task<bool> Spawn(CancellationToken ct) { PromptPopup6 p = await Utils.LoadScene<PromptPopup6>("Popup", LoadSceneMode.Additive, ct); var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(ct); try { var linkedCt = linkedCts.Token; var buttonTask = Utils.SelectButton(linkedCt, p.buttonAccept, p.buttonCancel, p.buttonClose); var pressBackButtonTask = Utils.WaitForPressBackButton(linkedCt); var finishedTask = await Task.WhenAny(buttonTask, pressBackButtonTask); await finishedTask; // Propagate exception if the task finished because of exception linkedCts.Cancel(); return finishedTask == buttonTask && buttonTask.Result == popup.buttonAccept; } finally { linkedCts.Dispose(); popup.StartCoroutine(Utils.PlayPopupCloseAnimation(p.gameObject)); } } 41 Implement closing the popup
  40. 40. 42
  41. 41. Choosing between Async and Coroutines 43
  42. 42. Choosing between Async and Coroutines Rules of Thumb 44 1. Use async when you deal with IO (User input, Network calls) 2. Use coroutines for fire-and-forget routines. 3. Don’t intertwine them
  43. 43. Thank you! Johannes Ahvenniemi Lead Programmer, Seriously Digital Entertainment johannes@seriously.com
  44. 44. We are hiring! Johannes Ahvenniemi Lead Programmer, Seriously Digital Entertainment johannes@seriously.com

×